Skip to content
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

chore: add new endpoint for backfill #2601

Merged
merged 34 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
be99c17
- Modal added for backfilling and disconnecting subscription
krazziekay Dec 5, 2023
150023e
- Fixing eslint in a test case
krazziekay Dec 5, 2023
4dd1eed
- WIP - backfill modal
krazziekay Dec 5, 2023
7377807
- WIP - backfill modal
krazziekay Dec 5, 2023
e5d872b
Merge branch 'main' of github.com:atlassian/github-for-jira into ARC-…
krazziekay Dec 5, 2023
9379076
Merge branch 'main' into ARC-adding-modals-for-backfill-page
krazziekay Dec 5, 2023
e940786
Merge branch 'main' of github.com:atlassian/github-for-jira into ARC-…
krazziekay Dec 5, 2023
54747ff
- WIP - backfill modal
krazziekay Dec 5, 2023
ef67e5a
Merge branch 'main' into ARC-adding-modals-for-backfill-page
krazziekay Dec 6, 2023
85c0ec9
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
75bf2cd
Merge branch 'main' into ARC-2714-Kamakshee
kamaksheeAtl Dec 6, 2023
b8a0402
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
d2b2ce1
chore: add new endpoint for backfill
kamaksheeAtl Dec 6, 2023
2a2fcac
chore: add test cases
kamaksheeAtl Dec 6, 2023
8675da7
chore: add test cases
kamaksheeAtl Dec 6, 2023
1ed4f9b
chore: PR comments
kamaksheeAtl Dec 6, 2023
d077876
chore: PR comments
kamaksheeAtl Dec 6, 2023
4da351c
chore: PR comments
kamaksheeAtl Dec 6, 2023
b12fe93
chore: PR comments
kamaksheeAtl Dec 6, 2023
2ea4d5d
chore: PR comments
kamaksheeAtl Dec 6, 2023
ae84f00
chore: PR comments
kamaksheeAtl Dec 6, 2023
0b4d49f
- Using the corrected datepicker
krazziekay Dec 7, 2023
e2bf47a
Merge branch 'ARC-adding-modals-for-backfill-page' into ARC-2714-Kama…
kamaksheeAtl Dec 7, 2023
3726cea
chore: merge main
kamaksheeAtl Dec 7, 2023
a4ea01a
chore: PR comment
kamaksheeAtl Dec 7, 2023
263b5dc
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
1f6533f
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
b772dad
Merge branch 'main' into ARC-2714-Kamakshee
kamaksheeAtl Dec 7, 2023
7888cd0
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
f1a77b2
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
f478afc
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
c40b726
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
93183ab
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
2519132
chore: revoke unnecessary changes
kamaksheeAtl Dec 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion spa/src/api/subscriptions/index.ts
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),
};
5 changes: 5 additions & 0 deletions src/rest-interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export type RestSyncReqBody = {
syncType: string;
source: string;
commitsFromDate: string;
}

export type GetRedirectUrlResponse = {
redirectUrl: string;
Expand Down
3 changes: 3 additions & 0 deletions src/rest/routes/subscriptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Installation } from "models/installation";
import { removeSubscription } from "utils/jira-utils";
import { GitHubServerApp } from "models/github-server-app";
import { InvalidArgumentError } from "config/errors";
import { SyncRouterHandler } from "./sync";

export const SubscriptionsRouter = Router({ mergeParams: true });

Expand Down Expand Up @@ -38,3 +39,5 @@ SubscriptionsRouter.delete("/", errorWrapper("SubscriptionDelete", async (req: R

res.sendStatus(204);
}));

SubscriptionsRouter.post("/sync", SyncRouterHandler);
150 changes: 150 additions & 0 deletions src/rest/routes/subscriptions/sync.test.ts
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());
});
});

});

});
99 changes: 99 additions & 0 deletions src/rest/routes/subscriptions/sync.ts
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."
);
}
kamaksheeAtl marked this conversation as resolved.
Show resolved Hide resolved

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
);
29 changes: 3 additions & 26 deletions src/routes/jira/sync/jira-sync-post.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Subscription, SyncStatus } from "models/subscription";
import { Subscription } from "models/subscription";
import * as Sentry from "@sentry/node";
import { NextFunction, Request, Response } from "express";
import { findOrStartSync } from "~/src/sync/sync-utils";
import { sendAnalytics } from "utils/analytics-client";
import { AnalyticsEventTypes, AnalyticsTrackEventsEnum, AnalyticsTrackSource } from "interfaces/common";
import { TaskType, SyncType } from "~/src/sync/sync.types";
import { booleanFlag, BooleanFlags } from "config/feature-flags";
import { determineSyncTypeAndTargetTasks, getStartTimeInDaysAgo } from "../../../util/github-sync-helper";

export const JiraSyncPost = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const { installationId: gitHubInstallationId, appId: gitHubAppId, syncType: syncTypeFromReq, source } = req.body;
Expand Down Expand Up @@ -35,7 +35,7 @@ export const JiraSyncPost = async (req: Request, res: Response, next: NextFuncti
return;
}

const { syncType, targetTasks } = await determineSyncTypeAndTargetTasks(syncTypeFromReq, subscription);
const { syncType, targetTasks } = determineSyncTypeAndTargetTasks(syncTypeFromReq, subscription);
await findOrStartSync(subscription, req.log, syncType, commitsFromDate || subscription.backfillSince, targetTasks, { source });

await sendAnalytics(res.locals.jiraHost, AnalyticsEventTypes.TrackEvent, {
Expand Down Expand Up @@ -64,26 +64,3 @@ export const JiraSyncPost = async (req: Request, res: Response, next: NextFuncti
next(new Error("Unauthorized"));
}
};

const MILLISECONDS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
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,
};

const determineSyncTypeAndTargetTasks = async (syncTypeFromReq: string, subscription: Subscription): Promise<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"] };
};
26 changes: 26 additions & 0 deletions src/util/github-sync-helper.ts
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"] };
};
4 changes: 4 additions & 0 deletions test/snapshots/app.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionsGet
:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionDelete
:POST ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/sync/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-callback/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,OAuthCallbackHandler
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/github-installed/?$
Expand Down Expand Up @@ -45,6 +47,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionsGet
:DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionDelete
:POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/sync/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,AnalyticsProxyHandler
:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/org/?(?=/|$)^/?$
query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,GitHubTokenHandler,GitHubOrgsFetchOrgs
:POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/org/?(?=/|$)^/?$
Expand Down
Loading