From 9a1fd24ca8db4ff89a5455645c19aa45ba86042e Mon Sep 17 00:00:00 2001 From: Nicole Brooks <83994164+NBrooks-Roll20@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:27:18 -0500 Subject: [PATCH] adding new sheet checks to the public repo --- .actions/sheet-checks/.tool-versions | 1 + .actions/sheet-checks/README.md | 8 + .../sheet-checks/__tests__/annotate.test.ts | 56 + .../__tests__/checkAdvanced.test.ts | 37 + .../__tests__/checkLineEndings.test.ts | 64 + .../__tests__/checkNewSheet.test.ts | 79 + .../sheet-checks/__tests__/getFiles.test.ts | 75 + .../__tests__/sendSummary.test.ts | 155 + .../sheet-checks/__tests__/status.test.ts | 27 + .../__tests__/validateCSS.test.ts | 17 + .../__tests__/validateCodeOwners.test.ts | 137 + .../__tests__/validateFiles.test.ts | 117 + .../__tests__/validateHTML.test.ts | 65 + .../__tests__/validateSheetJson.test.ts | 242 ++ .../__tests__/validateTranslation.test.ts | 29 + .actions/sheet-checks/action.yml | 19 + .actions/sheet-checks/dist/index.js | 21 + .actions/sheet-checks/dist/licenses.txt | 679 ++++ .actions/sheet-checks/old.js | 510 +++ .actions/sheet-checks/package-lock.json | 3013 +++++++++++++++++ .actions/sheet-checks/package.json | 28 + .actions/sheet-checks/plan.md | 23 + .actions/sheet-checks/src/annotate.ts | 22 + .../sheet-checks/src/checks/checkAdvanced.ts | 13 + .../src/checks/checkLineEndings.ts | 28 + .../sheet-checks/src/checks/checkNewSheet.ts | 52 + .../sheet-checks/src/checks/validateCSS.ts | 60 + .../src/checks/validateCodeOwners.ts | 64 + .../sheet-checks/src/checks/validateHTML.ts | 34 + .../src/checks/validateSheetJson.ts | 96 + .../src/checks/validateTranslation.ts | 17 + .actions/sheet-checks/src/getFiles.ts | 20 + .../src/helpers/MarkdownGenerator.ts | 156 + .actions/sheet-checks/src/helpers/utils.ts | 75 + .actions/sheet-checks/src/index.ts | 123 + .actions/sheet-checks/src/sendSummary.ts | 209 ++ .actions/sheet-checks/src/settings.ts | 27 + .actions/sheet-checks/src/statuses.ts | 233 ++ .actions/sheet-checks/src/types.ts | 14 + .actions/sheet-checks/src/validateFiles.ts | 104 + .actions/sheet-checks/tsconfig.json | 12 + .github/workflows/sheet-checks.yml | 35 + 42 files changed, 6796 insertions(+) create mode 100644 .actions/sheet-checks/.tool-versions create mode 100644 .actions/sheet-checks/README.md create mode 100644 .actions/sheet-checks/__tests__/annotate.test.ts create mode 100644 .actions/sheet-checks/__tests__/checkAdvanced.test.ts create mode 100644 .actions/sheet-checks/__tests__/checkLineEndings.test.ts create mode 100644 .actions/sheet-checks/__tests__/checkNewSheet.test.ts create mode 100644 .actions/sheet-checks/__tests__/getFiles.test.ts create mode 100644 .actions/sheet-checks/__tests__/sendSummary.test.ts create mode 100644 .actions/sheet-checks/__tests__/status.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateCSS.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateCodeOwners.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateFiles.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateHTML.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateSheetJson.test.ts create mode 100644 .actions/sheet-checks/__tests__/validateTranslation.test.ts create mode 100644 .actions/sheet-checks/action.yml create mode 100644 .actions/sheet-checks/dist/index.js create mode 100644 .actions/sheet-checks/dist/licenses.txt create mode 100644 .actions/sheet-checks/old.js create mode 100644 .actions/sheet-checks/package-lock.json create mode 100644 .actions/sheet-checks/package.json create mode 100644 .actions/sheet-checks/plan.md create mode 100644 .actions/sheet-checks/src/annotate.ts create mode 100644 .actions/sheet-checks/src/checks/checkAdvanced.ts create mode 100644 .actions/sheet-checks/src/checks/checkLineEndings.ts create mode 100644 .actions/sheet-checks/src/checks/checkNewSheet.ts create mode 100644 .actions/sheet-checks/src/checks/validateCSS.ts create mode 100644 .actions/sheet-checks/src/checks/validateCodeOwners.ts create mode 100644 .actions/sheet-checks/src/checks/validateHTML.ts create mode 100644 .actions/sheet-checks/src/checks/validateSheetJson.ts create mode 100644 .actions/sheet-checks/src/checks/validateTranslation.ts create mode 100644 .actions/sheet-checks/src/getFiles.ts create mode 100644 .actions/sheet-checks/src/helpers/MarkdownGenerator.ts create mode 100644 .actions/sheet-checks/src/helpers/utils.ts create mode 100644 .actions/sheet-checks/src/index.ts create mode 100644 .actions/sheet-checks/src/sendSummary.ts create mode 100644 .actions/sheet-checks/src/settings.ts create mode 100644 .actions/sheet-checks/src/statuses.ts create mode 100644 .actions/sheet-checks/src/types.ts create mode 100644 .actions/sheet-checks/src/validateFiles.ts create mode 100644 .actions/sheet-checks/tsconfig.json create mode 100644 .github/workflows/sheet-checks.yml diff --git a/.actions/sheet-checks/.tool-versions b/.actions/sheet-checks/.tool-versions new file mode 100644 index 0000000000..5b19f03d86 --- /dev/null +++ b/.actions/sheet-checks/.tool-versions @@ -0,0 +1 @@ +nodejs 20.1.0 diff --git a/.actions/sheet-checks/README.md b/.actions/sheet-checks/README.md new file mode 100644 index 0000000000..64a4acdd28 --- /dev/null +++ b/.actions/sheet-checks/README.md @@ -0,0 +1,8 @@ +# Dependencies + +* You will need to install the correct asdf node js listed in the root of this repo's .tools-version file +* Then you will need to run `npm i -g @vercel/ncc` +* Then run `npm run build` + +NOTE: You will need to run `npm run build` and commit that change every time you make file changes +in `index.ts` \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/annotate.test.ts b/.actions/sheet-checks/__tests__/annotate.test.ts new file mode 100644 index 0000000000..af8761ed48 --- /dev/null +++ b/.actions/sheet-checks/__tests__/annotate.test.ts @@ -0,0 +1,56 @@ +import { it, expect, describe, vi } from "vitest" +import { sendAllStatuses } from "../src/annotate"; +import { VALIDATION_STATUS } from "../src/statuses"; + +const hoisted = vi.hoisted(() => { + return { + core: { + error: vi.fn(), + warning: vi.fn(), + notice: vi.fn(), + setFailed: vi.fn(), + }, + } +}); + +vi.mock("@actions/core", () => hoisted.core); + +describe("annotate", () => { + describe("sendAllStatuses", () => { + it("should send an error message to the core", () => { + // arrange + const error = VALIDATION_STATUS.CHANGING_MULTIPLE_SHEETS; + const statuses = [error]; + + // act + sendAllStatuses(statuses); + + // assert + expect(hoisted.core.error).toHaveBeenCalledWith(error.description, undefined); + }); + + it("should send a warning message to the core", () => { + // arrange + const warning = VALIDATION_STATUS.SHEET_HTTP_GET_FAILED; + const statuses = [warning]; + + // act + sendAllStatuses(statuses); + + // assert + expect(hoisted.core.warning).toHaveBeenCalledWith(warning.description, undefined); + }); + + it("should send a notice message to the core", () => { + // arrange + const notice = VALIDATION_STATUS.CHANGING_DOT_FILE; + const statuses = [notice]; + + // act + sendAllStatuses(statuses); + + // assert + expect(hoisted.core.notice).toHaveBeenCalledWith(notice.description, undefined); + }); + }); +}); \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/checkAdvanced.test.ts b/.actions/sheet-checks/__tests__/checkAdvanced.test.ts new file mode 100644 index 0000000000..643d8d830e --- /dev/null +++ b/.actions/sheet-checks/__tests__/checkAdvanced.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { SheetJSON } from "../src/types"; +import { isAdvanced } from "../src/checks/checkAdvanced" +import { VALIDATION_STATUS } from "../src/statuses"; + +const sheetJSON: SheetJSON = { + html: "vampireArchaeologistRPG/varpg.html", + css: "vampireArchaeologistRPG/varpg.html", + preview: "vampireArchaeologistRPG/image.png", + authors: "me", + roll20userid: "01", + advanced: false, + instructions: "You're a vampire and you can't find your stuff! Lie about your credentials so you're allowed to find it." +}; + +describe("isAdvanced", () => { + it("should return an advancedSheet property of false for empty/false advanced sheetJSON values", () => { + const firstJSON = {...sheetJSON}; + delete firstJSON.advanced; + const firstResult = isAdvanced(firstJSON); + expect(firstResult.advancedSheet).toBeFalsy(); + const secondResult = isAdvanced(sheetJSON); + expect(secondResult.advancedSheet).toBeFalsy(); + }) + it("should return an advancedSheet property of true for true advanced sheetJSON values", () => { + const firstJSON = {...sheetJSON}; + firstJSON.advanced = true; + const firstResult = isAdvanced(firstJSON); + expect(firstResult.advancedSheet).toBeTruthy(); + }) + it("should return a status indicating the sheet is being skipped when it finds a true advanced sheetJSON value", () => { + const firstJSON = {...sheetJSON}; + firstJSON.advanced = true; + const firstResult = isAdvanced(firstJSON); + expect(firstResult.advancedStatuses).toContainEqual(VALIDATION_STATUS.SKIPPED_ADVANCED_SHEET); + }) +}) \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/checkLineEndings.test.ts b/.actions/sheet-checks/__tests__/checkLineEndings.test.ts new file mode 100644 index 0000000000..ea4bc31ca3 --- /dev/null +++ b/.actions/sheet-checks/__tests__/checkLineEndings.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi } from "vitest" + +import { hasCRLF, checkAllLineEndings } from "../src/checks/checkLineEndings" +import { VALIDATION_STATUS } from "../src/statuses"; + +const badFile = "This is a bad line\r\nThis is another line"; +const goodFile = "This is a bad line\nThis is another line"; + +const sheetFile = "MidnightTacoBellRPG"; + +vi.mock("@actions/core", () => { + return { + debug: vi.fn(), + } +}); + +describe("checkLineEndings", () => { + vi.stubEnv("GITHUB_WORKSPACE", "/github/workspace"); + + describe("hasCRLF", () => { + it("should return true if it the provided string has CRLF endings", () => { + const result = hasCRLF(badFile) + + expect(result).toBeTruthy(); + }) + + it("should return false if the provided string has LF endings", () => { + const result = hasCRLF(goodFile) + + expect(result).toBeFalsy(); + }) + }) + + describe("checkAllLineEndings", () => { + it("should return a success when all provided file paths have good endings", () => { + const result = checkAllLineEndings({ + ["a_random_name.html"]: goodFile, + ["final_name.html"]: goodFile, + }, sheetFile); + expect(result).toStrictEqual([]) + }); + it("should return a failure if any provided file paths have bad endings, and include their file paths as errors", () => { + const result = checkAllLineEndings({ + [`a_random_name.html`]: goodFile, + [`a_different_name.css`]: badFile, + [`a_third_name.json`]: badFile, + }, sheetFile); + expect(result).toStrictEqual([ + { + ...VALIDATION_STATUS.INCORRECT_LINE_ENDINGS, + annotation: { + file: `${sheetFile}/a_different_name.css` + } + }, + { + ...VALIDATION_STATUS.INCORRECT_LINE_ENDINGS, + annotation: { + file: `${sheetFile}/a_third_name.json`, + } + }, + ]) + }) + }) +}) \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/checkNewSheet.test.ts b/.actions/sheet-checks/__tests__/checkNewSheet.test.ts new file mode 100644 index 0000000000..611098f78b --- /dev/null +++ b/.actions/sheet-checks/__tests__/checkNewSheet.test.ts @@ -0,0 +1,79 @@ +import { it, expect, describe, vi, beforeAll } from "vitest" + +import { checkNewSheet } from "../src/checks/checkNewSheet"; +import { VALIDATION_STATUS } from "../src/statuses"; + +const hoisted = vi.hoisted(() => { + return { + fetch: vi.fn(), + core: { + getInput: vi.fn(), + debug: vi.fn(), + }, + github: { + getOctokit: vi.fn(), + context: { + payload: { + pull_request: { + number: 123, + }, + } + }, + }, + octokit: { + request: vi.fn(), + }, + } +}); + +vi.stubGlobal("fetch", hoisted.fetch); + +vi.mock("@actions/core", () => hoisted.core); + +vi.mock("@actions/github", () => hoisted.github); + +const emptyResponse = { + json: () => ([]), +} + +const responseWithData = { + json: () => ([{ + shortname: "bobsburgs", + longname: "Bob's Burgers: The Movie: The Game", + system: "Flipping CORE", + path: "Bobs Burgers the TTRPG", + repo: "roll20-character-sheets", + hidden: false, + official: null, + updated_at: "2024_11_19T11:00:00.000Z", + }]), +} + +describe("checkNewSheet", () => { + beforeAll(() => { + hoisted.github.getOctokit.mockReturnValueOnce(hoisted.octokit); + hoisted.octokit.request.mockResolvedValue({ data: [] }); + }); + + describe("checkNewSheet", () => { + const mockSheetName = "Bobs Burgers the TTRPG"; + + it("returns a success state with a notice if the sheet-http service is not found", async () => { + hoisted.fetch.mockRejectedValueOnce(new Error("this is an error")); + const res = await checkNewSheet(mockSheetName); + expect(res).toStrictEqual([VALIDATION_STATUS.SHEET_HTTP_GET_FAILED]) + }); + + it("returns new sheet true if the sheet-http service is found and the sheet does not exist", async () => { + hoisted.fetch.mockResolvedValueOnce(emptyResponse); + const res = await checkNewSheet(mockSheetName); + expect(res).toStrictEqual([VALIDATION_STATUS.NEW_SHEET]) + }); + + it("returns new sheet false if the sheet-http service is found and the sheet does exist", async () => { + hoisted.fetch.mockResolvedValueOnce(responseWithData); + const res = await checkNewSheet(mockSheetName); + expect(res).toStrictEqual([]); + }); + }); +}); diff --git a/.actions/sheet-checks/__tests__/getFiles.test.ts b/.actions/sheet-checks/__tests__/getFiles.test.ts new file mode 100644 index 0000000000..4581e166c8 --- /dev/null +++ b/.actions/sheet-checks/__tests__/getFiles.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, afterEach, it, expect, describe, vi } from "vitest" + +import { convertToUtf8, getFileList } from "../src/getFiles"; + +const hoisted = vi.hoisted(() => { + return { + getInput: vi.fn(), + } +}); + +vi.mock("@actions/core", () => ({ + getInput: hoisted.getInput +})); + +describe("getFiles", () => { + describe("getFileList", () => { + it("should take a raw file list and a separator and return a list of files", async () => { + // arrange + const rawFileList = "file1,file2,file3"; + const sep = ","; + hoisted.getInput.mockReturnValueOnce(rawFileList) + .mockReturnValueOnce(sep); + + // act + const rawFiles = getFileList(); + + // assert + expect(rawFiles).toEqual(["file1", "file2", "file3"]); + }); + + it("should take a raw file list with subdirectories and a seperator and return a list of files", () => { + // arrange + const rawFileArray = [ + "example sheet name/sheet.html", + "example sheet name/sheet.css", + "example sheet name/translation.json", + "example sheet name/sheet.json", + ]; + const rawFileList = rawFileArray.join(","); + const sep = ","; + hoisted.getInput.mockReturnValueOnce(rawFileList) + .mockReturnValueOnce(sep); + + // act + const rawFiles = getFileList(); + + // assert + expect(rawFiles).toEqual(rawFileArray); + }); + }); + + describe("convertToUtf8", () => { + it("should convert an octal escaped string to utf8", () => { + // arrange + const input = '"Brigandyne 2e \\303\\251dition/sheet.json"'; + + // act + const output = convertToUtf8(input); + + // assert + expect(output).toEqual("Brigandyne 2e édition/sheet.json"); + }); + + it("should convert antoher octal escaped strihng to utf8", () => { + // arrange + const input = '"hell\\303\\264 w\\303\\264rld/sheet.json"'; + + // act + const output = convertToUtf8(input); + + // assert + expect(output).toEqual("hellô wôrld/sheet.json"); + }); + }); +}); \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/sendSummary.test.ts b/.actions/sheet-checks/__tests__/sendSummary.test.ts new file mode 100644 index 0000000000..5a877041dd --- /dev/null +++ b/.actions/sheet-checks/__tests__/sendSummary.test.ts @@ -0,0 +1,155 @@ +import { beforeEach, afterEach, it, expect, describe, vi, beforeAll } from "vitest" +import { sendSummary } from "../src/sendSummary"; + +const hoisted = vi.hoisted(() => { + return { + fetch: vi.fn(), + core: { + getInput: vi.fn(), + debug: vi.fn(), + }, + github: { + getOctokit: vi.fn(), + context: { + payload: { + pull_request: { + number: 123, + }, + } + }, + }, + octokit: { + request: vi.fn(), + rest: { + issues: { + createComment: vi.fn(), + listComments: vi.fn(), + } + } + }, + } +}); + +vi.mock("@actions/core", () => hoisted.core); + +vi.mock("@actions/github", () => hoisted.github); + +describe("sendSummary", () => { + + describe("sendSummary", () => { + beforeEach(() => { + vi.resetAllMocks(); + hoisted.github.getOctokit.mockReturnValueOnce(hoisted.octokit); + hoisted.octokit.rest.issues.createComment.mockResolvedValue({ data: [] }); + }); + + it("Creates an error if there is one to create", () => { + sendSummary([ + { + name: "", + type: "error", + description: "Random Error", + } + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + expect(firstCall.body).toContain("Random Error"); + expect(firstCall.body).toContain("ERROR:"); + }); + + it("Creates a warning if there is one to create", () => { + sendSummary([ + { + name: "", + type: "warning", + description: "Random Warning", + } + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + expect(firstCall.body).toContain("Random Warning"); + expect(firstCall.body).toContain("WARNING:"); + }); + + it("Creates a notice if there is one to create", () => { + sendSummary([ + { + name: "", + type: "notice", + description: "Random Notice", + } + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + expect(firstCall.body).toContain("Random Notice"); + expect(firstCall.body).toContain("NOTICE:"); + }); + + it("adds a code block annotation if applicable", () => { + sendSummary([ + { + name: "", + type: "warning", + description: "Random Warning", + annotation: { + title: "more data" + } + } + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + expect(firstCall.body).toContain("```\ntitle: more data\n```"); + }); + + it("combines multiple instances of the same error", () => { + sendSummary([ + { + name: "name", + type: "warning", + description: "Random Warning", + annotation: { + title: "more data" + } + }, + { + name: "name", + type: "warning", + description: "Random Warning 2", + annotation: { + title: "more data" + } + }, + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + + expect(firstCall.body).toContain("Random Warning (2 instances)"); + }); + + it("shows annotations for multiple instances of the same error with different annotations", () => { + sendSummary([ + { + name: "name", + type: "warning", + description: "Random Warning", + annotation: { + title: "a lot more info" + } + }, + { + name: "name", + type: "warning", + description: "Random Warning 2", + annotation: { + title: "more data" + } + }, + ]); + const { calls } = hoisted.octokit.rest.issues.createComment.mock; + const firstCall = calls[0][0]; + + expect(firstCall.body).toContain("```\ntitle: more data\n```"); + expect(firstCall.body).toContain("```\ntitle: a lot more info\n```"); + }); + }); +}); \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/status.test.ts b/.actions/sheet-checks/__tests__/status.test.ts new file mode 100644 index 0000000000..e648b19212 --- /dev/null +++ b/.actions/sheet-checks/__tests__/status.test.ts @@ -0,0 +1,27 @@ +import { beforeEach, afterEach, it, expect, describe, vi } from "vitest" + +const hoisted = vi.hoisted(() => { + return { + getInput: vi.fn(), + } +}); + +vi.mock("@actions/core", () => ({ + getInput: hoisted.getInput +})); + +describe("run", () => { + it("should take a raw file list and a separator and return a list of files", async () => { + // arrange + const rawFileList = "file1,file2,file3"; + const sep = ","; + hoisted.getInput.mockReturnValueOnce(rawFileList) + .mockReturnValueOnce(sep); + + // act + const rawFiles = rawFileList.split(sep); + + // assert + expect(rawFiles).toEqual(["file1", "file2", "file3"]); + }); +}) \ No newline at end of file diff --git a/.actions/sheet-checks/__tests__/validateCSS.test.ts b/.actions/sheet-checks/__tests__/validateCSS.test.ts new file mode 100644 index 0000000000..3a24e023ef --- /dev/null +++ b/.actions/sheet-checks/__tests__/validateCSS.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { SheetJSONFiles } from "../src/checks/validateSheetJson"; +import {validateCSS} from "../src/checks/validateCSS" +import { VALIDATION_STATUS } from "../src/statuses"; + +describe("validateCSS", () => { + it("should return an error when no css file is specified/found", () => { + const mockRawFiles: SheetJSONFiles = { + html: "
| A Table Header |
|---|
| a value |
| A Table Header | \n
|---|
| a value | \n
| a | \n
|---|