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

Arc 968 view repos addons #2357

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2a1a19f
display list of repos states
jkay-atlas Mar 1, 2023
76f86e2
added pagination for repo list
jkay-atlas Mar 2, 2023
8d29cfc
Merge branch 'main' into ARC-968-view-repos
joshkay10 Mar 2, 2023
80880df
removed unusd handlebar helper
jkay-atlas Mar 2, 2023
2bb2a60
Merge branch 'main' into ARC-968-view-repos
joshkay10 Mar 5, 2023
0855423
Merge branch 'main' into ARC-968-view-repos
joshkay10 Mar 7, 2023
2651b6c
Merge branch 'main' into ARC-968-view-repos
joshkay10 Mar 12, 2023
ab71260
Merge branch 'main' into ARC-968-view-repos
rachellerathbone Mar 13, 2023
c090d65
Merge branch 'main' into ARC-968-view-repos
joshkay10 Mar 14, 2023
b2d5466
minor tidies on order and view hbs
jkay-atlas Mar 14, 2023
ad10523
Merge branch 'main' into ARC-968-view-repos
Harminder84 Apr 3, 2023
c13f6c9
Updated pagination, adding filtering and searching
Harminder84 Apr 6, 2023
1988d09
Merge branch 'main' into ARC-968-view-repos
Harminder84 Apr 6, 2023
4bbec83
update message
Harminder84 Apr 6, 2023
00ad2f0
Merge branch main into ARC-968-view-repos
Harminder84 Jun 25, 2023
49b8ed7
updated UI as per specs
Harminder84 Jun 29, 2023
c985e19
Merge branch main into ARC-968-view-repos
Harminder84 Jun 29, 2023
db71b9e
Merge branch 'main' into ARC-968-view-repos
Harminder84 Jun 29, 2023
c5d4ec4
Merge branch 'main' into ARC-968-view-repos
Harminder84 Jun 29, 2023
e5001b3
Merge branch 'main' into ARC-968-view-repos
Harminder84 Jun 30, 2023
05c5d39
fix unit testing
Harminder84 Jun 30, 2023
9b0b23e
Fixed unit testing
Harminder84 Jun 30, 2023
0eae8ea
Merge branch 'main' into ARC-968-view-repos
gxueatlassian Jul 19, 2023
db69d40
resolve import conlfict
jkay-atlas Aug 17, 2023
8d09645
Merge branch 'main' into ARC-968-view-repos
joshkay10 Aug 18, 2023
519ce2c
Merge branch 'main' into ARC-968-view-repos
joshkay10 Aug 18, 2023
2d63fad
chore: addons and fixes
kamaksheeAtl Aug 20, 2023
7f23be3
chore: merge parent branch
kamaksheeAtl Aug 21, 2023
896da53
chore: add test cases for repo view
kamaksheeAtl Aug 21, 2023
659c9df
chore: add test cases for repo view
kamaksheeAtl Aug 21, 2023
fc21edf
chore: incorporate PR reviews
kamaksheeAtl Aug 21, 2023
dda0648
Merge branch 'main' into ARC-968-view-repos
bgvozdev Aug 25, 2023
87b5cce
NONE: outlined TODOs and fixed some dodgy scenarios
bgvozdev Aug 25, 2023
63854e8
NONE: fix in_progress logic
bgvozdev Aug 28, 2023
17e8ec2
ARC-968: fix navigation
bgvozdev Aug 28, 2023
a6c1ad1
ARC-968: fix layout
bgvozdev Aug 29, 2023
1e9fa08
Merge branch 'main' into ARC-968-view-repos
bgvozdev Aug 29, 2023
caedc1d
ARC-968: filter by fullName
bgvozdev Aug 29, 2023
6cc708f
ARC-968: fix tests, add placeholders for the new tests
bgvozdev Aug 29, 2023
0238634
ARC-968: add some tests
bgvozdev Aug 30, 2023
0117676
ARC-968: fix test
bgvozdev Aug 30, 2023
192e2d5
ARC-968: tests
bgvozdev Aug 30, 2023
9a70c60
Merge branch 'main' into ARC-968-view-repos
bgvozdev Aug 31, 2023
a2a08bf
ARC-968: uncomment the FF
bgvozdev Aug 31, 2023
1028517
chore: merge parent branhc
kamaksheeAtl Aug 31, 2023
1ac8da7
chore: merge parent branhc
kamaksheeAtl Aug 31, 2023
763cc1f
chore: remove finished filter item
kamaksheeAtl Aug 31, 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 @@ -24,6 +24,7 @@ export enum BooleanFlags {
SKIP_REQUESTED_REVIEWERS = "skip-requested-reviewers",
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",
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
1 change: 1 addition & 0 deletions src/config/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface AppInstallation extends Octokit.AppsGetInstallationResponse {
syncWarning?: string;
totalNumberOfRepos?: number;
numberOfSyncedRepos?: number;
subscriptionId?: number;
backfillSince?: Date;
failedSyncErrors?: Record<string, number>
jiraHost: string;
Expand Down
2 changes: 1 addition & 1 deletion src/models/reposyncstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@
return RepoSyncState.create(merge(values, { subscriptionId: subscription.id }), options);
}

private static async countSubscriptionRepos(subscription: Subscription, options: CountOptions = {}): Promise<number> {
static async countSubscriptionRepos(subscription: Subscription, options: CountOptions = {}): Promise<number> {
return RepoSyncState.count(merge(options, {
where: {
subscriptionId: subscription.id
Expand All @@ -196,7 +196,7 @@
}

static async findRepoByRepoIdAndJiraHost(repoId: number, jiraHost: string): Promise<RepoSyncState & Subscription | null> {
const results = await this.sequelize!.query(

Check warning on line 199 in src/models/reposyncstate.ts

View workflow job for this annotation

GitHub Actions / build

Forbidden non-null assertion
"SELECT * " +
"FROM \"Subscriptions\" s " +
"LEFT JOIN \"RepoSyncStates\" rss on s.\"id\" = rss.\"subscriptionId\" " +
Expand Down Expand Up @@ -320,7 +320,7 @@
LIMIT :limit
`;

const repositories = await this.sequelize!.query(query, {

Check warning on line 323 in src/models/reposyncstate.ts

View workflow job for this annotation

GitHub Actions / build

Forbidden non-null assertion
replacements: {
jiraHost,
subscriptionIds: subscriptionIdsArray,
Expand All @@ -335,7 +335,7 @@
}

static async findOneForRepoUrlAndRepoIdAndJiraHost(repoUrl: string, repoId: number, jiraHost: string):Promise<RepoSyncState | null> {
const results = await this.sequelize!.query(

Check warning on line 338 in src/models/reposyncstate.ts

View workflow job for this annotation

GitHub Actions / build

Forbidden non-null assertion
`SELECT rss.*
FROM "RepoSyncStates" rss
JOIN "Subscriptions" s ON rss."subscriptionId" = s."id"
Expand Down
2 changes: 1 addition & 1 deletion src/models/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class Subscription extends Model {
numberOfSyncedRepos?: number;
repositoryCursor?: string;
repositoryStatus?: TaskStatus;
gitHubAppId: number | undefined;
gitHubAppId: number | undefined; // the primary key (id) of GitHubServerApp
avatarUrl: string | undefined;
isSecurityPermissionsAccepted: boolean;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ const modules = {
url: "/spa",
location: "none",
conditions: adminCondition
}, {
url: "/jira/subscription/{ac.subscriptionId}/repos?pageNumber={ac.pageNumber}&repoName={ac.repoName}&syncStatus={ac.syncStatus}",
name: {
value: "Sync status"
},
conditions: adminCondition,
key: "gh-addon-subscription-repos",
location: "none"
}
],
webSections: [
Expand Down
191 changes: 191 additions & 0 deletions src/routes/jira/jira-connected-repos-get.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 { createQueryStringHash, 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-connected-repos-get", () => {

let app;
let installation: Installation;
let subscription: Subscription;
let repoSyncState: RepoSyncState;
const generateJwt = async (subscriptionId: number, query: any = {}) => {
return encodeSymmetric({
qsh: createQueryStringHash({
method: "GET",
pathname: `/jira/subscription/${subscriptionId}/repos`,
query
}, false),
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 403 when not an admin", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id + 1}/repos`)
.set("authorization", `JWT ${await generateJwt(subscription.id + 1)}`);
expect(resp.status).toStrictEqual(403);
});

it("should return 401 when no JWT was provided", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id + 1}/repos`);
expect(resp.status).toStrictEqual(401);
});

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 subscription was found", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id + 1}/repos`)
.set("authorization", `JWT ${await generateJwt(subscription.id + 1)}`);
expect(resp.status).toStrictEqual(400);
});

it("should return 400 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/subscription/${result.subscription.id}/repos`)
.set("authorization", `JWT ${await generateJwt(result.subscription.id)}`);
expect(resp.status).toStrictEqual(400);
});

it("should return 400 when unknown filtered status was provided", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?syncStatus=blah`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { syncStatus: "blah" })}`);
expect(resp.status).toStrictEqual(400);
});

it("should return 400 when a page size is too great", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?pageSize=50000`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { pageSize: "50000" })}`);
expect(resp.status).toStrictEqual(400);
});

describe("happy paths", () => {
beforeEach(async () => {
const newRepoSyncStatesData: any[] = [];
for (let newRepoStateNo = 1; newRepoStateNo < 50; newRepoStateNo++) {
const newRepoSyncState = { ...repoSyncState.dataValues };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete newRepoSyncState["id"];
delete newRepoSyncState["commitStatus"];
delete newRepoSyncState["branchStatus"];
newRepoSyncState["repoId"] = repoSyncState.repoId + newRepoStateNo;
newRepoSyncState["repoName"] = repoSyncState.repoName + newRepoStateNo;
newRepoSyncState["repoFullName"] = repoSyncState.repoFullName + String(newRepoStateNo).padStart(3, "0");
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 the first page of repos by default without any filters", async ()=> {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos`)
.set("authorization", `JWT ${await generateJwt(subscription.id)}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).toContain("<div class=\"page-num-link page-selector\" data-page-num=\"5\">5</div>");
expect(resp.text).not.toContain("<div class=\"page-num-link page-selector\" data-page-num=\"6\">6</div>");
expect(resp.text).toContain("test-repo-name006");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__in-progress");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__failed");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__finished");
});

it("should correctly apply repoSearch filter", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?repoName=est-repo-name048`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { repoName: "est-repo-name048" })}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).toContain("test-repo-name048");
expect(resp.text).not.toContain("<div class=\"page-num-link page-selector\" data-page-num=\"2\">2</div>");
});

it("should correctly apply all status filter", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?syncStatus=all`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { syncStatus: "all" })}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__in-progress");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__failed");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__finished");
});

it("should correctly apply pending status filter", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?syncStatus=pending`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { syncStatus: "pending" })}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__in-progress");
expect(resp.text).not.toContain("<span class=\"jiraConnectedRepos__table__failed");
expect(resp.text).not.toContain("<span class=\"jiraConnectedRepos__table__finished");
});

it("should correctly apply failed status filter", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?syncStatus=failed`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { syncStatus: "failed" })}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).not.toContain("<span class=\"jiraConnectedRepos__table__in-progress");
expect(resp.text).toContain("<span class=\"jiraConnectedRepos__table__failed");
expect(resp.text).not.toContain("<span class=\"jiraConnectedRepos__table__finished");
});

it("should correctly apply pagination", async () => {
const resp = await supertest(app)
.get(`/jira/subscription/${subscription.id}/repos?pageNumber=2`)
.set("authorization", `JWT ${await generateJwt(subscription.id, { pageNumber: "2" })}`);
expect(resp.status).toStrictEqual(200);
expect(resp.text).not.toContain("test-repo-name006");
expect(resp.text).toContain("test-repo-name016");
});
});
});

});
Loading
Loading