import {existsSync, readFileSync, rmSync, writeFileSync} from "node:fs"; import path from "node:path"; import type * as CoreApi from "@actions/core"; import type {Octokit} from "@octokit/rest"; import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi} from "vitest"; import * as common from "../src/common.js"; import {checkOtaPR} from "../src/ghw_check_ota_pr.js"; import type {Context, RepoImageMeta} from "../src/types.js"; import { BASE_IMAGES_TEST_DIR_PATH, getAdjustedContent, IMAGE_GLEDOPTO, IMAGE_INVALID, IMAGE_LUMI, IMAGE_TUYA, IMAGE_V12_1, IMAGE_V12_1_METAS, IMAGE_V13_1, IMAGE_V13_1_METAS, IMAGE_V13_1_METAS_MAIN, IMAGE_V14_1, IMAGE_V14_1_METAS, IMAGE_V14_2, IMAGE_V14_2_COPY, IMAGE_V14_2_MANUF_METAS, IMAGE_V14_2_METAS, IMAGES_TEST_DIR, PREV_IMAGES_TEST_DIR_PATH, useImage, withExtraMetas, } from "./data.test.js"; const github = { rest: { repos: { compareCommitsWithBasehead: vi.fn< ( ...args: Parameters ) => ReturnType >(), }, }, }; const core: Partial = { debug: console.debug, info: console.log, warning: console.warn, error: console.error, notice: console.log, startGroup: vi.fn(), endGroup: vi.fn(), }; const context: Partial = { payload: { pull_request: { number: 1, head: { sha: "abcd", }, base: { sha: "zyxw", }, }, }, issue: { owner: "Koenkk", repo: "zigbee-OTA", number: 1, }, repo: { owner: "Koenkk", repo: "zigbee-OTA", }, }; describe("Github Workflow: Check OTA PR", () => { let baseManifest: RepoImageMeta[]; let prevManifest: RepoImageMeta[]; let readManifestSpy: MockInstance; let writeManifestSpy: MockInstance; let addImageToBaseSpy: MockInstance; let addImageToPrevSpy: MockInstance; let filePaths: ReturnType[] = []; const getManifest = (fileName: string): RepoImageMeta[] => { if (fileName === common.BASE_INDEX_MANIFEST_FILENAME) { return baseManifest; } if (fileName === common.PREV_INDEX_MANIFEST_FILENAME) { return prevManifest; } throw new Error(`${fileName} not supported`); }; const setManifest = (fileName: string, content: RepoImageMeta[]): void => { const adjustedContent = getAdjustedContent(fileName, content); if (fileName === common.BASE_INDEX_MANIFEST_FILENAME) { baseManifest = adjustedContent; } else if (fileName === common.PREV_INDEX_MANIFEST_FILENAME) { prevManifest = adjustedContent; } else { throw new Error(`${fileName} not supported`); } }; const resetManifests = (): void => { baseManifest = []; prevManifest = []; }; const withBody = (body: string): Partial => { const newContext = structuredClone(context); newContext.payload!.pull_request!.body = body; return newContext; }; const expectNoChanges = (noReadManifest = false): void => { if (noReadManifest) { expect(readManifestSpy).toHaveBeenCalledTimes(0); } else { expect(readManifestSpy).toHaveBeenCalledTimes(2); } expect(addImageToBaseSpy).toHaveBeenCalledTimes(0); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(0); }; beforeAll(() => {}); afterAll(() => { readManifestSpy.mockRestore(); writeManifestSpy.mockRestore(); addImageToBaseSpy.mockRestore(); addImageToPrevSpy.mockRestore(); rmSync(BASE_IMAGES_TEST_DIR_PATH, {recursive: true, force: true}); rmSync(PREV_IMAGES_TEST_DIR_PATH, {recursive: true, force: true}); }); beforeEach(() => { resetManifests(); filePaths = []; readManifestSpy = vi.spyOn(common, "readManifest").mockImplementation(getManifest); writeManifestSpy = vi.spyOn(common, "writeManifest").mockImplementation(setManifest); addImageToBaseSpy = vi.spyOn(common, "addImageToBase"); addImageToPrevSpy = vi.spyOn(common, "addImageToPrev"); github.rest.repos.compareCommitsWithBasehead.mockImplementation( // @ts-expect-error mock () => ({data: {files: filePaths}}), ); }); afterEach(() => { rmSync(BASE_IMAGES_TEST_DIR_PATH, {recursive: true, force: true}); rmSync(PREV_IMAGES_TEST_DIR_PATH, {recursive: true, force: true}); rmSync(common.PR_ARTIFACT_DIR, {recursive: true, force: true}); }); // XXX: Util // it('Get headers', async () => { // const firmwareBuffer = readFileSync(getImageOriginalDirPath(IMAGE_V14_1)); // console.log(IMAGE_V14_1); // console.log(JSON.stringify(common.parseImageHeader(firmwareBuffer))); // console.log(`URL: ${common.getRepoFirmwareFileUrl(IMAGES_TEST_DIR, IMAGE_V14_1, common.BASE_IMAGES_DIR)}`); // console.log(`SHA512: ${common.computeSHA512(firmwareBuffer)}`); // }) it("hard failure from outside PR context", async () => { filePaths = [useImage(IMAGE_V14_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, {payload: {}}); }).rejects.toThrow("Not a pull request"); expectNoChanges(true); }); it("hard failure from merged PR context", async () => { filePaths = [useImage(IMAGE_V14_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, {payload: {pull_request: {merged: true}}}); }).rejects.toThrow("Should not be executed on a merged pull request"); expectNoChanges(true); }); it("hard failure with no file changed", async () => { filePaths = []; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow("No file"); expectNoChanges(false); expect(existsSync(common.PR_ARTIFACT_NUMBER_FILEPATH)).toStrictEqual(true); expect(readFileSync(common.PR_ARTIFACT_NUMBER_FILEPATH, "utf8")).toStrictEqual(`${context.payload?.pull_request?.number}`); expect(existsSync(common.PR_ARTIFACT_DIFF_FILEPATH)).toStrictEqual(false); expect(existsSync(common.PR_ARTIFACT_ERROR_FILEPATH)).toStrictEqual(true); expect(readFileSync(common.PR_ARTIFACT_ERROR_FILEPATH, "utf8")).toStrictEqual("No file"); }); it("failure with file outside of images directory", async () => { filePaths = [useImage(IMAGE_V13_1, PREV_IMAGES_TEST_DIR_PATH), useImage(IMAGE_V14_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining("Detected changes in files outside")})); expectNoChanges(false); }); it("failure when no manufacturer subfolder", async () => { filePaths = [useImage(IMAGE_V14_1, common.BASE_IMAGES_DIR)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining("File should be in its associated manufacturer subfolder")})); expectNoChanges(false); rmSync(path.join(common.BASE_IMAGES_DIR, IMAGE_V14_1), {force: true}); }); it("failure with invalid OTA file", async () => { filePaths = [useImage(IMAGE_INVALID)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining("Not a valid OTA file")})); expectNoChanges(false); }); it("failure with identical OTA file", async () => { setManifest(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); filePaths = [useImage(IMAGE_V14_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining("Conflict with image at index `0`")})); expectNoChanges(false); }); it("failure with older OTA file that has identical in prev", async () => { setManifest(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); setManifest(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); filePaths = [useImage(IMAGE_V13_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow( expect.objectContaining({message: expect.stringContaining("an equal or better match is already present in prev manifest")}), ); expectNoChanges(false); }); it("failure with older OTA file that has newer in prev", async () => { setManifest(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); setManifest(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); filePaths = [useImage(IMAGE_V12_1)]; await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, context); }).rejects.toThrow( expect.objectContaining({message: expect.stringContaining("an equal or better match is already present in prev manifest")}), ); expectNoChanges(false); }); it("success into base", async () => { filePaths = [useImage(IMAGE_V14_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(1); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); expect(existsSync(common.PR_ARTIFACT_NUMBER_FILEPATH)).toStrictEqual(true); expect(readFileSync(common.PR_ARTIFACT_NUMBER_FILEPATH, "utf8")).toStrictEqual(`${context.payload?.pull_request?.number}`); expect(existsSync(common.PR_ARTIFACT_DIFF_FILEPATH)).toStrictEqual(true); expect(existsSync(common.PR_ARTIFACT_ERROR_FILEPATH)).toStrictEqual(false); }); it("success into prev", async () => { setManifest(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); filePaths = [useImage(IMAGE_V13_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(0); expect(addImageToPrevSpy).toHaveBeenCalledTimes(1); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); }); it("success with newer than current without existing prev", async () => { filePaths = [useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); // adds both, relocates first during second processing expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); }); it("success with newer than current with existing prev", async () => { filePaths = [useImage(IMAGE_V12_1), useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(3); // adds both, relocates first during second processing expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); }); it("success with older that is newer than prev", async () => { setManifest(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V12_1_METAS]); filePaths = [useImage(IMAGE_V14_1), useImage(IMAGE_V13_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(1); expect(addImageToPrevSpy).toHaveBeenCalledTimes(1); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); }); it("success with newer with missing file", async () => { setManifest(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V13_1_METAS]); filePaths = [useImage(IMAGE_V14_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(1); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_1_METAS]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, []); }); it("success with multiple different files", async () => { filePaths = [useImage(IMAGE_V14_2), useImage(IMAGE_V14_1)]; // @ts-expect-error mock await checkOtaPR(github, core, context); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); // adds both, relocates first during second processing expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [IMAGE_V14_2_METAS, IMAGE_V14_1_METAS]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, []); }); it("success with extra metas", async () => { filePaths = [useImage(IMAGE_V14_1)]; const newContext = withBody(`Text before start tag \`\`\`json {"manufacturerName": ["lixee"]} \`\`\` Text after end tag`); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(1); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V14_1_METAS, {manufacturerName: ["lixee"]}), ]); }); it("success with all extra metas", async () => { filePaths = [useImage(IMAGE_V14_1)]; const newContext = withBody(`Text before start tag \`\`\`json { "force": false, "hardwareVersionMax": 2, "hardwareVersionMin": 1, "manufacturerName": ["lixee"], "maxFileVersion": 5, "minFileVersion": 3, "modelId": "bogus", "releaseNotes": "bugfixes" } \`\`\` Text after end tag`); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(1); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V14_1_METAS, { force: false, hardwareVersionMax: 2, hardwareVersionMin: 1, manufacturerName: ["lixee"], maxFileVersion: 5, minFileVersion: 3, modelId: "bogus", releaseNotes: "bugfixes", }), ]); }); it("success without extra metas with matching type-manuf present", async () => { filePaths = [useImage(IMAGE_V14_2_COPY), useImage(IMAGE_V14_2)]; const newContext = withBody(`\`\`\`json [{"fileName": "${IMAGE_V14_2_COPY}", "manufacturerName": ["lixee"]}] \`\`\``); // manip SHA512 so it doesn't match on that point in `hasManufacturerImage` const f = readFileSync(filePaths[0].filename); f[f.byteLength - 1] = 0xff; writeFileSync(filePaths[0].filename, f); // @ts-expect-error mock await checkOtaPR(github, core, newContext); const metaCopy = { ...IMAGE_V14_2_MANUF_METAS, sha512: "071be434a1c4ef95da68bfcfc0fdff9fb23729b16df50c4d6b70612414e39caacb1fd856c3849df6e99509a15ef8088cfdcd74c7f1f44d2048bac8fb5421ee64", }; expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [metaCopy, IMAGE_V14_2_METAS]); }); it("failure with SHA-matching image present", async () => { filePaths = [useImage(IMAGE_V14_2_COPY), useImage(IMAGE_V14_2)]; const newContext = withBody(`\`\`\`json [{"fileName": "${IMAGE_V14_2_COPY}", "manufacturerName": ["lixee"]}] \`\`\``); await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, newContext); }).rejects.toThrow(`[${path.join(BASE_IMAGES_TEST_DIR_PATH, IMAGE_V14_2)}] Image already present for manufacturer`); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(0); expect(baseManifest).toStrictEqual([IMAGE_V14_2_MANUF_METAS]); }); it("failure with spec-matching image present", async () => { filePaths = [useImage(IMAGE_V14_2_COPY), useImage(IMAGE_V14_2)]; // bypass initial "conflict" match by using random `minFileVersion` const newContext = withBody(`\`\`\`json [{"fileName": "${IMAGE_V14_2_COPY}", "minFileVersion": 1}] \`\`\``); // manip SHA512 so it doesn't match on that point in `hasManufacturerImage` const f = readFileSync(filePaths[0].filename); f[f.byteLength - 1] = 0xff; writeFileSync(filePaths[0].filename, f); await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, newContext); }).rejects.toThrow(`[${path.join(BASE_IMAGES_TEST_DIR_PATH, IMAGE_V14_2)}] Image already present for manufacturer`); const metaCopy = { ...IMAGE_V14_2_METAS, sha512: "071be434a1c4ef95da68bfcfc0fdff9fb23729b16df50c4d6b70612414e39caacb1fd856c3849df6e99509a15ef8088cfdcd74c7f1f44d2048bac8fb5421ee64", fileName: IMAGE_V14_2_COPY, minFileVersion: 1, url: IMAGE_V14_2_METAS.url.replace(IMAGE_V14_2, IMAGE_V14_2_COPY), }; expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(0); expect(baseManifest).toStrictEqual([metaCopy]); }); it("success with newer than current but minFileVersion keeps both", async () => { filePaths = [useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; const newContext = withBody( `Text before start tag \`\`\`json [{"fileName":"ZLinky_router_v14.ota", "minFileVersion": 16783874}] \`\`\` Text after end tag`, ); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V13_1_METAS, { // @ts-expect-error override url: `${common.BASE_REPO_URL}${common.REPO_BRANCH}/${common.BASE_IMAGES_DIR}/${IMAGES_TEST_DIR}/${IMAGE_V13_1}`, }), withExtraMetas(IMAGE_V14_1_METAS, {minFileVersion: 16783874}), ]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, []); }); it("success with newer than current but maxFileVersion keeps both", async () => { filePaths = [useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; const newContext = withBody( `Text before start tag \`\`\`json [{"fileName":"ZLinky_router_v13.ota", "maxFileVersion": 16783873}] \`\`\` Text after end tag`, ); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V13_1_METAS, { // @ts-expect-error override url: `${common.BASE_REPO_URL}${common.REPO_BRANCH}/${common.BASE_IMAGES_DIR}/${IMAGES_TEST_DIR}/${IMAGE_V13_1}`, maxFileVersion: 16783873, }), IMAGE_V14_1_METAS, ]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, []); }); it("success with newer than current but maxFileVersion/minFileVersion keeps both", async () => { filePaths = [useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; const newContext = withBody( `Text before start tag \`\`\`json [{"fileName":"ZLinky_router_v13.ota", "maxFileVersion": 16783873},{"fileName":"ZLinky_router_v14.ota", "minFileVersion": 16783874}] \`\`\` Text after end tag`, ); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledTimes(2); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V13_1_METAS, { // @ts-expect-error override url: `${common.BASE_REPO_URL}${common.REPO_BRANCH}/${common.BASE_IMAGES_DIR}/${IMAGES_TEST_DIR}/${IMAGE_V13_1}`, maxFileVersion: 16783873, }), withExtraMetas(IMAGE_V14_1_METAS, {minFileVersion: 16783874}), ]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, []); }); it("failure with invalid extra metas", async () => { filePaths = [useImage(IMAGE_V14_1)]; const newContext = withBody(`Text before start tag \`\`\`json {"manufacturerName": "myManuf"} \`\`\` Text after end tag`); await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, newContext); }).rejects.toThrow( expect.objectContaining({message: expect.stringContaining(`Invalid format for 'manufacturerName', expected 'array of string' type.`)}), ); expectNoChanges(false); }); it.each([ ["fileName"], ["originalUrl"], ["force"], ["hardwareVersionMax"], ["hardwareVersionMin"], ["manufacturerName"], ["maxFileVersion"], ["minFileVersion"], ["modelId"], ["releaseNotes"], ])("failure with invalid type for extra meta %s", async (metaName) => { filePaths = [useImage(IMAGE_V14_1)]; // use object since no value type is ever expected to be object const newContext = withBody(`Text before start tag \`\`\`json {"${metaName}": {}} \`\`\` Text after end tag`); await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, newContext); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining(`Invalid format for '${metaName}'`)})); expectNoChanges(false); }); it("success with multiple files and specific extra metas", async () => { filePaths = [useImage(IMAGE_V13_1), useImage(IMAGE_V14_1), useImage(IMAGE_V12_1)]; const newContext = withBody(`Text before start tag \`\`\`json [ {"fileName": "${IMAGE_V14_1}", "manufacturerName": ["lixee"], "hardwareVersionMin": 2}, {"fileName": "${IMAGE_V13_1}", "manufacturerName": ["lixee"]}, {"fileName": "${IMAGE_V12_1}", "manufacturerName": ["lixee"]} ] \`\`\` Text after end tag`); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(2); expect(addImageToPrevSpy).toHaveBeenCalledTimes(1); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V13_1_METAS_MAIN, {manufacturerName: ["lixee"]}), withExtraMetas(IMAGE_V14_1_METAS, {manufacturerName: ["lixee"], hardwareVersionMin: 2}), ]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [ withExtraMetas(IMAGE_V12_1_METAS, {manufacturerName: ["lixee"]}), ]); }); it("success with multiple files and specific extra metas, ignore without fileName", async () => { filePaths = [useImage(IMAGE_V12_1), useImage(IMAGE_V13_1), useImage(IMAGE_V14_1)]; const newContext = withBody(`Text before start tag \`\`\`json [ {"fileName": "${IMAGE_V14_1}", "manufacturerName": ["lixee"], "hardwareVersionMin": 2}, {"manufacturerName": ["lixee"]} ] \`\`\` Text after end tag`); // @ts-expect-error mock await checkOtaPR(github, core, newContext); expect(readManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME); expect(readManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME); expect(addImageToBaseSpy).toHaveBeenCalledTimes(3); expect(addImageToPrevSpy).toHaveBeenCalledTimes(0); expect(writeManifestSpy).toHaveBeenCalledTimes(2); expect(writeManifestSpy).toHaveBeenCalledWith(common.BASE_INDEX_MANIFEST_FILENAME, [ IMAGE_V13_1_METAS_MAIN, withExtraMetas(IMAGE_V14_1_METAS, {manufacturerName: ["lixee"], hardwareVersionMin: 2}), ]); expect(writeManifestSpy).toHaveBeenCalledWith(common.PREV_INDEX_MANIFEST_FILENAME, [IMAGE_V12_1_METAS]); }); it.each([ ['"manufacturerName": ["_TZ3000"]', IMAGE_TUYA, undefined], ["", IMAGE_TUYA, "Tuya image requires extra `manufacturerName` metadata"], ['"modelId": "GL-C-008"', IMAGE_GLEDOPTO, undefined], ["", IMAGE_GLEDOPTO, "Gledopto image requires extra `modelId` metadata"], ['"modelId": "lumi.switch.n1aeu1"', IMAGE_LUMI, undefined], ["", IMAGE_LUMI, "Lumi/Aqara image requires extra `modelId` metadata"], ['"manufacturerName": ["GleDopto"]', IMAGE_GLEDOPTO, "Gledopto image requires extra `modelId` metadata"], ['"manufacturerName": ["OtherManuf"]', IMAGE_GLEDOPTO, undefined], ])("manufacturer specific checks", async (body: string, image: string, error: string | undefined) => { filePaths = [useImage(image)]; const newContext = withBody(`\`\`\`json { ${body} } \`\`\``); if (error === undefined) { // @ts-expect-error mock await checkOtaPR(github, core, newContext); } else { await expect(async () => { // @ts-expect-error mock await checkOtaPR(github, core, newContext); }).rejects.toThrow(expect.objectContaining({message: expect.stringContaining(error)})); } }); });