Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/skus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,17 @@ export const SKUS: Record<string, SkuArtifacts> = {
// 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",
},
};

Expand Down
67 changes: 61 additions & 6 deletions test/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
});

Expand Down
9 changes: 6 additions & 3 deletions test/skus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
Expand Down