From 75d69820b9175fb11aee32db599c6242ad7ea18b Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Thu, 28 Sep 2023 16:26:50 +0800 Subject: [PATCH] feat: add ability to update repo state for GGS (#949) * chore: update admin repo list and add e2e repos * chore: reorder unit tests to match order on main file * feat: add ability to update repo state for GGS * feat: add unit tests for new functionality --- src/constants/constants.ts | 6 + src/services/db/GitFileSystemService.ts | 68 +- src/services/db/GitHubService.js | 4 +- src/services/db/RepoService.ts | 45 +- .../db/__tests__/GitFileSystemService.spec.ts | 834 ++++++++++++------ .../db/__tests__/GitHubService.spec.ts | 14 + src/services/db/__tests__/RepoService.spec.ts | 470 +++++----- 7 files changed, 923 insertions(+), 518 deletions(-) diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 10672f96c..44d3cb210 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -40,6 +40,7 @@ export const ISOMER_ADMIN_REPOS = [ "isomercms-backend", "isomercms-frontend", "isomer-redirection", + "isomer-indirection", "isomerpages-template", "isomer-conversion-scripts", "isomer-wysiwyg", @@ -53,6 +54,11 @@ export const ISOMER_ADMIN_REPOS = [ "infra", "markdown-helper", ] +export const ISOMER_E2E_TEST_REPOS = [ + "e2e-test-repo", + "e2e-email-test-repo", + "e2e-notggs-test-repo", +] export const INACTIVE_USER_THRESHOLD_DAYS = 60 export const GITHUB_ORG_REPOS_ENDPOINT = `https://api.github.com/orgs/${ISOMER_GITHUB_ORG_NAME}/repos` diff --git a/src/services/db/GitFileSystemService.ts b/src/services/db/GitFileSystemService.ts index a0dc4c645..8fe1a4332 100644 --- a/src/services/db/GitFileSystemService.ts +++ b/src/services/db/GitFileSystemService.ts @@ -21,6 +21,7 @@ import { config } from "@config/config" import logger from "@logger/logger" +import { BadRequestError } from "@errors/BadRequestError" import { ConflictError } from "@errors/ConflictError" import GitFileSystemError from "@errors/GitFileSystemError" import GitFileSystemNeedsRollbackError from "@errors/GitFileSystemNeedsRollbackError" @@ -155,8 +156,11 @@ export default class GitFileSystemService { }) } - // Ensure that the repository is in the BRANCH_REF branch - ensureCorrectBranch(repoName: string): ResultAsync { + // Ensure that the repository is in the specified branch + ensureCorrectBranch( + repoName: string, + branchName: string + ): ResultAsync { return ResultAsync.fromPromise( this.git .cwd(`${EFS_VOL_PATH}/${repoName}`) @@ -171,11 +175,11 @@ export default class GitFileSystemService { return new GitFileSystemError("An unknown error occurred") } ).andThen((currentBranch) => { - if (currentBranch !== BRANCH_REF) { + if (currentBranch !== branchName) { return ResultAsync.fromPromise( - this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).checkout(BRANCH_REF), + this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).checkout(branchName), (error) => { - logger.error(`Error when checking out ${BRANCH_REF}: ${error}`) + logger.error(`Error when checking out ${branchName}: ${error}`) if (error instanceof GitError) { return new GitFileSystemError("Unable to checkout branch") @@ -346,7 +350,7 @@ export default class GitFileSystemService { ) } - return this.ensureCorrectBranch(repoName).andThen(() => + return this.ensureCorrectBranch(repoName, BRANCH_REF).andThen(() => ResultAsync.fromPromise( this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).pull(), (error) => { @@ -398,6 +402,7 @@ export default class GitFileSystemService { // Push the latest changes to upstream Git hosting provider push( repoName: string, + branchName: string, isForce = false ): ResultAsync { return this.isValidGitRepo(repoName).andThen((isValid) => { @@ -407,7 +412,7 @@ export default class GitFileSystemService { ) } - return this.ensureCorrectBranch(repoName) + return this.ensureCorrectBranch(repoName, branchName) .andThen(() => ResultAsync.fromPromise( isForce @@ -486,7 +491,7 @@ export default class GitFileSystemService { const commitMessage = JSON.stringify(commitMessageObj) - return this.ensureCorrectBranch(repoName) + return this.ensureCorrectBranch(repoName, BRANCH_REF) .andThen(() => { if (skipGitAdd) { // This is necessary when we have performed a git mv @@ -1173,4 +1178,51 @@ export default class GitFileSystemService { ) }) } + + updateRepoState( + repoName: string, + branchName: string, + sha: string + ): ResultAsync { + return this.isValidGitRepo(repoName).andThen((isValid) => { + if (!isValid) { + return errAsync( + new GitFileSystemError(`Folder "${repoName}" is not a valid Git repo`) + ) + } + + return this.ensureCorrectBranch(repoName, branchName) + .andThen(() => + ResultAsync.fromPromise( + this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).catFile(["-t", sha]), + (error) => { + // An error is thrown if the SHA does not exist in the branch + if (error instanceof GitError) { + return new BadRequestError("The provided SHA is invalid") + } + + return new GitFileSystemError("An unknown error occurred") + } + ) + ) + .andThen(() => + ResultAsync.fromPromise( + this.git.cwd(`${EFS_VOL_PATH}/${repoName}`).reset(["--hard", sha]), + (error) => { + logger.error(`Error when updating repo state: ${error}`) + + if (error instanceof GitError) { + return new GitFileSystemError( + `Unable to update repo state to commit SHA ${sha}` + ) + } + + return new GitFileSystemError("An unknown error occurred") + } + ) + ) + .andThen(() => this.push(repoName, branchName, true)) + .map(() => undefined) + }) + } } diff --git a/src/services/db/GitHubService.js b/src/services/db/GitHubService.js index def4aca2d..5ee7b8e74 100644 --- a/src/services/db/GitHubService.js +++ b/src/services/db/GitHubService.js @@ -455,10 +455,10 @@ class GitHubService { return newCommitSha } - async updateRepoState(sessionData, { commitSha }) { + async updateRepoState(sessionData, { commitSha, branchName = BRANCH_REF }) { const { accessToken } = sessionData const { siteName } = sessionData - const refEndpoint = `${siteName}/git/refs/heads/${BRANCH_REF}` + const refEndpoint = `${siteName}/git/refs/heads/${branchName}` const headers = { Authorization: `token ${accessToken}`, } diff --git a/src/services/db/RepoService.ts b/src/services/db/RepoService.ts index 93012ee18..5a0fbd0c6 100644 --- a/src/services/db/RepoService.ts +++ b/src/services/db/RepoService.ts @@ -25,6 +25,8 @@ import { GitHubService } from "./GitHubService" import * as ReviewApi from "./review" const PLACEHOLDER_FILE_NAME = ".keep" +const BRANCH_REF = config.get("github.branchRef") + export default class RepoService extends GitHubService { private readonly gitFileSystemService: GitFileSystemService @@ -172,7 +174,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return { sha: result.value.newSha } } return await super.create(sessionData, { @@ -385,7 +387,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return { newSha: result.value } } @@ -430,7 +432,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return } @@ -497,7 +499,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return } @@ -535,7 +537,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return { newSha: result.value } } @@ -626,7 +628,7 @@ export default class RepoService extends GitHubService { throw result.error } - this.gitFileSystemService.push(sessionData.siteName) + this.gitFileSystemService.push(sessionData.siteName, BRANCH_REF) return { newSha: result.value } } @@ -737,8 +739,35 @@ export default class RepoService extends GitHubService { }) } - async updateRepoState(sessionData: any, { commitSha }: any): Promise { - return await super.updateRepoState(sessionData, { commitSha }) + async updateRepoState( + sessionData: UserWithSiteSessionData, + { + commitSha, + branchName = BRANCH_REF, + }: { commitSha: string; branchName?: string } + ): Promise { + const { siteName } = sessionData + if ( + this.isRepoWhitelisted( + siteName, + this.getGgsWhitelistedRepos(sessionData.growthbook) + ) + ) { + logger.info( + `Updating repo state for site ${siteName} to ${commitSha} on local Git file system` + ) + const result = await this.gitFileSystemService.updateRepoState( + siteName, + branchName, + commitSha + ) + if (result.isErr()) { + throw result.error + } + return + } + + return await super.updateRepoState(sessionData, { commitSha, branchName }) } async checkHasAccess(sessionData: any): Promise { diff --git a/src/services/db/__tests__/GitFileSystemService.spec.ts b/src/services/db/__tests__/GitFileSystemService.spec.ts index 3e640a4e1..6f96368a6 100644 --- a/src/services/db/__tests__/GitFileSystemService.spec.ts +++ b/src/services/db/__tests__/GitFileSystemService.spec.ts @@ -6,6 +6,7 @@ import { GitError, SimpleGit } from "simple-git" import config from "@config/config" +import { BadRequestError } from "@errors/BadRequestError" import { ConflictError } from "@errors/ConflictError" import GitFileSystemError from "@errors/GitFileSystemError" import GitFileSystemNeedsRollbackError from "@errors/GitFileSystemNeedsRollbackError" @@ -23,11 +24,8 @@ import { GitDirectoryItem, GitFile } from "@root/types/gitfilesystem" import _GitFileSystemService from "@services/db/GitFileSystemService" const MockSimpleGit = { - checkIsRepo: jest.fn(), clone: jest.fn(), cwd: jest.fn(), - remote: jest.fn(), - revparse: jest.fn(), } const GitFileSystemService = new _GitFileSystemService( @@ -365,9 +363,10 @@ describe("GitFileSystemService", () => { }) describe("ensureCorrectBranch", () => { - it("should perform a branch change if the current branch is not the correct branch", async () => { + it("should perform a branch change if the current branch is not the desired branch", async () => { const revparseMock = jest.fn().mockResolvedValueOnce("incorrect-branch") const checkoutMock = jest.fn().mockResolvedValueOnce(undefined) + const anotherBranchRef = "another-branch-ref" MockSimpleGit.cwd.mockReturnValueOnce({ revparse: revparseMock, @@ -376,7 +375,31 @@ describe("GitFileSystemService", () => { checkout: checkoutMock, }) - const result = await GitFileSystemService.ensureCorrectBranch("fake-repo") + const result = await GitFileSystemService.ensureCorrectBranch( + "fake-repo", + anotherBranchRef + ) + + expect(revparseMock).toHaveBeenCalledWith(["--abbrev-ref", "HEAD"]) + expect(checkoutMock).toHaveBeenCalledWith(anotherBranchRef) + expect(result._unsafeUnwrap()).toBeTrue() + }) + + it("should perform a branch change if the current branch is not the default branch", async () => { + const revparseMock = jest.fn().mockResolvedValueOnce("incorrect-branch") + const checkoutMock = jest.fn().mockResolvedValueOnce(undefined) + + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: revparseMock, + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + checkout: checkoutMock, + }) + + const result = await GitFileSystemService.ensureCorrectBranch( + "fake-repo", + BRANCH_REF + ) expect(revparseMock).toHaveBeenCalledWith(["--abbrev-ref", "HEAD"]) expect(checkoutMock).toHaveBeenCalledWith(BRANCH_REF) @@ -390,7 +413,10 @@ describe("GitFileSystemService", () => { revparse: revparseMock, }) - const result = await GitFileSystemService.ensureCorrectBranch("fake-repo") + const result = await GitFileSystemService.ensureCorrectBranch( + "fake-repo", + BRANCH_REF + ) expect(revparseMock).toHaveBeenCalledWith(["--abbrev-ref", "HEAD"]) expect(MockSimpleGit.cwd).toHaveBeenCalledTimes(1) @@ -402,7 +428,10 @@ describe("GitFileSystemService", () => { revparse: jest.fn().mockRejectedValueOnce(new GitError()), }) - const result = await GitFileSystemService.ensureCorrectBranch("fake-repo") + const result = await GitFileSystemService.ensureCorrectBranch( + "fake-repo", + BRANCH_REF + ) expect(result._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) @@ -415,7 +444,10 @@ describe("GitFileSystemService", () => { checkout: jest.fn().mockRejectedValueOnce(new GitError()), }) - const result = await GitFileSystemService.ensureCorrectBranch("fake-repo") + const result = await GitFileSystemService.ensureCorrectBranch( + "fake-repo", + BRANCH_REF + ) expect(result._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) @@ -685,7 +717,34 @@ describe("GitFileSystemService", () => { push: jest.fn().mockResolvedValueOnce(undefined), }) - const result = await GitFileSystemService.push("fake-repo") + const result = await GitFileSystemService.push("fake-repo", BRANCH_REF) + + expect(result.isOk()).toBeTrue() + }) + + it("should push successfully for a valid Git repo with a non-standard branch ref", async () => { + const nonStandardBranchRef = "non-standard-branch-ref" + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(nonStandardBranchRef), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + push: jest.fn().mockResolvedValueOnce(undefined), + }) + + const result = await GitFileSystemService.push( + "fake-repo", + nonStandardBranchRef + ) expect(result.isOk()).toBeTrue() }) @@ -711,7 +770,7 @@ describe("GitFileSystemService", () => { push: jest.fn().mockResolvedValueOnce(undefined), }) - const result = await GitFileSystemService.push("fake-repo") + const result = await GitFileSystemService.push("fake-repo", BRANCH_REF) expect(result.isOk()).toBeTrue() }) @@ -737,7 +796,7 @@ describe("GitFileSystemService", () => { push: jest.fn().mockRejectedValueOnce(new GitError()), }) - const result = await GitFileSystemService.push("fake-repo") + const result = await GitFileSystemService.push("fake-repo", BRANCH_REF) expect(result._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) @@ -747,7 +806,7 @@ describe("GitFileSystemService", () => { checkIsRepo: jest.fn().mockResolvedValueOnce(false), }) - const result = await GitFileSystemService.push("fake-repo") + const result = await GitFileSystemService.push("fake-repo", BRANCH_REF) expect(result._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) @@ -1984,294 +2043,493 @@ describe("GitFileSystemService", () => { ) expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) + }) - describe("deleteFile", () => { - it("should delete a file successfully", async () => { - // getLatestCommitOfBranch - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "test-commit-sha", - }, - }), - }) - - // getGitBlobHash - MockSimpleGit.cwd.mockReturnValueOnce({ - revparse: jest.fn().mockResolvedValueOnce("fake-old-hash"), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - checkIsRepo: jest.fn().mockResolvedValueOnce(true), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - remote: jest - .fn() - .mockResolvedValueOnce( - `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` - ), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - add: jest.fn().mockResolvedValueOnce(undefined), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - commit: jest.fn().mockResolvedValueOnce({ commit: "fake-new-hash" }), - }) - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir/fake-file", - "fake-old-hash", - "fake-user-id", - false - ) + describe("deleteFile", () => { + it("should delete a file successfully", async () => { + // getLatestCommitOfBranch + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "test-commit-sha", + }, + }), + }) + + // getGitBlobHash + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce("fake-old-hash"), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) - expect(actual._unsafeUnwrap()).toEqual("fake-new-hash") - }) - - it("should return a error if the file is not valid", async () => { - // getLatestCommitOfBranch - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "test-commit-sha", - }, - }), - }) - const mockStats = new Stats() - const spyGetFilePathStats = jest - .spyOn(GitFileSystemService, "getFilePathStats") + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() .mockResolvedValueOnce( - okAsync({ - ...mockStats, - isFile: () => false, - isDirectory: () => true, - }) - ) - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir", - "fake-old-hash", - "fake-user-id", - false + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + add: jest.fn().mockResolvedValueOnce(undefined), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + commit: jest.fn().mockResolvedValueOnce({ commit: "fake-new-hash" }), + }) + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir/fake-file", + "fake-old-hash", + "fake-user-id", + false + ) + + expect(actual._unsafeUnwrap()).toEqual("fake-new-hash") + }) + + it("should return a error if the file is not valid", async () => { + // getLatestCommitOfBranch + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "test-commit-sha", + }, + }), + }) + const mockStats = new Stats() + const spyGetFilePathStats = jest + .spyOn(GitFileSystemService, "getFilePathStats") + .mockResolvedValueOnce( + okAsync({ + ...mockStats, + isFile: () => false, + isDirectory: () => true, + }) + ) + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir", + "fake-old-hash", + "fake-user-id", + false + ) + expect(spyGetFilePathStats).toBeCalledTimes(1) + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) + }) + + it("should return a error if the file hash does not match", async () => { + // getLatestCommitOfBranch + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "wrong-sha", + }, + }), + }) + + const mockStats = new Stats() + jest + .spyOn(GitFileSystemService, "getFilePathStats") + .mockResolvedValueOnce( + okAsync({ + ...mockStats, + isFile: () => true, + isDirectory: () => false, + }) ) - expect(spyGetFilePathStats).toBeCalledTimes(1) - expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) - }) - - it("should return a error if the file hash does not match", async () => { - // getLatestCommitOfBranch - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "wrong-sha", - }, - }), - }) - - const mockStats = new Stats() - jest - .spyOn(GitFileSystemService, "getFilePathStats") + + const spyGetGitBlobHash = jest + .spyOn(GitFileSystemService, "getGitBlobHash") + .mockReturnValueOnce(okAsync("correct-sha")) + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir", + "fake-old-hash", + "fake-user-id", + false + ) + expect(spyGetGitBlobHash).toBeCalledTimes(1) + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(ConflictError) + }) + }) + + describe("deleteDirectory", () => { + it("should delete a directory successfully", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "test-commit-sha", + }, + }), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() .mockResolvedValueOnce( - okAsync({ - ...mockStats, - isFile: () => true, - isDirectory: () => false, - }) - ) - - const spyGetGitBlobHash = jest - .spyOn(GitFileSystemService, "getGitBlobHash") - .mockReturnValueOnce(okAsync("correct-sha")) - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir", - "fake-old-hash", - "fake-user-id", - false + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + add: jest.fn().mockResolvedValueOnce(undefined), + }) + + // commit + MockSimpleGit.cwd.mockReturnValueOnce({ + commit: jest.fn().mockResolvedValueOnce({ commit: "fake-new-hash" }), + }) + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir", + "", + "fake-user-id", + true + ) + + expect(actual._unsafeUnwrap()).toEqual("fake-new-hash") + }) + + it("should return a error if the directory is not valid", async () => { + // getLatestCommitOfBranch + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "test-commit-sha", + }, + }), + }) + const mockStats = new Stats() + const spyGetFilePathStats = jest + .spyOn(GitFileSystemService, "getFilePathStats") + .mockResolvedValueOnce( + okAsync({ + ...mockStats, + isFile: () => true, + isDirectory: () => false, + }) ) - expect(spyGetGitBlobHash).toBeCalledTimes(1) - expect(actual._unsafeUnwrapErr()).toBeInstanceOf(ConflictError) - }) - }) - - describe("deleteDirectory", () => { - it("should delete a directory successfully", async () => { - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "test-commit-sha", - }, - }), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - checkIsRepo: jest.fn().mockResolvedValueOnce(true), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - remote: jest - .fn() - .mockResolvedValueOnce( - `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` - ), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - add: jest.fn().mockResolvedValueOnce(undefined), - }) - - // commit - MockSimpleGit.cwd.mockReturnValueOnce({ - commit: jest.fn().mockResolvedValueOnce({ commit: "fake-new-hash" }), - }) - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir", - "", - "fake-user-id", - true + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir", + "", + "fake-user-id", + true + ) + expect(spyGetFilePathStats).toBeCalledTimes(1) + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) + }) + + it("should rollback changes if an error occurred when committing", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + log: jest.fn().mockResolvedValueOnce({ + latest: { + author_name: "fake-author", + author_email: "fake-email", + date: "fake-date", + message: "fake-message", + hash: "test-commit-sha", + }, + }), + }) + const mockStats = new Stats() + jest + .spyOn(GitFileSystemService, "getFilePathStats") + .mockResolvedValueOnce( + okAsync({ + ...mockStats, + isFile: () => false, + isDirectory: () => true, + }) ) - expect(actual._unsafeUnwrap()).toEqual("fake-new-hash") - }) - - it("should return a error if the directory is not valid", async () => { - // getLatestCommitOfBranch - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "test-commit-sha", - }, - }), - }) - const mockStats = new Stats() - const spyGetFilePathStats = jest - .spyOn(GitFileSystemService, "getFilePathStats") + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() .mockResolvedValueOnce( - okAsync({ - ...mockStats, - isFile: () => true, - isDirectory: () => false, - }) - ) - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir", - "", - "fake-user-id", - true - ) - expect(spyGetFilePathStats).toBeCalledTimes(1) - expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) - }) - - it("should rollback changes if an error occurred when committing", async () => { - MockSimpleGit.cwd.mockReturnValueOnce({ - log: jest.fn().mockResolvedValueOnce({ - latest: { - author_name: "fake-author", - author_email: "fake-email", - date: "fake-date", - message: "fake-message", - hash: "test-commit-sha", - }, - }), - }) - const mockStats = new Stats() - jest - .spyOn(GitFileSystemService, "getFilePathStats") + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + add: jest.fn().mockResolvedValueOnce(undefined), + }) + + MockSimpleGit.cwd.mockReturnValueOnce({ + commit: jest.fn().mockRejectedValueOnce(new GitError()), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + reset: jest.fn().mockReturnValueOnce({ + clean: jest.fn().mockResolvedValueOnce(undefined), + }), + }) + + const spyRollback = jest.spyOn(GitFileSystemService, "rollback") + + const actual = await GitFileSystemService.delete( + "fake-repo", + "fake-dir", + "fake new content", + "fake-user-id", + true + ) + + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) + expect(spyRollback).toHaveBeenCalledWith("fake-repo", "test-commit-sha") + }) + }) + + describe("updateRepoState", () => { + it("should successfully update the repo state for a valid Git repo", async () => { + const mockResetFn = jest.fn().mockResolvedValueOnce("reset") + + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() .mockResolvedValueOnce( - okAsync({ - ...mockStats, - isFile: () => false, - isDirectory: () => true, - }) - ) - - MockSimpleGit.cwd.mockReturnValueOnce({ - checkIsRepo: jest.fn().mockResolvedValueOnce(true), - }) - MockSimpleGit.cwd.mockReturnValueOnce({ - remote: jest - .fn() - .mockResolvedValueOnce( - `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` - ), - }) - MockSimpleGit.cwd.mockReturnValueOnce({ - revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), - }) - MockSimpleGit.cwd.mockReturnValueOnce({ - add: jest.fn().mockResolvedValueOnce(undefined), - }) - - MockSimpleGit.cwd.mockReturnValueOnce({ - commit: jest.fn().mockRejectedValueOnce(new GitError()), - }) - MockSimpleGit.cwd.mockReturnValueOnce({ - reset: jest.fn().mockReturnValueOnce({ - clean: jest.fn().mockResolvedValueOnce(undefined), - }), - }) - - const spyRollback = jest.spyOn(GitFileSystemService, "rollback") - - const actual = await GitFileSystemService.delete( - "fake-repo", - "fake-dir", - "fake new content", - "fake-user-id", - true - ) + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + catFile: jest.fn().mockResolvedValueOnce("commit"), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + reset: mockResetFn, + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + push: jest.fn().mockResolvedValueOnce(undefined), + }) + + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + BRANCH_REF, + "fake-sha" + ) + + expect(actual._unsafeUnwrap()).toBeUndefined() + expect(mockResetFn).toHaveBeenCalledWith(["--hard", "fake-sha"]) + }) + + it("should successfully update the repo state for a valid Git repo with a non-standard branch", async () => { + const mockResetFn = jest.fn().mockResolvedValueOnce("reset") + const nonStandardBranchRef = "non-standard-branch" + + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(nonStandardBranchRef), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + catFile: jest.fn().mockResolvedValueOnce("commit"), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + reset: mockResetFn, + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(nonStandardBranchRef), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + push: jest.fn().mockResolvedValueOnce(undefined), + }) + + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + nonStandardBranchRef, + "another-fake-sha" + ) + + expect(actual._unsafeUnwrap()).toBeUndefined() + expect(mockResetFn).toHaveBeenCalledWith(["--hard", "another-fake-sha"]) + }) + + it("should return an error if an error occurred when resetting the repo", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + catFile: jest.fn().mockResolvedValueOnce("commit"), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + reset: jest.fn().mockRejectedValueOnce(new GitError()), + }) + + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + BRANCH_REF, + "fake-sha" + ) + + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) + }) + + it("should return a BadRequestError if the SHA does not exist on the branch", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + catFile: jest.fn().mockRejectedValueOnce(new GitError()), + }) + + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + BRANCH_REF, + "fake-sha" + ) + + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(BadRequestError) + }) + + it("should return an error if an error occurred when checking if the SHA exists on the branch", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(true), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + remote: jest + .fn() + .mockResolvedValueOnce( + `git@github.com:${ISOMER_GITHUB_ORG_NAME}/fake-repo.git` + ), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + revparse: jest.fn().mockResolvedValueOnce(BRANCH_REF), + }) + MockSimpleGit.cwd.mockReturnValueOnce({ + catFile: jest.fn().mockRejectedValueOnce(new Error()), + }) - expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) - expect(spyRollback).toHaveBeenCalledWith("fake-repo", "test-commit-sha") + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + BRANCH_REF, + "fake-sha" + ) + + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) + }) + + it("should return an error if the repo is not a valid Git repo", async () => { + MockSimpleGit.cwd.mockReturnValueOnce({ + checkIsRepo: jest.fn().mockResolvedValueOnce(false), }) + + const actual = await GitFileSystemService.updateRepoState( + "fake-repo", + BRANCH_REF, + "fake-sha" + ) + + expect(actual._unsafeUnwrapErr()).toBeInstanceOf(GitFileSystemError) }) }) }) diff --git a/src/services/db/__tests__/GitHubService.spec.ts b/src/services/db/__tests__/GitHubService.spec.ts index 95206a632..26778b547 100644 --- a/src/services/db/__tests__/GitHubService.spec.ts +++ b/src/services/db/__tests__/GitHubService.spec.ts @@ -897,6 +897,20 @@ describe("Github Service", () => { authHeader ) }) + + it("should update a repo state for a non-standard branch correctly", async () => { + const branchName = "test-branch" + const branchRefEndpoint = `${siteName}/git/refs/heads/${branchName}` + await service.updateRepoState(sessionData, { + commitSha: sha, + branchName, + }) + expect(mockAxiosInstance.patch).toHaveBeenCalledWith( + branchRefEndpoint, + { sha, force: true }, + authHeader + ) + }) }) describe("checkHasAccess", () => { diff --git a/src/services/db/__tests__/RepoService.spec.ts b/src/services/db/__tests__/RepoService.spec.ts index c5f2bb685..eb52fb77c 100644 --- a/src/services/db/__tests__/RepoService.spec.ts +++ b/src/services/db/__tests__/RepoService.spec.ts @@ -1,6 +1,8 @@ import { AxiosCacheInstance } from "axios-cache-interceptor" import { okAsync } from "neverthrow" +import config from "@config/config" + import { mockAccessToken, mockEmail, @@ -21,6 +23,8 @@ import _RepoService from "@services/db/RepoService" import { GitHubService } from "../GitHubService" +const BRANCH_REF = config.get("github.branchRef") + const MockAxiosInstance = { put: jest.fn(), get: jest.fn(), @@ -40,6 +44,7 @@ const MockGitFileSystemService = { getLatestCommitOfBranch: jest.fn(), renameSinglePath: jest.fn(), moveFiles: jest.fn(), + updateRepoState: jest.fn(), } const RepoService = new _RepoService( @@ -245,6 +250,81 @@ describe("RepoService", () => { }) }) + describe("readMediaFile", () => { + it("should read image from the local Git file system for whitelisted repos", async () => { + const expected: MediaFileOutput = { + name: "test content", + sha: "test-sha", + mediaUrl: "sampleBase64Img", + mediaPath: "images/test-img.jpeg", + type: "image" as ItemType, + } + MockGitFileSystemService.readMediaFile.mockResolvedValueOnce( + okAsync(expected) + ) + + const actual = await RepoService.readMediaFile( + mockUserWithSiteSessionDataAndGrowthBook, + { + directoryName: "test", + fileName: "test content", + } + ) + + expect(actual).toEqual(expected) + }) + + it("should read image from GitHub for whitelisted repos", async () => { + const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ + githubId: mockGithubId, + accessToken: mockAccessToken, + isomerUserId: mockIsomerUserId, + email: mockEmail, + siteName: "not-whitelisted", + }) + + const expected: MediaFileOutput = { + name: "test-image", + sha: "test-sha", + mediaUrl: "http://some-cdn.com/image", + mediaPath: "images/test-img.jpeg", + type: "image" as ItemType, + } + + const gitHubServiceReadDirectory = jest.spyOn( + GitHubService.prototype, + "readDirectory" + ) + const gitHubServiceGetRepoInfo = jest.spyOn( + GitHubService.prototype, + "getRepoInfo" + ) + gitHubServiceReadDirectory.mockResolvedValueOnce([ + { + name: ".keep", + }, + { + name: "test-image", + }, + { + name: "fake-dir", + }, + ]) + gitHubServiceGetRepoInfo.mockResolvedValueOnce({ private: false }) + const getMediaFileInfo = jest + .spyOn(mediaUtils, "getMediaFileInfo") + .mockResolvedValueOnce(expected) + + const actual = await RepoService.readMediaFile(sessionData, { + directoryName: "images", + fileName: "test-image", + }) + + expect(actual).toEqual(expected) + expect(getMediaFileInfo).toBeCalledTimes(1) + }) + }) + describe("readDirectory", () => { it("should read from the local Git file system if the repo is whitelisted", async () => { const expected: GitDirectoryItem[] = [ @@ -329,6 +409,115 @@ describe("RepoService", () => { }) }) + describe("readMediaDirectory", () => { + it("should return an array of files and directories from disk if repo is whitelisted", async () => { + const image: MediaFileOutput = { + name: "image-name", + sha: "test-sha", + mediaUrl: "base64ofimage", + mediaPath: "images/image-name.jpg", + type: "file", + } + const dir: MediaDirOutput = { + name: "imageDir", + type: "dir", + } + const expected = [image, dir] + MockGitFileSystemService.listDirectoryContents.mockResolvedValueOnce( + okAsync([ + { + name: "image-name", + type: "file", + sha: "test-sha", + path: "images/image-name.jpg", + }, + { + name: "imageDir", + type: "dir", + sha: "test-sha", + path: "images/imageDir", + }, + { + name: ".keep", + type: "file", + sha: "test-sha", + path: "images/.keep", + }, + ]) + ) + MockGitFileSystemService.readMediaFile.mockResolvedValueOnce( + okAsync(image) + ) + + const actual = await RepoService.readMediaDirectory( + mockUserWithSiteSessionDataAndGrowthBook, + "images" + ) + + expect(actual).toEqual(expected) + }) + + it("should return an array of files and directories from GitHub if repo is not whitelisted", async () => { + const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ + githubId: mockGithubId, + accessToken: mockAccessToken, + isomerUserId: mockIsomerUserId, + email: mockEmail, + siteName: "not-whitelisted", + }) + + const image: MediaFileOutput = { + name: "image-name", + sha: "test-sha", + mediaUrl: "base64ofimage", + mediaPath: "images/image-name.jpg", + type: "file", + } + const dir: MediaDirOutput = { + name: "imageDir", + type: "dir", + } + const expected = [image, dir] + + const gitHubServiceGetRepoInfo = jest + .spyOn(GitHubService.prototype, "getRepoInfo") + .mockResolvedValueOnce({ private: false }) + const gitHubServiceReadDirectory = jest + .spyOn(GitHubService.prototype, "readDirectory") + .mockResolvedValueOnce([ + { + name: "image-name", + type: "file", + sha: "test-sha", + path: "images/image-name.jpg", + }, + { + name: "imageDir", + type: "dir", + sha: "test-sha", + path: "images/imageDir", + }, + { + name: ".keep", + type: "file", + sha: "test-sha", + path: "images/.keep", + }, + ]) + + const repoServiceReadMediaFile = jest + .spyOn(_RepoService.prototype, "readMediaFile") + .mockResolvedValueOnce(image) + + const actual = await RepoService.readMediaDirectory(sessionData, "images") + + expect(actual).toEqual(expected) + expect(gitHubServiceGetRepoInfo).toBeCalledTimes(1) + expect(gitHubServiceReadDirectory).toBeCalledTimes(1) + expect(repoServiceReadMediaFile).toBeCalledTimes(1) + }) + }) + describe("update", () => { it("should update the local Git file system if the repo is whitelisted", async () => { const expectedSha = "fake-commit-sha" @@ -373,6 +562,59 @@ describe("RepoService", () => { }) }) + describe("delete", () => { + it("should delete a file from Git file system when repo is whitelisted", async () => { + MockGitFileSystemService.delete.mockResolvedValueOnce( + okAsync("some-fake-sha") + ) + + await RepoService.delete(mockUserWithSiteSessionDataAndGrowthBook, { + sha: "fake-original-sha", + fileName: "test.md", + directoryName: "pages", + }) + + expect(MockGitFileSystemService.delete).toBeCalledTimes(1) + expect(MockGitFileSystemService.delete).toBeCalledWith( + mockUserWithSiteSessionDataAndGrowthBook.siteName, + "pages/test.md", + "fake-original-sha", + mockUserWithSiteSessionDataAndGrowthBook.isomerUserId, + false + ) + expect(MockGitFileSystemService.push).toBeCalledTimes(1) + expect(MockGitFileSystemService.push).toBeCalledWith( + mockUserWithSiteSessionDataAndGrowthBook.siteName, + BRANCH_REF + ) + }) + + it("should delete a file from GitHub when repo is not whitelisted", async () => { + const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ + githubId: mockGithubId, + accessToken: mockAccessToken, + isomerUserId: mockIsomerUserId, + email: mockEmail, + siteName: "not-whitelisted", + }) + + const gitHubServiceDelete = jest.spyOn(GitHubService.prototype, "delete") + + await RepoService.delete(sessionData, { + sha: "fake-original-sha", + fileName: "test.md", + directoryName: "pages", + }) + + expect(gitHubServiceDelete).toBeCalledTimes(1) + expect(gitHubServiceDelete).toBeCalledWith(sessionData, { + sha: "fake-original-sha", + fileName: "test.md", + directoryName: "pages", + }) + }) + }) + describe("renameSinglePath", () => { it("should rename using the local Git file system if the repo is whitelisted", async () => { const expectedSha = "fake-commit-sha" @@ -608,31 +850,24 @@ describe("RepoService", () => { }) }) - describe("readMediaFile", () => { - it("should read image from the local Git file system for whitelisted repos", async () => { - const expected: MediaFileOutput = { - name: "test content", - sha: "test-sha", - mediaUrl: "sampleBase64Img", - mediaPath: "images/test-img.jpeg", - type: "image" as ItemType, - } - MockGitFileSystemService.readMediaFile.mockResolvedValueOnce( - okAsync(expected) + describe("updateRepoState", () => { + it("should update the repo state on the local Git file system if the repo is whitelisted", async () => { + MockGitFileSystemService.updateRepoState.mockResolvedValueOnce( + okAsync(undefined) ) - const actual = await RepoService.readMediaFile( + await RepoService.updateRepoState( mockUserWithSiteSessionDataAndGrowthBook, { - directoryName: "test", - fileName: "test content", + commitSha: "fake-sha", + branchName: "master", } ) - expect(actual).toEqual(expected) + expect(MockGitFileSystemService.updateRepoState).toBeCalledTimes(1) }) - it("should read image from GitHub for whitelisted repos", async () => { + it("should update the repo state on GitHub if the repo is not whitelisted", async () => { const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ githubId: mockGithubId, accessToken: mockAccessToken, @@ -640,207 +875,18 @@ describe("RepoService", () => { email: mockEmail, siteName: "not-whitelisted", }) - - const expected: MediaFileOutput = { - name: "test-image", - sha: "test-sha", - mediaUrl: "http://some-cdn.com/image", - mediaPath: "images/test-img.jpeg", - type: "image" as ItemType, - } - - const gitHubServiceReadDirectory = jest.spyOn( - GitHubService.prototype, - "readDirectory" - ) - const gitHubServiceGetRepoInfo = jest.spyOn( + const gitHubServiceUpdateRepoState = jest.spyOn( GitHubService.prototype, - "getRepoInfo" - ) - gitHubServiceReadDirectory.mockResolvedValueOnce([ - { - name: ".keep", - }, - { - name: "test-image", - }, - { - name: "fake-dir", - }, - ]) - gitHubServiceGetRepoInfo.mockResolvedValueOnce({ private: false }) - const getMediaFileInfo = jest - .spyOn(mediaUtils, "getMediaFileInfo") - .mockResolvedValueOnce(expected) - - const actual = await RepoService.readMediaFile(sessionData, { - directoryName: "images", - fileName: "test-image", - }) - - expect(actual).toEqual(expected) - expect(getMediaFileInfo).toBeCalledTimes(1) - }) - }) - - describe("readMediaDirectory", () => { - it("should return an array of files and directories from disk if repo is whitelisted", async () => { - const image: MediaFileOutput = { - name: "image-name", - sha: "test-sha", - mediaUrl: "base64ofimage", - mediaPath: "images/image-name.jpg", - type: "file", - } - const dir: MediaDirOutput = { - name: "imageDir", - type: "dir", - } - const expected = [image, dir] - MockGitFileSystemService.listDirectoryContents.mockResolvedValueOnce( - okAsync([ - { - name: "image-name", - type: "file", - sha: "test-sha", - path: "images/image-name.jpg", - }, - { - name: "imageDir", - type: "dir", - sha: "test-sha", - path: "images/imageDir", - }, - { - name: ".keep", - type: "file", - sha: "test-sha", - path: "images/.keep", - }, - ]) - ) - MockGitFileSystemService.readMediaFile.mockResolvedValueOnce( - okAsync(image) - ) - - const actual = await RepoService.readMediaDirectory( - mockUserWithSiteSessionDataAndGrowthBook, - "images" - ) - - expect(actual).toEqual(expected) - }) - - it("should return an array of files and directories from GitHub if repo is not whitelisted", async () => { - const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ - githubId: mockGithubId, - accessToken: mockAccessToken, - isomerUserId: mockIsomerUserId, - email: mockEmail, - siteName: "not-whitelisted", - }) - - const image: MediaFileOutput = { - name: "image-name", - sha: "test-sha", - mediaUrl: "base64ofimage", - mediaPath: "images/image-name.jpg", - type: "file", - } - const dir: MediaDirOutput = { - name: "imageDir", - type: "dir", - } - const expected = [image, dir] - - const gitHubServiceGetRepoInfo = jest - .spyOn(GitHubService.prototype, "getRepoInfo") - .mockResolvedValueOnce({ private: false }) - const gitHubServiceReadDirectory = jest - .spyOn(GitHubService.prototype, "readDirectory") - .mockResolvedValueOnce([ - { - name: "image-name", - type: "file", - sha: "test-sha", - path: "images/image-name.jpg", - }, - { - name: "imageDir", - type: "dir", - sha: "test-sha", - path: "images/imageDir", - }, - { - name: ".keep", - type: "file", - sha: "test-sha", - path: "images/.keep", - }, - ]) - - const repoServiceReadMediaFile = jest - .spyOn(_RepoService.prototype, "readMediaFile") - .mockResolvedValueOnce(image) - - const actual = await RepoService.readMediaDirectory(sessionData, "images") - - expect(actual).toEqual(expected) - expect(gitHubServiceGetRepoInfo).toBeCalledTimes(1) - expect(gitHubServiceReadDirectory).toBeCalledTimes(1) - expect(repoServiceReadMediaFile).toBeCalledTimes(1) - }) - }) - - describe("delete", () => { - it("should delete a file from Git file system when repo is whitelisted", async () => { - MockGitFileSystemService.delete.mockResolvedValueOnce( - okAsync("some-fake-sha") - ) - - await RepoService.delete(mockUserWithSiteSessionDataAndGrowthBook, { - sha: "fake-original-sha", - fileName: "test.md", - directoryName: "pages", - }) - - expect(MockGitFileSystemService.delete).toBeCalledTimes(1) - expect(MockGitFileSystemService.delete).toBeCalledWith( - mockUserWithSiteSessionDataAndGrowthBook.siteName, - "pages/test.md", - "fake-original-sha", - mockUserWithSiteSessionDataAndGrowthBook.isomerUserId, - false - ) - expect(MockGitFileSystemService.push).toBeCalledTimes(1) - expect(MockGitFileSystemService.push).toBeCalledWith( - mockUserWithSiteSessionDataAndGrowthBook.siteName + "updateRepoState" ) - }) - - it("should delete a file from GitHub when repo is not whitelisted", async () => { - const sessionData: UserWithSiteSessionData = new UserWithSiteSessionData({ - githubId: mockGithubId, - accessToken: mockAccessToken, - isomerUserId: mockIsomerUserId, - email: mockEmail, - siteName: "not-whitelisted", - }) - - const gitHubServiceDelete = jest.spyOn(GitHubService.prototype, "delete") + gitHubServiceUpdateRepoState.mockResolvedValueOnce(undefined) - await RepoService.delete(sessionData, { - sha: "fake-original-sha", - fileName: "test.md", - directoryName: "pages", + await RepoService.updateRepoState(sessionData, { + commitSha: "fake-sha", + branchName: "master", }) - expect(gitHubServiceDelete).toBeCalledTimes(1) - expect(gitHubServiceDelete).toBeCalledWith(sessionData, { - sha: "fake-original-sha", - fileName: "test.md", - directoryName: "pages", - }) + expect(gitHubServiceUpdateRepoState).toBeCalledTimes(1) }) }) })