-
Notifications
You must be signed in to change notification settings - Fork 196
Arc 1192 update backfilling status #2482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 58 commits
28ee606
bb37377
44a779b
c8b0cf1
93cb05e
e7cb1c7
291ac6d
973ee29
99aec7a
603dc2a
3d7ddb7
e54d420
5094704
463fb6e
b9bcf6b
f123a7f
a5404df
07edd59
f3701e9
dd6c60a
1d61632
bc4286b
b267700
10aae00
195d2b6
c7312a4
2c4a3b1
7887a8f
637389d
63c421e
50c78f2
a023315
4df488e
09b5c30
9a5ed36
bec3e9c
d95c309
87dac3c
d98c7d5
a47cff2
8545cab
40fc18c
61e6241
2d874a8
7138c4b
dca1b90
d332cec
20ea125
f8615bb
a99a7ff
208e8f9
279e950
737f208
cd2ba30
6d04509
d56a78e
6b308f6
7929f06
9cfa423
7446db2
b1d3aa3
13677c9
5e1d563
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| import { getFrontendApp } from "~/src/app"; | ||
| import { Installation } from "models/installation"; | ||
| import { encodeSymmetric } from "atlassian-jwt"; | ||
| import { getLogger } from "config/logger"; | ||
| import { Subscription } from "models/subscription"; | ||
| import { DatabaseStateCreator } from "test/utils/database-state-creator"; | ||
| import supertest from "supertest"; | ||
| import { booleanFlag, BooleanFlags } from "config/feature-flags"; | ||
| import { when } from "jest-when"; | ||
| import { RepoSyncState } from "models/reposyncstate"; | ||
|
|
||
| jest.mock("config/feature-flags"); | ||
|
|
||
| describe("jira-get-connections-backfillStatus.test", () => { | ||
| let app; | ||
| let installation: Installation; | ||
| let subscription: Subscription; | ||
| let repoSyncState: RepoSyncState; | ||
| const generateJwt = async () => { | ||
| return encodeSymmetric( | ||
| { | ||
| qsh: "context-qsh", | ||
| iss: installation.plainClientKey, | ||
| sub: "myAccountId" | ||
| }, | ||
| await installation.decrypt("encryptedSharedSecret", getLogger("test")) | ||
| ); | ||
| }; | ||
|
|
||
| beforeEach(async () => { | ||
| app = getFrontendApp(); | ||
| const result = await new DatabaseStateCreator() | ||
| .withActiveRepoSyncState() | ||
| .create(); | ||
| installation = result.installation; | ||
| subscription = result.subscription; | ||
| repoSyncState = result.repoSyncState!; | ||
| when(booleanFlag) | ||
| .calledWith(BooleanFlags.JIRA_ADMIN_CHECK) | ||
| .mockResolvedValue(true); | ||
| }); | ||
|
|
||
| it("should return 401 when no JWT was provided", async () => { | ||
| const resp = await supertest(app).get( | ||
| `/jira/subscriptions/backfill-status/?subscriptionIds=${subscription.id}` | ||
| ); | ||
| expect(resp.status).toStrictEqual(401); | ||
| expect(resp.text).toBe("Unauthorised"); | ||
| }); | ||
|
|
||
| it("should return 403 when not an admin", async () => { | ||
| const resp = await supertest(app) | ||
| .get( | ||
| `/jira/subscriptions/backfill-status?subscriptionIds=${subscription.id}` | ||
| ) | ||
| .set( | ||
| "authorization", | ||
| `JWT ${await generateJwt()}` | ||
| ); | ||
| expect(resp.status).toStrictEqual(403); | ||
| }); | ||
|
|
||
| describe("admin and JWT are OK", () => { | ||
| beforeEach(() => { | ||
| const payload = { | ||
| accountId: "myAccountId", | ||
| globalPermissions: ["ADMINISTER"] | ||
| }; | ||
| jiraNock | ||
| .post("/rest/api/latest/permissions/check", payload) | ||
| .reply(200, { globalPermissions: ["ADMINISTER"] }); | ||
| }); | ||
|
|
||
| it("should return 400 when no subscriptions were found", async () => { | ||
| const resp = await supertest(app) | ||
| .get( | ||
| `/jira/subscriptions/backfill-status?subscriptionIds=${ | ||
| subscription.id + 1 | ||
| }` | ||
| ) | ||
| .set( | ||
| "authorization", | ||
| `JWT ${await generateJwt()}` | ||
| ); | ||
| expect(resp.status).toStrictEqual(400); | ||
| }); | ||
|
|
||
| it("should return 400 when no Missing Subscription IDs were found in query", async () => { | ||
| const resp = await supertest(app) | ||
| .get(`/jira/subscriptions/backfill-status`) | ||
| .set("authorization", `JWT ${await generateJwt()}`); | ||
| expect(resp.status).toStrictEqual(400); | ||
| expect(resp.text).toBe("Missing Subscription IDs"); | ||
| }); | ||
|
|
||
| it("should return 403 if the subscription belongs to a different user", async () => { | ||
| const result = await new DatabaseStateCreator() | ||
| .forJiraHost("https://another-one.atlassian.net") | ||
| .create(); | ||
| const resp = await supertest(app) | ||
| .get( | ||
| `/jira/subscriptions/backfill-status?subscriptionIds=${result.subscription.id}` | ||
| ) | ||
| .set( | ||
| "authorization", | ||
| `JWT ${await generateJwt()}` | ||
| ); | ||
|
|
||
| expect(resp.status).toStrictEqual(403); | ||
| }); | ||
|
|
||
| it("should return 200 if the subscription belongs to the same user", async () => { | ||
| const resp = await supertest(app) | ||
| .get( | ||
| `/jira/subscriptions/backfill-status?subscriptionIds=${subscription.id}` | ||
| ) | ||
| .set( | ||
| "authorization", | ||
| `JWT ${await generateJwt()}` | ||
| ); | ||
| expect(resp.status).toStrictEqual(200); | ||
| }); | ||
|
|
||
| describe("happy paths", () => { | ||
| beforeEach(async () => { | ||
| const newRepoSyncStatesData: any[] = []; | ||
| for (let newRepoStateNo = 1; newRepoStateNo < 50; newRepoStateNo++) { | ||
| const newRepoSyncState = { ...repoSyncState.dataValues }; | ||
| delete newRepoSyncState["id"]; | ||
| delete newRepoSyncState["commitStatus"]; | ||
| delete newRepoSyncState["branchStatus"]; | ||
| newRepoSyncState["repoId"] = repoSyncState.repoId + newRepoStateNo; | ||
| newRepoSyncState["repoName"] = | ||
| repoSyncState.repoName + newRepoStateNo.toString(); | ||
| newRepoSyncState["repoFullName"] = | ||
| repoSyncState.repoFullName + | ||
| String(newRepoStateNo).padStart(3, "0"); | ||
| if (newRepoStateNo === 1) { | ||
| newRepoSyncState["commitStatus"] = "pending"; | ||
| newRepoSyncState["branchStatus"] = "complete"; | ||
| newRepoSyncState["pullStatus"] = "complete"; | ||
| newRepoSyncState["buildStatus"] = "complete"; | ||
| newRepoSyncState["deploymentStatus"] = "pending"; | ||
| } else if (newRepoStateNo % 3 == 1) { | ||
| newRepoSyncState["commitStatus"] = "complete"; | ||
| newRepoSyncState["branchStatus"] = "complete"; | ||
| newRepoSyncState["pullStatus"] = "complete"; | ||
| newRepoSyncState["buildStatus"] = "complete"; | ||
| newRepoSyncState["deploymentStatus"] = "complete"; | ||
| } else if (newRepoStateNo % 3 == 2) { | ||
| newRepoSyncState["commitStatus"] = "failed"; | ||
| newRepoSyncState["branchStatus"] = "complete"; | ||
| newRepoSyncState["pullStatus"] = "complete"; | ||
| newRepoSyncState["buildStatus"] = "complete"; | ||
| newRepoSyncState["deploymentStatus"] = "failed"; | ||
| } | ||
| newRepoSyncStatesData.push(newRepoSyncState); | ||
| } | ||
| await RepoSyncState.bulkCreate(newRepoSyncStatesData); | ||
| }); | ||
|
|
||
| it("should return 200 if the subscription belongs to the same user", async () => { | ||
| const resp = await supertest(app) | ||
| .get( | ||
| `/jira/subscriptions/backfill-status?subscriptionIds=${subscription.id}` | ||
| ) | ||
| .set( | ||
| "authorization", | ||
| `JWT ${await generateJwt()}` | ||
| ); | ||
| expect(resp.status).toStrictEqual(200); | ||
|
|
||
| expect(resp.body).toMatchObject({ | ||
| data: { | ||
| subscriptions: { | ||
| [subscription.id]: { | ||
| isSyncComplete: false, | ||
| syncStatus: "IN PROGRESS", | ||
| totalRepos: 33, | ||
| syncedRepos: 17, | ||
| backfillSince: null | ||
| } | ||
| }, | ||
| isBackfillComplete: false, | ||
| subscriptionIds: [subscription.id] | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { NextFunction, Request, Response } from "express"; | ||
| import { groupBy } from "lodash"; | ||
| import { RepoSyncState } from "~/src/models/reposyncstate"; | ||
| import { Subscription, SyncStatus } from "~/src/models/subscription"; | ||
| import { | ||
| mapSyncStatus, | ||
| ConnectionSyncStatus, | ||
| getRetryableFailedSyncErrors | ||
| } from "~/src/util/github-installations-helper"; | ||
|
|
||
| type SubscriptionBackfillState = { | ||
| totalRepos?: number; | ||
| syncedRepos?: number; | ||
| syncStatus: ConnectionSyncStatus; | ||
| isSyncComplete: boolean; | ||
| backfillSince?: string; | ||
| failedSyncErrors?: Record<string, number>; | ||
| syncWarning?: string; | ||
| }; | ||
|
|
||
| type BackFillType = { | ||
| [key: string]: SubscriptionBackfillState; | ||
| }; | ||
|
|
||
| export const JiraGetConnectionsBackfillStatus = async ( | ||
| req: Request, | ||
| res: Response, | ||
| next: NextFunction | ||
| ): Promise<void> => { | ||
| try { | ||
| const { jiraHost: localJiraHost } = res.locals; | ||
| const subscriptionIds = String(req.query?.subscriptionIds) | ||
| .split(",") | ||
| .map(Number) | ||
| .filter(Boolean); | ||
|
|
||
| if (subscriptionIds.length === 0) { | ||
| req.log.warn("Missing Subscription IDs"); | ||
| res.status(400).send("Missing Subscription IDs"); | ||
| return; | ||
| } | ||
|
|
||
| const subscriptions = await Subscription.findAll({ | ||
| where: { | ||
| id: subscriptionIds | ||
| } | ||
| }); | ||
| const resultSubscriptionIds = subscriptions.map( | ||
| (subscription) => subscription.id | ||
| ); | ||
|
|
||
| if (!subscriptions || subscriptions.length === 0) { | ||
| req.log.error("Missing Subscription"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have an enum set up for this in error.ts. Just import |
||
| res.status(400).send("Missing Subscription"); | ||
| return; | ||
| } | ||
|
|
||
| const jiraHosts = subscriptions.map( | ||
| (subscription) => subscription?.jiraHost | ||
| ); | ||
|
|
||
| const jiraHostsMatched = jiraHosts.every( | ||
kamaksheeAtl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| (jiraHost) => jiraHost === localJiraHost | ||
| ); | ||
|
|
||
| if (!jiraHostsMatched) { | ||
| req.log.error("mismatched Jira Host"); | ||
| res.status(403).send("mismatched Jira Host"); | ||
| return; | ||
| } | ||
| const subscriptionsById = groupBy(subscriptions, "id"); | ||
| const backfillStatus = await getBackfillStatus(subscriptionsById); | ||
| const isBackfillComplete = getBackfillCompletionStatus(backfillStatus); | ||
| res.status(200).send({ | ||
| data: { | ||
| subscriptions: backfillStatus, | ||
| isBackfillComplete, | ||
| subscriptionIds: resultSubscriptionIds | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| return next(new Error(`Failed to render connected repos`)); | ||
kamaksheeAtl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| }; | ||
|
|
||
| const getBackfillCompletionStatus = (backfillStatus: BackFillType): boolean => | ||
| Object.values(backfillStatus).every( | ||
| (backFill: SubscriptionBackfillState): boolean => backFill?.isSyncComplete | ||
| ); | ||
|
|
||
| const getBackfillStatus = async (subscriptionsById): Promise<BackFillType> => { | ||
|
||
| const backfillStatus: BackFillType = {}; | ||
| for (const subscriptionId in subscriptionsById) { | ||
| const subscription = subscriptionsById[subscriptionId][0]; | ||
| const isSyncComplete = | ||
| subscription?.syncStatus === SyncStatus.COMPLETE || | ||
| subscription?.syncStatus === SyncStatus.FAILED; | ||
| const failedSyncErrors = await getRetryableFailedSyncErrors(subscription); | ||
|
|
||
| backfillStatus[subscriptionId] = { | ||
| isSyncComplete, | ||
| syncStatus: mapSyncStatus(subscription?.syncStatus), | ||
| totalRepos: subscription?.totalNumberOfRepos, | ||
| syncedRepos: await RepoSyncState.countFullySyncedReposForSubscription( | ||
kamaksheeAtl marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| subscription | ||
| ), | ||
| failedSyncErrors, | ||
| backfillSince: subscription?.backfillSince || null, | ||
| syncWarning: subscription.syncWarning | ||
| }; | ||
| } | ||
| return backfillStatus; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.