Files
pleno-vue/tests/unit/release-channel-availability.spec.js

375 lines
12 KiB
JavaScript

// @vitest-environment jsdom
import { afterEach, describe, expect, it } from "vitest";
import {
acknowledgeReleaseChannelSwitch,
buildReleaseFrontendRedirectUrl,
getReleaseChannelSwitchedStatus,
getReleaseChannelOptions,
getReleaseChannelUnavailableStatus,
hasSelectableReleaseChannels,
ignoreUnavailableReleaseChannel,
isReleaseChannelApiAvailabilityError,
markReleaseChannelApiUnavailable,
reconcileSelectedReleaseChannel,
RELEASE_CHANNEL_IGNORE_MS,
RELEASE_CHANNEL_IGNORE_STORAGE_KEY,
RELEASE_CHANNEL_SELECTION_STORAGE_KEY,
RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY,
releaseChannelRuntimeRequestParams,
selectReleaseChannel,
switchSelectedReleaseChannel,
__resetReleaseChannelAvailabilityForTests,
} from "@/services/releaseChannelAvailability.js";
import {
__resetReleaseTimelineForTests,
configureReleaseRuntime,
releaseRuntimeState,
} from "@/services/releaseTimeline.js";
const canaryRuntime = {
channel: {
id: 2,
slug: "canary",
name: "Canary",
description: "Early validation channel",
default_channel: false,
},
versions: {
frontend: null,
api: null,
bundle_id: null,
},
availability: {
configured: false,
missing: ["release_bundle", "frontend_version", "api_version"],
status: "unconfigured",
},
};
const configuredCanaryRuntime = {
...canaryRuntime,
versions: {
frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" },
api: { version_label: "api-canary", commit_sha: "feedface" },
},
availability: {
configured: true,
missing: [],
status: "ready",
},
};
describe("release channel availability", () => {
afterEach(() => {
__resetReleaseChannelAvailabilityForTests();
__resetReleaseTimelineForTests();
});
it("blocks a non-default assigned channel when API targets are missing", () => {
const status = getReleaseChannelUnavailableStatus(canaryRuntime, 1_000, 0);
expect(status.shouldBlock).toBe(true);
expect(status.channelSlug).toBe("canary");
expect(status.missing).toEqual(["api_version"]);
});
it("does not block non-default channels only because frontend release assets are missing", () => {
const status = getReleaseChannelUnavailableStatus(
{
...canaryRuntime,
versions: {
frontend: null,
api: { version_label: "api-canary", commit_sha: "feedface" },
},
availability: {
configured: false,
missing: ["release_bundle", "frontend_version", "frontend_base_url", "frontend_entry"],
status: "unconfigured",
},
},
1_000,
0
);
expect(status.configured).toBe(true);
expect(status.shouldBlock).toBe(false);
expect(status.missing).toEqual([]);
});
it("does not block the stable default channel even when runtime targets are absent", () => {
const status = getReleaseChannelUnavailableStatus({
channel: {
slug: "stable",
name: "Stable",
default_channel: true,
},
availability: {
configured: false,
missing: ["release_bundle"],
},
});
expect(status.unavailable).toBe(false);
expect(status.shouldBlock).toBe(false);
});
it("does not block branch-based channels only because a legacy bundle is missing", () => {
const status = getReleaseChannelUnavailableStatus({
channel: {
slug: "canary",
name: "Canary",
default_channel: false,
},
availability: {
configured: false,
missing: ["release_bundle"],
status: "unconfigured",
},
});
expect(status.configured).toBe(true);
expect(status.shouldBlock).toBe(false);
expect(status.missing).toEqual([]);
expect(status.status).toBe("ready");
});
it("exposes selectable runtime channels when a non-default assignment is available", () => {
const runtime = {
channel: configuredCanaryRuntime.channel,
versions: configuredCanaryRuntime.versions,
availability: configuredCanaryRuntime.availability,
availableChannels: [
{
channel: {
slug: "stable",
name: "Stable",
default_channel: true,
},
availability: {
configured: true,
missing: [],
status: "ready",
},
},
{
channel: configuredCanaryRuntime.channel,
versions: configuredCanaryRuntime.versions,
availability: configuredCanaryRuntime.availability,
},
],
};
const options = getReleaseChannelOptions(runtime);
expect(options.map((option) => option.channelSlug)).toEqual(["stable", "canary"]);
expect(hasSelectableReleaseChannels(runtime)).toBe(true);
});
it("hides the selector when only the default stable channel is available", () => {
expect(
hasSelectableReleaseChannels({
channel: {
slug: "stable",
name: "Stable",
default_channel: true,
},
availableChannels: [
{
channel: {
slug: "stable",
name: "Stable",
default_channel: true,
},
},
],
})
).toBe(false);
});
it("stores the selected release channel for runtime refresh requests", () => {
const selectedSlug = selectReleaseChannel({ slug: "canary" });
expect(selectedSlug).toBe("canary");
expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBe("canary");
expect(releaseChannelRuntimeRequestParams()).toEqual({ release_channel: "canary" });
});
it("rolls back a selected channel when the runtime refresh fails", async () => {
let refreshCalls = 0;
await expect(
switchSelectedReleaseChannel({ slug: "canary" }, async () => {
refreshCalls += 1;
if (refreshCalls > 1) {
return { channel: { slug: "stable", name: "Stable", default_channel: true } };
}
throw new Error("runtime unavailable");
})
).rejects.toThrow("runtime unavailable");
expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull();
expect(releaseChannelRuntimeRequestParams()).toEqual({});
expect(refreshCalls).toBe(2);
});
it("rejects and rolls back when the refreshed runtime confirms a different channel", async () => {
configureReleaseRuntime({
channel: { slug: "stable", name: "Stable", default_channel: true },
availableChannels: [
{ channel: { slug: "stable", name: "Stable", default_channel: true } },
{ channel: configuredCanaryRuntime.channel },
],
});
await expect(
switchSelectedReleaseChannel({ slug: "canary" }, async () => ({
channel: { slug: "stable", name: "Stable", default_channel: true },
}))
).rejects.toThrow("instead of canary");
expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull();
});
it("keeps a selected channel only after the refreshed runtime confirms it", async () => {
const selectedSlug = await switchSelectedReleaseChannel({ slug: "canary" }, async () => configuredCanaryRuntime);
expect(selectedSlug).toBe("canary");
expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBe("canary");
});
it("clears a stored selection when the runtime no longer offers that channel", () => {
selectReleaseChannel("canary");
const selectedSlug = reconcileSelectedReleaseChannel({
availableChannels: [
{
channel: {
slug: "stable",
name: "Stable",
default_channel: true,
},
},
],
});
expect(selectedSlug).toBe("");
expect(window.localStorage.getItem(RELEASE_CHANNEL_SELECTION_STORAGE_KEY)).toBeNull();
});
it("does not block legacy runtime payloads that omit availability and target URLs", () => {
const runtime = {
channel: {
slug: "canary",
name: "Canary",
default_channel: false,
},
};
const status = getReleaseChannelUnavailableStatus(runtime);
const switchedStatus = getReleaseChannelSwitchedStatus(runtime, "user:42", {});
expect(status.configured).toBe(true);
expect(status.shouldBlock).toBe(false);
expect(switchedStatus.shouldShow).toBe(false);
});
it("supports a five-minute channel-specific ignore window", () => {
const ignoredUntil = ignoreUnavailableReleaseChannel(canaryRuntime.channel);
const stored = JSON.parse(window.localStorage.getItem(RELEASE_CHANNEL_IGNORE_STORAGE_KEY));
const status = getReleaseChannelUnavailableStatus(canaryRuntime, Date.now(), ignoredUntil);
expect(ignoredUntil).toBeGreaterThan(Date.now() + RELEASE_CHANNEL_IGNORE_MS - 2_000);
expect(stored.canary).toBe(ignoredUntil);
expect(status.ignored).toBe(true);
expect(status.shouldBlock).toBe(false);
});
it("shows a one-time switched-channel notice for configured non-default channels", () => {
const status = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:42", {});
expect(status.shouldShow).toBe(true);
expect(status.noticeKey).toBe("user:42:canary");
});
it("stores switched-channel acknowledgement per account and channel", () => {
const noticeKey = acknowledgeReleaseChannelSwitch(configuredCanaryRuntime.channel, "user:42");
const stored = JSON.parse(window.localStorage.getItem(RELEASE_CHANNEL_SWITCH_NOTICE_STORAGE_KEY));
const status = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:42", stored);
const otherUserStatus = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "user:99", stored);
expect(noticeKey).toBe("user:42:canary");
expect(stored["user:42:canary"]).toEqual(expect.any(String));
expect(status.shouldShow).toBe(false);
expect(otherUserStatus.shouldShow).toBe(true);
});
it("does not show the switched-channel notice for default channels or unknown accounts", () => {
const defaultStatus = getReleaseChannelSwitchedStatus(
{
...configuredCanaryRuntime,
channel: {
...configuredCanaryRuntime.channel,
default_channel: true,
},
},
"user:42",
{}
);
const anonymousStatus = getReleaseChannelSwitchedStatus(configuredCanaryRuntime, "", {});
expect(defaultStatus.shouldShow).toBe(false);
expect(anonymousStatus.shouldShow).toBe(false);
});
it("does not build a channel-specific frontend redirect", () => {
const redirectUrl = buildReleaseFrontendRedirectUrl(
{
channel: { slug: "beta", default_channel: false },
versions: {
frontend: { version_label: "frontend-beta" },
api: { version_label: "api-beta" },
bundle_id: 52,
},
},
{
origin: "https://app.example.test",
pathname: "/user/bookings",
search: "?page=2",
hash: "#next",
}
);
expect(redirectUrl).toBeNull();
});
it("classifies selected channel auth session 404s as release API availability failures", () => {
configureReleaseRuntime({
trace_id: "trace-channel-api",
channel: { slug: "internal", name: "Intern", default_channel: false },
versions: {
frontend: { version_label: "frontend-internal" },
api: { version_label: "api-internal" },
bundle_id: 42,
},
api_base_url: "https://api-v2.truckwash.io/internal/api",
frontend_base_url: "https://api-v2.truckwash.io/internal/frontend",
});
const error = {
response: { status: 404 },
config: { url: "https://api-v2.truckwash.io/internal/api/auth/session" },
};
expect(isReleaseChannelApiAvailabilityError(error)).toBe(true);
const unavailableRuntime = markReleaseChannelApiUnavailable();
expect(unavailableRuntime.availability).toMatchObject({
configured: false,
status: "unconfigured",
explicit: true,
});
expect(unavailableRuntime.availability.missing).toContain("api_base_url");
expect(releaseRuntimeState.availability.configured).toBe(false);
});
});