From 4b92c7a48e818cb938b7f3f4facd697d3ab30dc2 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Wed, 25 Oct 2023 10:31:11 +1300 Subject: [PATCH] test: add API tests --- src/api.spec.ts | 221 +++++++++++++++++++++++++++++++++++++++++++++++ src/api.ts | 5 +- vitest.config.ts | 2 + 3 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 src/api.spec.ts diff --git a/src/api.spec.ts b/src/api.spec.ts new file mode 100644 index 0000000..b545f3e --- /dev/null +++ b/src/api.spec.ts @@ -0,0 +1,221 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, + type SpyInstance, +} from "vitest"; + +import type { ActionConfig } from "./action.ts"; +import { createComment, deleteComment, getCommentIds, init, updateComment } from "./api.ts"; + +vi.mock("@actions/core"); + +describe("API", () => { + const cfg: ActionConfig = { + token: "secret", + commandScriptName: "npm", + commentId: "knip-report", + ignoreResults: false, + }; + type Octokit = ReturnType<(typeof github)["getOctokit"]>; + let octokit: Octokit; + + beforeAll(() => { + octokit = github.getOctokit("token", { + request: { + fetch: vi.fn(async () => { + throw new Error("API calls should be mocked"); + }), + }, + }); + }); + + beforeEach(() => { + process.env.GITHUB_REPOSITORY = "a/b"; + + vi.spyOn(core, "getInput").mockReturnValue(""); + vi.spyOn(github, "getOctokit").mockReturnValue(octokit); + init(cfg); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("createComment", () => { + it("should not throw", async () => { + vi.spyOn(octokit.rest.issues, "createComment").mockReturnValue( + Promise.resolve({ + data: {}, + status: 201, + }) as any, + ); + + const state = await createComment(123456, ""); + expect(state.status).toStrictEqual(201); + }); + + it("should throw if a non-201 status is returned", async () => { + const errorStatus = 401; + vi.spyOn(octokit.rest.issues, "createComment").mockReturnValue( + Promise.resolve({ + data: undefined, + status: errorStatus, + }) as any, + ); + + await expect(createComment(123456, "")).rejects.toThrow( + `Failed to create comment, expected 201 but received ${errorStatus}`, + ); + }); + }); + + describe("getCommentIds", () => { + let listCommentsToReturn: any[]; + let listCommentsSpy: SpyInstance; + + beforeEach(() => { + // This mock may look like a case of "set foo assert foo is foo" but we don't need to test the + // github API, we're testing that our iterator handling works as expected. + let call = 0; + listCommentsSpy = vi + .spyOn(octokit.rest.issues, "listComments") + .mockImplementation((async () => { + const toReturn = listCommentsToReturn[call]; + call++; + + if (!toReturn) { + return undefined; + } + + return { + data: toReturn, + status: 200, + }; + }) as any); + + vi.spyOn(octokit.paginate, "iterator").mockImplementation((rest: any) => { + return (async function* () { + const boundRest = rest; + let results: any = await boundRest(); + while (results) { + yield results; + results = await boundRest(); + } + })(); + }); + }); + + it("should return undefined for no results", async () => { + listCommentsToReturn = [[{ id: 0, body: "" }]]; + const commentIds = await getCommentIds("knip", 123456); + expect(commentIds).toBeUndefined(); + }); + + it("should return an ID for a single match", async () => { + listCommentsToReturn = [[{ id: 0, body: "" }], [{ id: 123, body: "knip" }]]; + const commentIds = await getCommentIds("knip", 123456); + expect(Array.isArray(commentIds)).toStrictEqual(true); + expect(commentIds?.length).toStrictEqual(1); + expect(commentIds!).toContain(123); + }); + + it("should return an ID for every match", async () => { + listCommentsToReturn = [ + [{ id: 0, body: "" }], + [{ id: 123, body: "knip" }], + [{ id: 456, body: "knip" }], + [{ id: 789, body: "knop" }], + ]; + const commentIds = await getCommentIds("knip", 123456); + expect(Array.isArray(commentIds)).toStrictEqual(true); + expect(commentIds?.length).toStrictEqual(2); + expect(commentIds!).toContain(123); + expect(commentIds!).toContain(456); + }); + + it("should not return an invalid match", async () => { + listCommentsToReturn = [[{ id: 0, body: "" }], [{ id: 123, body: "knop" }]]; + const commentIds = await getCommentIds("knip", 123456); + expect(commentIds).toBeUndefined(); + }); + + it("should throw if a non-201 status is returned", async () => { + listCommentsSpy.mockRestore(); + + const errorStatus = 401; + vi.spyOn(octokit.rest.issues, "listComments").mockReturnValue( + Promise.resolve({ + data: undefined, + status: errorStatus, + } as any), + ); + + await expect(getCommentIds("knip", 123456)).rejects.toThrow( + `Failed to find comment ID, expected 200 but received ${errorStatus}`, + ); + }); + }); + + describe("updateComment", () => { + it("should not throw", async () => { + vi.spyOn(octokit.rest.issues, "updateComment").mockReturnValue( + Promise.resolve({ + data: {}, + status: 200, + }) as any, + ); + + const state = await updateComment(123456, ""); + expect(state.status).toStrictEqual(200); + }); + + it("should throw if a non-200 status is returned", async () => { + const errorStatus = 401; + vi.spyOn(octokit.rest.issues, "updateComment").mockReturnValue( + Promise.resolve({ + data: undefined, + status: errorStatus, + }) as any, + ); + + await expect(updateComment(123456, "")).rejects.toThrow( + `Failed to update comment, expected 200 but received ${errorStatus}`, + ); + }); + }); + + describe("deleteComment", () => { + it("should not throw", async () => { + vi.spyOn(octokit.rest.issues, "deleteComment").mockReturnValue( + Promise.resolve({ + data: {}, + status: 204, + }) as any, + ); + + const state = await deleteComment(123456); + expect(state.status).toStrictEqual(204); + }); + + it("should throw if a non-204 status is returned", async () => { + const errorStatus = 401; + vi.spyOn(octokit.rest.issues, "deleteComment").mockReturnValue( + Promise.resolve({ + data: undefined, + status: errorStatus, + }) as any, + ); + + await expect(deleteComment(123456)).rejects.toThrow( + `Failed to delete comment, expected 204 but received ${errorStatus}`, + ); + }); + }); +}); diff --git a/src/api.ts b/src/api.ts index db95539..1e4efaf 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,10 +1,9 @@ import * as github from "@actions/github"; -import type { GitHub } from "@actions/github/lib/utils.ts"; import { type ActionConfig, getConfig } from "./action.ts"; export const GITHUB_COMMENT_MAX_COMMENT_LENGTH = 65535; -type Octokit = InstanceType; +type Octokit = ReturnType<(typeof github)["getOctokit"]>; let config: ActionConfig; let octokit: Octokit; @@ -99,7 +98,7 @@ export async function deleteComment(commentId: number): Promise