-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add new endpoint for backfill (#2601)
* - Modal added for backfilling and disconnecting subscription * - Fixing eslint in a test case * - WIP - backfill modal * - WIP - backfill modal * - WIP - backfill modal * chore: add new endpoint for backfill * chore: add new endpoint for backfill * chore: add new endpoint for backfill * chore: add test cases * chore: add test cases * chore: PR comments * chore: PR comments * chore: PR comments * chore: PR comments * chore: PR comments * chore: PR comments * - Using the corrected datepicker * chore: PR comment * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes * chore: revoke unnecessary changes --------- Co-authored-by: Kayub Maharjan <kmaharjan4@atlassian.com>
- Loading branch information
1 parent
6b2bb0f
commit 913b45d
Showing
8 changed files
with
294 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,10 @@ | ||
import { axiosRest } from "../axiosInstance"; | ||
import { RestSyncReqBody } from "~/src/rest-interfaces"; | ||
|
||
export default { | ||
getSubscriptions: () => axiosRest.get("/rest/subscriptions"), | ||
deleteSubscription: (subscriptionId: number) => | ||
axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`) | ||
axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`), | ||
syncSubscriptions: (subscriptionId: number, reqBody: RestSyncReqBody) => | ||
axiosRest.post(`/rest/app/cloud/subscriptions/${subscriptionId}/sync`, reqBody), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import { getFrontendApp } from "~/src/app"; | ||
import { Installation } from "models/installation"; | ||
import { Subscription } from "models/subscription"; | ||
import express, { Express } from "express"; | ||
import { RootRouter } from "routes/router"; | ||
import supertest from "supertest"; | ||
import { encodeSymmetric } from "atlassian-jwt"; | ||
import { GitHubServerApp } from "models/github-server-app"; | ||
import { v4 as newUUID } from "uuid"; | ||
import { sqsQueues } from "~/src/sqs/queues"; | ||
import { DatabaseStateCreator } from "~/test/utils/database-state-creator"; | ||
|
||
jest.mock("~/src/sqs/queues"); | ||
jest.mock("config/feature-flags"); | ||
|
||
describe("Checking the deferred request parsing route", () => { | ||
let app: Express; | ||
let installation: Installation; | ||
const installationIdForCloud = 1; | ||
const installationIdForServer = 2; | ||
const gitHubInstallationId = 15; | ||
let subscription; | ||
let gitHubServerApp: GitHubServerApp; | ||
// let jwt: string; | ||
const testSharedSecret = "test-secret"; | ||
const clientKey = "jira-client-key"; | ||
const getToken = ({ | ||
secret = testSharedSecret, | ||
iss = clientKey, | ||
exp = Date.now() / 1000 + 10000, | ||
qsh = "context-qsh", | ||
sub = "myAccount" } = {}): string => { | ||
return encodeSymmetric({ | ||
qsh, | ||
iss, | ||
exp, | ||
sub | ||
}, secret); | ||
}; | ||
beforeEach(async () => { | ||
app = getFrontendApp(); | ||
installation = await Installation.install({ | ||
host: jiraHost, | ||
sharedSecret: testSharedSecret, | ||
clientKey: clientKey | ||
}); | ||
await Subscription.install({ | ||
installationId: installationIdForCloud, | ||
host: jiraHost, | ||
hashedClientKey: installation.clientKey, | ||
gitHubAppId: undefined | ||
}); | ||
gitHubServerApp = await GitHubServerApp.install({ | ||
uuid: newUUID(), | ||
appId: 123, | ||
gitHubAppName: "My GitHub Server App", | ||
gitHubBaseUrl: gheUrl, | ||
gitHubClientId: "lvl.1234", | ||
gitHubClientSecret: "myghsecret", | ||
webhookSecret: "mywebhooksecret", | ||
privateKey: "myprivatekey", | ||
installationId: installation.id | ||
}, jiraHost); | ||
await Subscription.install({ | ||
installationId: installationIdForServer, | ||
host: jiraHost, | ||
hashedClientKey: installation.clientKey, | ||
gitHubAppId: gitHubServerApp.id | ||
}); | ||
app = express(); | ||
app.use(RootRouter); | ||
subscription = await Subscription.create({ | ||
gitHubInstallationId, | ||
jiraHost | ||
}); | ||
}); | ||
|
||
describe("cloud", () => { | ||
it("should throw 401 error when no github token is passed", async () => { | ||
const resp = await supertest(app) | ||
.get(`/rest/app/cloud/subscriptions/${subscription.id}/sync`); | ||
|
||
expect(resp.status).toEqual(401); | ||
}); | ||
|
||
it("should return 403 on correct sub id with different jiraHost", async () => { | ||
const commitsFromDate = new Date(new Date().getTime() - 2000); | ||
const result = await new DatabaseStateCreator() | ||
.forJiraHost("https://another-one.atlassian.net") | ||
.create(); | ||
return supertest(app) | ||
.post(`/rest/app/cloud/subscriptions/${result.subscription.id}/sync`) | ||
.set("authorization", `${getToken()}`) | ||
.send({ | ||
jiraHost, | ||
syncType: "full", | ||
commitsFromDate | ||
}) | ||
.expect(403); | ||
}); | ||
|
||
it("should return 400 on incorrect commitsFromDate", async () => { | ||
const commitsFromDate = new Date(new Date().getTime() - 2000); | ||
return supertest(app) | ||
.post(`/rest/app/cloud/subscriptions/${undefined}/sync`) | ||
.set("authorization", `${getToken()}`) | ||
.send({ | ||
jiraHost, | ||
syncType: "full", | ||
commitsFromDate | ||
}) | ||
.expect(400); | ||
}); | ||
|
||
it("should return 400 on incorrect installationIdForCloud", async () => { | ||
const commitsFromDate = new Date(new Date().getTime() + 2000); | ||
return supertest(app) | ||
.post(`/rest/app/cloud/subscriptions/${subscription.id}/sync`) | ||
.set("authorization", `${getToken()}`) | ||
.send({ | ||
jiraHost, | ||
syncType: "full", | ||
commitsFromDate | ||
}) | ||
.expect(400); | ||
}); | ||
|
||
it("should return 202 on correct post for /rest/app/cloud/sync one for Cloud app", async () => { | ||
const commitsFromDate = new Date(new Date().getTime() - 2000); | ||
return supertest(app) | ||
.post(`/rest/app/cloud/subscriptions/${subscription.id}/sync`) | ||
.set("authorization", `${getToken()}`) | ||
.send({ | ||
jiraHost, | ||
syncType: "full", | ||
commitsFromDate | ||
}) | ||
.expect(202) | ||
.then(() => { | ||
expect(sqsQueues.backfill.sendMessage).toBeCalledWith(expect.objectContaining({ | ||
jiraHost, | ||
startTime: expect.anything(), | ||
gitHubAppConfig: expect.objectContaining({ gitHubAppId: undefined, uuid: undefined }) | ||
}), expect.anything(), expect.anything()); | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { Request, Response } from "express"; | ||
import { ParamsDictionary } from "express-serve-static-core"; | ||
import { errorWrapper } from "../../helper"; | ||
import { Subscription } from "models/subscription"; | ||
import { findOrStartSync } from "~/src/sync/sync-utils"; | ||
import { determineSyncTypeAndTargetTasks } from "~/src/util/github-sync-helper"; | ||
import { BaseLocals } from ".."; | ||
import { InsufficientPermissionError, RestApiError } from "~/src/config/errors"; | ||
import { RestSyncReqBody } from "~/src/rest-interfaces"; | ||
// import { GitHubServerApp } from "~/src/models/github-server-app"; | ||
|
||
const restSyncPost = async ( | ||
req: Request<ParamsDictionary, unknown, RestSyncReqBody>, | ||
res: Response<string, BaseLocals> | ||
) => { | ||
const { | ||
syncType: syncTypeFromReq, | ||
source, | ||
commitsFromDate: commitsFrmDate | ||
} = req.body; | ||
|
||
// A date to start fetching commit history(main and branch) from. | ||
const commitsFromDate = commitsFrmDate ? new Date(commitsFrmDate) : undefined; | ||
if (commitsFromDate && commitsFromDate.valueOf() > Date.now()) { | ||
throw new RestApiError( | ||
400, | ||
"INVALID_OR_MISSING_ARG", | ||
"Invalid date value, cannot select a future date" | ||
); | ||
} | ||
|
||
const subscriptionId: number = Number(req.params.subscriptionId); | ||
if (!subscriptionId) { | ||
req.log.info( | ||
{ | ||
jiraHost: res.locals.installation.jiraHost, | ||
subscriptionId | ||
}, | ||
"Subscription ID not found when retrying sync." | ||
); | ||
throw new RestApiError( | ||
400, | ||
"INVALID_OR_MISSING_ARG", | ||
"Subscription ID not found when retrying sync." | ||
); | ||
} | ||
|
||
//TODO: We are yet to handle enterprise backfill | ||
// const gitHubAppId: number | undefined = undefined; | ||
// const cloudOrUUID = req.params.cloudOrUUID; | ||
// const gheUUID = cloudOrUUID === "cloud" ? undefined : req.params.cloudOrUUID; | ||
// if (gheUUID) { | ||
// const ghEnterpriseServers: GitHubServerApp[] = await GitHubServerApp.findForInstallationId(gitHubInstallationId) || []; | ||
// gitHubAppId = ghEnterpriseServers[0]?.appId; | ||
// } | ||
|
||
const subscription = await Subscription.findByPk(subscriptionId); | ||
|
||
if (!subscription) { | ||
req.log.info( | ||
{ | ||
jiraHost: res.locals.installation.jiraHost, | ||
subscriptionId | ||
}, | ||
"Subscription not found when retrying sync." | ||
); | ||
throw new RestApiError( | ||
400, | ||
"INVALID_OR_MISSING_ARG", | ||
"Subscription not found, cannot resync." | ||
); | ||
} | ||
|
||
const localJiraHost = res.locals.installation.jiraHost; | ||
|
||
if (subscription.jiraHost !== localJiraHost) { | ||
throw new InsufficientPermissionError("Forbidden - mismatched Jira Host"); | ||
} | ||
|
||
|
||
const { syncType, targetTasks } = determineSyncTypeAndTargetTasks( | ||
syncTypeFromReq, | ||
subscription | ||
); | ||
await findOrStartSync( | ||
subscription, | ||
req.log, | ||
syncType, | ||
commitsFromDate || subscription.backfillSince, | ||
targetTasks, | ||
{ source } | ||
); | ||
res.sendStatus(202); | ||
}; | ||
|
||
export const SyncRouterHandler = errorWrapper( | ||
"AnalyticsProxyHandler", | ||
restSyncPost | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Subscription, SyncStatus } from "models/subscription"; | ||
import { TaskType, SyncType } from "~/src/sync/sync.types"; | ||
|
||
|
||
const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000; | ||
export const getStartTimeInDaysAgo = (commitsFromDate: Date | undefined) => { | ||
if (commitsFromDate === undefined) return undefined; | ||
return Math.floor((Date.now() - commitsFromDate.getTime()) / MILLISECONDS_IN_ONE_DAY); | ||
}; | ||
|
||
type SyncTypeAndTargetTasks = { | ||
syncType: SyncType, | ||
targetTasks: TaskType[] | undefined, | ||
}; | ||
|
||
export const determineSyncTypeAndTargetTasks = (syncTypeFromReq: string, subscription: Subscription): SyncTypeAndTargetTasks => { | ||
if (syncTypeFromReq === "full") { | ||
return { syncType: "full", targetTasks: undefined }; | ||
} | ||
|
||
if (subscription.syncStatus === SyncStatus.FAILED) { | ||
return { syncType: "full", targetTasks: undefined }; | ||
} | ||
|
||
return { syncType: "partial", targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert", "codeScanningAlert"] }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters