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
18 changes: 13 additions & 5 deletions src/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,16 +471,18 @@ function dbReleaseToMetadata(release: DbRelease, sku: string): ReleaseMetadata {
};
}

async function getDefaultRelease(prefix: string, sku: string): Promise<DbRelease> {
/**
* Newest fully rolled out release for the prefix, or null when no release
* has reached 100% yet (a prefix whose first release is still staged).
*/
async function getDefaultRelease(prefix: string, sku: string): Promise<DbRelease | null> {
const rolledOutReleases = await prisma.release.findMany({
where: { type: prefix, rolloutPercentage: 100 },
select: compatibleReleaseSelect(sku),
});

if (rolledOutReleases.length === 0) {
throw new InternalServerError(
`No default release found for type ${prefix} and SKU "${sku}"`,
);
return null;
}

// Only consider releases that ship a binary for this SKU. Without this,
Expand Down Expand Up @@ -605,7 +607,13 @@ export async function Retrieve(req: Request, res: Response) {
latest.artifacts.length > 0 &&
(await isDeviceEligibleForLatestRelease(latest.rolloutPercentage, deviceId));

offered[kind] = dbReleaseToMetadata(useLatest ? latest : fallback, sku);
const chosen = useLatest ? latest : fallback;
if (!chosen) {
throw new NotFoundError(
`No ${prefix} release is rolled out yet for SKU "${sku}"`,
);
}
offered[kind] = dbReleaseToMetadata(chosen, sku);
}),
);

Expand Down
37 changes: 31 additions & 6 deletions test/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,15 +587,26 @@ describe("Retrieve handler", () => {
expect(s3Mock.commandCalls(GetObjectCommand)).toHaveLength(0);
});

it("fails when no fully rolled out default exists for background checks", async () => {
it("serves a staged release to an in-bucket device even when nothing is at 100%", async () => {
// early-adopter hashes to rollout bucket 8, inside a 50% rollout.
await testPrisma.release.updateMany({ data: { rolloutPercentage: 50 } });

const res = createMockResponse();
await Retrieve(createMockRequest({ deviceId: "early-adopter" }), res);

expect(jsonBody(res)).toMatchObject({ appVersion: "1.2.0", systemVersion: "1.2.0" });
});

it("answers 404, not 500, to an out-of-bucket device when nothing is at 100%", async () => {
// late-adopter hashes to rollout bucket 95, outside a 50% rollout.
await testPrisma.release.updateMany({ data: { rolloutPercentage: 50 } });

await expect(
Retrieve(
createMockRequest({ deviceId: "no-default-device" }),
createMockResponse(),
),
).rejects.toThrow(InternalServerError);
Retrieve(createMockRequest({ deviceId: "late-adopter" }), createMockResponse()),
).rejects.toThrow(NotFoundError);
await expect(
Retrieve(createMockRequest({ deviceId: "late-adopter" }), createMockResponse()),
).rejects.toThrow(/No (app|system) release is rolled out yet for SKU "jetkvm-v2"/);
});
});

Expand Down Expand Up @@ -789,6 +800,20 @@ describe("Retrieve handler", () => {
).rejects.toThrow('Version 1.0.0 has no artifact for SKU "jetkvm-mini-ethernet"');
});

it("serves the first staged mini release to an in-bucket device and 404s the rest", async () => {
// No mini row at 100% yet: the first release is at 10%.
// early-adopter hashes to bucket 8, late-adopter to bucket 95.
await createDbRelease("mini", "1.0.0", 10, miniArtifacts("1.0.0"));

const inBucket = createMockResponse();
await Retrieve(createMockRequest({ deviceId: "early-adopter", sku: MINI_ETHERNET_SKU }), inBucket);
expect(jsonBody(inBucket)).toMatchObject({ systemVersion: "1.0.0" });

await expect(
Retrieve(createMockRequest({ deviceId: "late-adopter", sku: MINI_ETHERNET_SKU }), createMockResponse()),
).rejects.toThrow('No mini release is rolled out yet for SKU "jetkvm-mini-ethernet"');
});

it("fails when no mini release exists for the SKU", async () => {
const res = createMockResponse();
await expect(
Expand Down
Loading