Skip to content
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
28ee606
chore: spike for backfill status fetch
kamaksheeAtl Aug 23, 2023
bb37377
chore: spike for backfill status fetch
kamaksheeAtl Aug 28, 2023
44a779b
chore: spike for backfill status fetch
kamaksheeAtl Aug 23, 2023
c8b0cf1
chore: add progress fetch fr backfill pg
kamaksheeAtl Aug 28, 2023
93cb05e
chore: update test snapshots
kamaksheeAtl Aug 28, 2023
e7cb1c7
chore: remove get-connected-repo changes
kamaksheeAtl Aug 28, 2023
291ac6d
chore: remove get-connected-repo changes
kamaksheeAtl Aug 28, 2023
973ee29
chore: update test snapshots
kamaksheeAtl Aug 28, 2023
99aec7a
fix: missing subscription id in data attri
kamaksheeAtl Aug 28, 2023
603dc2a
chore: update failing test cases
kamaksheeAtl Aug 28, 2023
3d7ddb7
chore: incorporate PR review comments
kamaksheeAtl Aug 29, 2023
e54d420
chore: incorporate PR review comments
kamaksheeAtl Aug 29, 2023
5094704
chore: incorporate pr review comments
kamaksheeAtl Aug 29, 2023
463fb6e
chore: fix test cases and add security middleware to new api route
kamaksheeAtl Aug 29, 2023
b9bcf6b
chore: update test cases to fix failing test
kamaksheeAtl Sep 4, 2023
f123a7f
chore: update api for 403 incase of missmached jirahost
kamaksheeAtl Sep 5, 2023
a5404df
fix: failing test cases
kamaksheeAtl Sep 5, 2023
07edd59
chore: add the backfill since status
kamaksheeAtl Sep 5, 2023
f3701e9
chore: update code as per PR review comment
kamaksheeAtl Sep 6, 2023
dd6c60a
chore: update the jwt in backfill call
kamaksheeAtl Sep 6, 2023
1d61632
chore: update the jwt in backfill call
kamaksheeAtl Sep 6, 2023
bc4286b
chore: merge main
kamaksheeAtl Sep 6, 2023
b267700
chore: add test cases
kamaksheeAtl Sep 6, 2023
10aae00
chore: add test cases
kamaksheeAtl Sep 7, 2023
195d2b6
chore: add test cases
kamaksheeAtl Sep 7, 2023
c7312a4
chore: update test cases to increase the coverage
kamaksheeAtl Sep 7, 2023
2c4a3b1
chore: remove explicit jwt pass
kamaksheeAtl Sep 7, 2023
7887a8f
chore: PR review comments
kamaksheeAtl Sep 7, 2023
637389d
chore: PR review comments
kamaksheeAtl Sep 7, 2023
63c421e
chore: PR review comments
kamaksheeAtl Sep 7, 2023
50c78f2
chore: update testcases
kamaksheeAtl Sep 7, 2023
a023315
chore: update snaps
kamaksheeAtl Sep 8, 2023
4df488e
chore: add feature flag
kamaksheeAtl Sep 8, 2023
09b5c30
chore: update space changes
kamaksheeAtl Sep 11, 2023
9a5ed36
chore: update space changes
kamaksheeAtl Sep 11, 2023
bec3e9c
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Sep 22, 2023
d95c309
fix: zero repo count scenario
kamaksheeAtl Sep 25, 2023
87dac3c
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Sep 25, 2023
d98c7d5
fix: give results of subs with totalrepos more than zero
kamaksheeAtl Sep 25, 2023
a47cff2
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Sep 27, 2023
8545cab
chore: simplify the logic
kamaksheeAtl Sep 29, 2023
40fc18c
chore: chng logic of detected backfill complete
kamaksheeAtl Oct 3, 2023
61e6241
fix: org with zero repo count display
kamaksheeAtl Oct 4, 2023
2d874a8
Merge branch 'main' into ARC-316-fix-zerorepo-org
kamaksheeAtl Oct 4, 2023
7138c4b
fix: org with zero repo count display
kamaksheeAtl Oct 4, 2023
dca1b90
fix: org with zero repo count display
kamaksheeAtl Oct 4, 2023
d332cec
chore: merge main brnch
kamaksheeAtl Oct 4, 2023
20ea125
chore: merge main branch
kamaksheeAtl Oct 4, 2023
f8615bb
chore: add sync err handling
kamaksheeAtl Oct 4, 2023
a99a7ff
chore: modify test cases
kamaksheeAtl Oct 4, 2023
208e8f9
chore: modify test cases
kamaksheeAtl Oct 4, 2023
279e950
chore: merge main
kamaksheeAtl Oct 4, 2023
737f208
chore: reformate code
kamaksheeAtl Oct 5, 2023
cd2ba30
chore: reformate code
kamaksheeAtl Oct 5, 2023
6d04509
chore: reformate code
kamaksheeAtl Oct 5, 2023
d56a78e
chore: reformate code
kamaksheeAtl Oct 5, 2023
6b308f6
chore: PR comment refactor
kamaksheeAtl Oct 5, 2023
7929f06
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Oct 5, 2023
9cfa423
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Oct 6, 2023
7446db2
chore: PR comments
kamaksheeAtl Oct 6, 2023
b1d3aa3
chore: PR comments
kamaksheeAtl Oct 6, 2023
13677c9
chore: PR comments
kamaksheeAtl Oct 6, 2023
5e1d563
Merge branch 'main' into ARC-1192-update-backfilling-status
kamaksheeAtl Oct 9, 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
1 change: 1 addition & 0 deletions src/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum BooleanFlags {
ENABLE_SUBSCRIPTION_DEFERRED_INSTALL = "enable-subscription-deferred-install",
EARLY_EXIT_ON_VALIDATION_FAILED = "early-exit-on-validation-failed",
ENABLE_CONNECTED_REPOS_VIEW="enable-connected-repos-view",
ENABLE_BACKFILLING_STATUS_POLLING="enable-backfilling-status-polling",
USE_REST_API_FOR_DISCOVERY = "use-rest-api-for-discovery-again",
ENABLE_GENERIC_CONTAINERS = "enable-generic-containers",
ENABLE_GITHUB_SECURITY_IN_JIRA = "enable-github-security-in-jira",
Expand Down
191 changes: 191 additions & 0 deletions src/routes/jira/jira-get-connections-backfill-status.test.ts
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]
}
});
});
});
});
});
110 changes: 110 additions & 0 deletions src/routes/jira/jira-get-connections-backfill-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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.findAllForSubscriptionIds(subscriptionIds);

