-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add new admin endpoint to reset repository (#950)
* feat: introduce new reset repo admin endpoint * feat: add unit tests for resetRepo functionality * fix: add e2e-notggs-test-repo as allowed e2e repo * fix: remove unneeded map to undefined * chore(e2e): rename variable for non-ggs enabled repo
- Loading branch information
Showing
8 changed files
with
322 additions
and
2 deletions.
There are no files selected for viewing
116 changes: 116 additions & 0 deletions
116
src/routes/v2/authenticatedSites/__tests__/RepoManagement.spec.ts
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,116 @@ | ||
import express from "express" | ||
import { errAsync, okAsync } from "neverthrow" | ||
import request from "supertest" | ||
|
||
import { BadRequestError } from "@errors/BadRequestError" | ||
import { ForbiddenError } from "@errors/ForbiddenError" | ||
import GitFileSystemError from "@errors/GitFileSystemError" | ||
import GitHubApiError from "@errors/GitHubApiError" | ||
|
||
import { AuthorizationMiddleware } from "@middleware/authorization" | ||
import { attachReadRouteHandlerWrapper } from "@middleware/routeHandler" | ||
|
||
import { | ||
generateRouter, | ||
generateRouterForDefaultUserWithSite, | ||
} from "@fixtures/app" | ||
import RepoManagementService from "@services/admin/RepoManagementService" | ||
|
||
import { RepoManagementRouter } from "../repoManagement" | ||
|
||
describe("RepoManagementRouter", () => { | ||
const mockRepoManagementService = { | ||
resetRepo: jest.fn(), | ||
} | ||
|
||
const mockAuthorizationMiddleware = { | ||
verifySiteAdmin: jest.fn(), | ||
} | ||
|
||
const router = new RepoManagementRouter({ | ||
repoManagementService: (mockRepoManagementService as unknown) as RepoManagementService, | ||
authorizationMiddleware: (mockAuthorizationMiddleware as unknown) as AuthorizationMiddleware, | ||
}) | ||
|
||
const subrouter = express() | ||
// We can use read route handler here because we don't need to lock the repo | ||
subrouter.post("/resetRepo", attachReadRouteHandlerWrapper(router.resetRepo)) | ||
|
||
const app = generateRouter(subrouter) | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
describe("resetRepo", () => { | ||
it("should return 200 if repo was successfully reset", async () => { | ||
mockRepoManagementService.resetRepo.mockReturnValueOnce( | ||
okAsync(undefined) | ||
) | ||
|
||
const response = await request(app).post("/resetRepo").send({ | ||
branchName: "branch-name", | ||
commitSha: "commit-sha", | ||
}) | ||
|
||
expect(response.status).toBe(200) | ||
expect(mockRepoManagementService.resetRepo).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it("should return 400 if a BadRequestError is received", async () => { | ||
mockRepoManagementService.resetRepo.mockReturnValueOnce( | ||
errAsync(new BadRequestError("error")) | ||
) | ||
|
||
const response = await request(app).post("/resetRepo").send({ | ||
branchName: "branch-name", | ||
commitSha: "commit-sha", | ||
}) | ||
|
||
expect(response.status).toBe(400) | ||
expect(mockRepoManagementService.resetRepo).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it("should return 403 if a ForbiddenError is received", async () => { | ||
mockRepoManagementService.resetRepo.mockReturnValueOnce( | ||
errAsync(new ForbiddenError()) | ||
) | ||
|
||
const response = await request(app).post("/resetRepo").send({ | ||
branchName: "branch-name", | ||
commitSha: "commit-sha", | ||
}) | ||
|
||
expect(response.status).toBe(403) | ||
expect(mockRepoManagementService.resetRepo).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it("should return 500 if a GitFileSystemError is received", async () => { | ||
mockRepoManagementService.resetRepo.mockReturnValueOnce( | ||
errAsync(new GitFileSystemError("error")) | ||
) | ||
|
||
const response = await request(app).post("/resetRepo").send({ | ||
branchName: "branch-name", | ||
commitSha: "commit-sha", | ||
}) | ||
|
||
expect(response.status).toBe(500) | ||
expect(mockRepoManagementService.resetRepo).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it("should return 502 if a GitHubApiError error is received", async () => { | ||
mockRepoManagementService.resetRepo.mockReturnValueOnce( | ||
errAsync(new GitHubApiError("error")) | ||
) | ||
|
||
const response = await request(app).post("/resetRepo").send({ | ||
branchName: "branch-name", | ||
commitSha: "commit-sha", | ||
}) | ||
|
||
expect(response.status).toBe(502) | ||
expect(mockRepoManagementService.resetRepo).toHaveBeenCalledTimes(1) | ||
}) | ||
}) | ||
}) |
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,71 @@ | ||
import autoBind from "auto-bind" | ||
import express from "express" | ||
|
||
import { BadRequestError } from "@errors/BadRequestError" | ||
import { ForbiddenError } from "@errors/ForbiddenError" | ||
|
||
import { AuthorizationMiddleware } from "@middleware/authorization" | ||
import { attachWriteRouteHandlerWrapper } from "@middleware/routeHandler" | ||
|
||
import UserWithSiteSessionData from "@root/classes/UserWithSiteSessionData" | ||
import GitFileSystemError from "@root/errors/GitFileSystemError" | ||
import { attachSiteHandler } from "@root/middleware" | ||
import type { RequestHandler } from "@root/types" | ||
import RepoManagementService from "@services/admin/RepoManagementService" | ||
|
||
interface RepoManagementRouterProps { | ||
repoManagementService: RepoManagementService | ||
authorizationMiddleware: AuthorizationMiddleware | ||
} | ||
|
||
export class RepoManagementRouter { | ||
private readonly repoManagementService | ||
|
||
private readonly authorizationMiddleware | ||
|
||
constructor({ | ||
repoManagementService, | ||
authorizationMiddleware, | ||
}: RepoManagementRouterProps) { | ||
this.repoManagementService = repoManagementService | ||
this.authorizationMiddleware = authorizationMiddleware | ||
autoBind(this) | ||
} | ||
|
||
resetRepo: RequestHandler< | ||
never, | ||
void | { message: string }, | ||
{ branchName: string; commitSha: string }, | ||
unknown, | ||
{ userWithSiteSessionData: UserWithSiteSessionData } | ||
> = async (req, res) => { | ||
const { userWithSiteSessionData } = res.locals | ||
const { branchName, commitSha } = req.body | ||
|
||
return this.repoManagementService | ||
.resetRepo(userWithSiteSessionData, branchName, commitSha) | ||
.map(() => res.status(200).send()) | ||
.mapErr((error) => { | ||
if (error instanceof BadRequestError) { | ||
return res.status(400).json({ message: error.message }) | ||
} | ||
if (error instanceof ForbiddenError) { | ||
return res.status(403).json({ message: error.message }) | ||
} | ||
if (error instanceof GitFileSystemError) { | ||
return res.status(500).json({ message: error.message }) | ||
} | ||
return res.status(502).json({ message: error.message }) | ||
}) | ||
} | ||
|
||
getRouter() { | ||
const router = express.Router({ mergeParams: true }) | ||
router.use(attachSiteHandler) | ||
router.use(this.authorizationMiddleware.verifySiteMember) | ||
|
||
router.post("/resetRepo", attachWriteRouteHandlerWrapper(this.resetRepo)) | ||
|
||
return router | ||
} | ||
} |
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,53 @@ | ||
import { ResultAsync, errAsync } from "neverthrow" | ||
|
||
import { BadRequestError } from "@errors/BadRequestError" | ||
import { ForbiddenError } from "@errors/ForbiddenError" | ||
import GitFileSystemError from "@errors/GitFileSystemError" | ||
import GitHubApiError from "@errors/GitHubApiError" | ||
|
||
import UserWithSiteSessionData from "@root/classes/UserWithSiteSessionData" | ||
import { ISOMER_E2E_TEST_REPOS } from "@root/constants" | ||
import RepoService from "@services/db/RepoService" | ||
|
||
interface RepoManagementServiceProps { | ||
repoService: RepoService | ||
} | ||
|
||
class RepoManagementService { | ||
private readonly repoService: RepoManagementServiceProps["repoService"] | ||
|
||
constructor({ repoService }: RepoManagementServiceProps) { | ||
this.repoService = repoService | ||
} | ||
|
||
resetRepo( | ||
sessionData: UserWithSiteSessionData, | ||
branchName: string, | ||
commitSha: string | ||
): ResultAsync< | ||
void, | ||
ForbiddenError | BadRequestError | GitFileSystemError | GitHubApiError | ||
> { | ||
const { siteName } = sessionData | ||
|
||
if (!ISOMER_E2E_TEST_REPOS.includes(siteName)) { | ||
return errAsync(new ForbiddenError(`${siteName} is not an e2e test repo`)) | ||
} | ||
|
||
return ResultAsync.fromPromise( | ||
this.repoService.updateRepoState(sessionData, { commitSha, branchName }), | ||
(error) => { | ||
if (error instanceof BadRequestError) { | ||
return new BadRequestError(error.message) | ||
} | ||
if (error instanceof GitFileSystemError) { | ||
return new GitFileSystemError(error.message) | ||
} | ||
|
||
return new GitHubApiError(`Failed to reset repo to commit ${commitSha}`) | ||
} | ||
) | ||
} | ||
} | ||
|
||
export default RepoManagementService |
60 changes: 60 additions & 0 deletions
60
src/services/admin/__tests__/RepoManagementService.spec.ts
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,60 @@ | ||
import { ForbiddenError } from "@errors/ForbiddenError" | ||
|
||
import UserWithSiteSessionData from "@root/classes/UserWithSiteSessionData" | ||
import { ISOMER_E2E_TEST_REPOS } from "@root/constants" | ||
import _RepoManagementService from "@services/admin/RepoManagementService" | ||
import RepoService from "@services/db/RepoService" | ||
|
||
const MockRepoService = { | ||
updateRepoState: jest.fn(), | ||
} | ||
|
||
const RepoManagementService = new _RepoManagementService({ | ||
repoService: (MockRepoService as unknown) as RepoService, | ||
}) | ||
|
||
describe("RepoManagementService", () => { | ||
// Prevent inter-test pollution of mocks | ||
afterEach(() => jest.clearAllMocks()) | ||
|
||
describe("resetRepo", () => { | ||
it("should reset an e2e test repo successfully", async () => { | ||
const mockSessionData = new UserWithSiteSessionData({ | ||
githubId: "githubId", | ||
accessToken: "accessToken", | ||
isomerUserId: "isomerUserId", | ||
email: "email", | ||
siteName: ISOMER_E2E_TEST_REPOS[0], | ||
}) | ||
MockRepoService.updateRepoState.mockResolvedValueOnce(undefined) | ||
|
||
await RepoManagementService.resetRepo( | ||
mockSessionData, | ||
"branchName", | ||
"commitSha" | ||
) | ||
|
||
expect(MockRepoService.updateRepoState).toHaveBeenCalledTimes(1) | ||
}) | ||
|
||
it("should not reset a non-e2e test repo", async () => { | ||
const mockSessionData = new UserWithSiteSessionData({ | ||
githubId: "githubId", | ||
accessToken: "accessToken", | ||
isomerUserId: "isomerUserId", | ||
email: "email", | ||
siteName: "some-other-site", | ||
}) | ||
|
||
const result = await RepoManagementService.resetRepo( | ||
mockSessionData, | ||
"branchName", | ||
"commitSha" | ||
) | ||
|
||
expect(result.isErr()).toBe(true) | ||
expect(result._unsafeUnwrapErr()).toBeInstanceOf(ForbiddenError) | ||
expect(MockRepoService.updateRepoState).toHaveBeenCalledTimes(0) | ||
}) | ||
}) | ||
}) |
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