diff --git a/src/skus.ts b/src/skus.ts index 1f8c494..82dcfff 100644 --- a/src/skus.ts +++ b/src/skus.ts @@ -50,12 +50,17 @@ export const SKUS: Record = { // JetKVM Mini (ESP32-P4). One firmware image is the whole system: // FreeRTOS, drivers, the application, the web UI and the ESP32-C5 Wi-Fi // co-processor firmware. There is no separate app. The Ethernet (IP101) - // and wireless (ESP32-C5 over SDIO) boards are separate builds. + // and wireless (ESP32-C5 over SDIO) boards are separate builds. The + // recovery image is the merged full-flash binary (bootloader, partition + // table, firmware in the first OTA slot, erased OTA selection data), + // written with esptool or a browser flasher at offset 0. "jetkvm-mini-ethernet": { system: "mini/jetkvm-mini.bin", + recovery: "mini/jetkvm-mini-full.bin", }, "jetkvm-mini-wireless": { system: "mini/jetkvm-mini.bin", + recovery: "mini/jetkvm-mini-full.bin", }, }; diff --git a/test/releases.test.ts b/test/releases.test.ts index c3c7174..2294de9 100644 --- a/test/releases.test.ts +++ b/test/releases.test.ts @@ -1460,15 +1460,70 @@ describe("RetrieveLatestSystemRecovery S3 redirect handler", () => { ); }); - it("should throw BadRequestError for a SKU without a recovery image", async () => { - const req = createMockRequest({ sku: MINI_ETHERNET_SKU }); + it("redirects a mini SKU to the full-flash image under the mini prefix", async () => { + s3Mock.on(ListObjectsV2Command, { Prefix: "mini/" }).resolves({ + CommonPrefixes: [{ Prefix: "mini/1.0.0/" }, { Prefix: "mini/1.1.0/" }], + }); + + const content = "mini-full-flash"; + const crypto = await import("crypto"); + const hash = crypto.createHash("sha256").update(content).digest("hex"); + mockS3SkuVersionWithContent( + "mini", + "1.1.0", + MINI_WIRELESS_SKU, + "jetkvm-mini-full.bin", + content, + hash, + ); + const res = createMockResponse(); + await RetrieveLatestSystemRecovery(createMockRequest({ sku: MINI_WIRELESS_SKU }), res); - await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow( - BadRequestError, + expect(res.redirect).toHaveBeenCalledWith( + 302, + `https://cdn.test.com/mini/1.1.0/skus/${MINI_WIRELESS_SKU}/jetkvm-mini-full.bin`, ); - await expect(RetrieveLatestSystemRecovery(req, res)).rejects.toThrow( - 'SKU "jetkvm-mini-ethernet" has no downloadable recovery image', + expect(s3Mock.commandCalls(ListObjectsV2Command, { Prefix: "system/" }).length).toBe(0); + }); + + it("keeps separate recovery cache entries for JetKVM and mini SKUs", async () => { + s3Mock.on(ListObjectsV2Command, { Prefix: "system/" }).resolves({ + CommonPrefixes: [{ Prefix: "system/2.0.0/" }], + }); + s3Mock.on(ListObjectsV2Command, { Prefix: "mini/" }).resolves({ + CommonPrefixes: [{ Prefix: "mini/1.0.0/" }], + }); + const crypto = await import("crypto"); + const systemContent = "system-recovery"; + const miniContent = "mini-full-flash"; + mockS3SkuVersionWithContent( + "system", + "2.0.0", + DEFAULT_SKU, + "update.img", + systemContent, + crypto.createHash("sha256").update(systemContent).digest("hex"), + ); + mockS3SkuVersionWithContent( + "mini", + "1.0.0", + MINI_ETHERNET_SKU, + "jetkvm-mini-full.bin", + miniContent, + crypto.createHash("sha256").update(miniContent).digest("hex"), + ); + + const systemRes = createMockResponse(); + await RetrieveLatestSystemRecovery(createMockRequest({ sku: DEFAULT_SKU }), systemRes); + const miniRes = createMockResponse(); + await RetrieveLatestSystemRecovery(createMockRequest({ sku: MINI_ETHERNET_SKU }), miniRes); + + expect(systemRes._redirectUrl).toBe( + "https://cdn.test.com/system/2.0.0/skus/jetkvm-v2/update.img", + ); + expect(miniRes._redirectUrl).toBe( + `https://cdn.test.com/mini/1.0.0/skus/${MINI_ETHERNET_SKU}/jetkvm-mini-full.bin`, ); }); diff --git a/test/skus.test.ts b/test/skus.test.ts index e61f208..67295cc 100644 --- a/test/skus.test.ts +++ b/test/skus.test.ts @@ -39,7 +39,7 @@ describe("SKU table", () => { } }); - it("keeps the JetKVM recovery image with the system artifact and gives the Mini none", () => { + it("stores each product's recovery image next to what it is built with", () => { expect(artifactFor("jetkvm-v2", "recovery")).toEqual({ prefix: "system", file: "update.img", @@ -49,11 +49,14 @@ describe("SKU table", () => { file: "update_sd.img.zip", }); for (const sku of MINI_SKUS) { - expect(artifactFor(sku, "recovery")).toBeUndefined(); + expect(artifactFor(sku, "recovery")).toEqual({ + prefix: "mini", + file: "jetkvm-mini-full.bin", + }); } }); - it("syncs three prefixes, each holding one over-the-air file", () => { + it("syncs three prefixes, each holding one over-the-air file (recovery is not synced)", () => { expect(OTA_PREFIXES).toEqual(["app", "system", "mini"]); expect(otaFileForPrefix("app")).toBe("jetkvm_app"); expect(otaFileForPrefix("system")).toBe("system.tar");