const resultSubscriptionIds = subscriptions.map(
(subscription) => subscription.id
);

if (!subscriptions || subscriptions.length === 0) {
req.log.error("Missing Subscription");
Copy link
Contributor

Choose a reason for hiding this comment

The 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 MISSING_SUBSCRIPTION

res.status(400).send("Missing Subscription");
return;
}

const jiraHosts = subscriptions.map(
(subscription) => subscription?.jiraHost
);

const jiraHostsMatched = jiraHosts.every(
(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`));
}
};

const getBackfillCompletionStatus = (backfillStatus: BackFillType): boolean =>
Object.values(backfillStatus).every(
(backFill: SubscriptionBackfillState): boolean => backFill?.isSyncComplete
);

const getBackfillStatus = async (subscriptionsById): Promise<BackFillType> => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created a new PR as i made loads of changes in this file to keep this backfill status polling insync with the way we are providing data to jira-configuration-table.hbs via getInstallation

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(
subscription
),
failedSyncErrors,
backfillSince: subscription?.backfillSince || null,
syncWarning: subscription.syncWarning
};
}
return backfillStatus;
};
1 change: 1 addition & 0 deletions src/routes/jira/jira-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const renderJiraCloudAndEnterpriseServer = async (res: Response, req: Request):
useNewSPAExperience,
APP_URL: process.env.APP_URL,
enableRepoConnectedPage: await booleanFlag(BooleanFlags.ENABLE_CONNECTED_REPOS_VIEW, jiraHost),
enableBackfillingStatusPolling: await booleanFlag(BooleanFlags.ENABLE_BACKFILLING_STATUS_POLLING, jiraHost),
csrfToken: req.csrfToken(),
nonce
});
Expand Down
2 changes: 2 additions & 0 deletions src/routes/jira/jira-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { JiraConnectRouter } from "routes/jira/connect/jira-connect-router";
import { body } from "express-validator";
import { returnOnValidationError } from "routes/api/api-utils";
import { jiraSymmetricJwtMiddleware } from "~/src/middleware/jira-symmetric-jwt-middleware";
import { JiraGetConnectionsBackfillStatus } from "~/src/routes/jira/jira-get-connections-backfill-status";
import { JiraConnectedReposGet } from "routes/jira/jira-connected-repos-get";
import { jiraAdminPermissionsMiddleware } from "middleware/jira-admin-permission-middleware";
import { JiraWorkspacesRouter } from "routes/jira/workspaces/jira-workspaces-router";
Expand Down Expand Up @@ -38,6 +39,7 @@ JiraRouter.use("/security", jiraSymmetricJwtMiddleware, JiraSecurityWorkspacesRo

JiraRouter.get("/", csrfMiddleware, jiraSymmetricJwtMiddleware, jiraAdminPermissionsMiddleware, JiraGet);

JiraRouter.get("/subscriptions/backfill-status", jiraSymmetricJwtMiddleware, jiraAdminPermissionsMiddleware, JiraGetConnectionsBackfillStatus);
JiraRouter.get("/subscription/:subscriptionId/repos", csrfMiddleware, jiraSymmetricJwtMiddleware, jiraAdminPermissionsMiddleware, JiraConnectedReposGet);

/********************************************************************************************************************
Expand Down
2 changes: 1 addition & 1 deletion src/util/github-installations-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface GitHubCloudObj {
}

export type ConnectionSyncStatus = "IN PROGRESS" | "FINISHED" | "PENDING" | "FAILED" | undefined;
const mapSyncStatus = (syncStatus: SyncStatus = SyncStatus.PENDING): ConnectionSyncStatus => {
export const mapSyncStatus = (syncStatus: SyncStatus = SyncStatus.PENDING): ConnectionSyncStatus => {
switch (syncStatus) {
case "ACTIVE":
return "IN PROGRESS";
Expand Down
Loading