Files
pleno-vue/tests/unit/release-timeline.spec.js
T
Jeppe B 5ffd471a45 Exchange one-time login grants in QR flow (#235)
## Summary

Updates the QR login view to consume the short-lived, one-time employee
login grants created by approved Pleno Control Plane
Conversations/Suggestions actions.

- reads generated grants from the URL fragment
- scrubs the bearer from the address bar before exchange
- exchanges the grant for a normal session token, then uses the existing
secure session-storage path
- preserves legacy token QR links
- validates exact URL origin and removes raw credential/QR logging
- prevents repeated scanner exchange attempts while one is in progress

## Visual change previews

No layout or styling changes. The visible flow changes only after
opening or scanning a grant:

- Before: one-time grant links were rejected as unknown QR content.
- After: the existing loader appears during exchange; invalid/expired
grants use the existing localized error dialog; successful grants
redirect through the existing login path.

## Verification

- focused Vitest: 2 passed
- focused ESLint: passed
- production Vite build: passed (existing chunk-size warning only)
- `git diff --check`: passed

## Dependency

Pair with copenhagentruckwash/api (one-time limited-backoffice login
grants) and merge after that backend PR. Required by
copenhagentruckwash/pleno-control-plane#1.
2026-07-29 00:01:22 +02:00

491 lines
18 KiB
JavaScript

// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
__resetReleaseTimelineForTests,
__setReleaseTimelineTransportForTests,
buildReleaseSessionSummary,
buildCurrentReleaseHeaders,
buildReleaseTimelineContext,
configureReleaseRuntime,
flushReleaseTimelineEvents,
getReleaseRuntimeApiBaseUrl,
getRecentFrontendFailureEvents,
recordReleaseTimelineEvent,
redactReleasePayload,
releaseRuntimeState,
resolveReleaseApiUrl,
} from "@/services/releaseTimeline.js";
describe("release timeline runtime", () => {
beforeEach(() => {
vi.useFakeTimers();
localStorage.clear();
__resetReleaseTimelineForTests();
});
afterEach(() => {
__resetReleaseTimelineForTests();
localStorage.clear();
vi.useRealTimers();
vi.restoreAllMocks();
});
it("configures release runtime from the JSON session contract", () => {
configureReleaseRuntime({
source: "deployment",
trace_id: "trace-123",
channel: { slug: "canary", name: "Canary" },
available_channels: [{ channel: { slug: "stable", name: "Stable", default_channel: true } }],
versions: { frontend: { version_label: "abc123" }, api: null, bundle_id: 31 },
capture_policy: { enabled: true, capture_level: "full_redacted", retention_days: 7 },
});
expect(releaseRuntimeState.source).toBe("deployment");
expect(releaseRuntimeState.requestedSource).toBe("deployment");
expect(releaseRuntimeState.channel.slug).toBe("canary");
expect(releaseRuntimeState.availableChannels).toHaveLength(1);
expect(releaseRuntimeState.traceId).toBe("trace-123");
expect(releaseRuntimeState.versions.frontend.version_label).toBe("abc123");
expect(releaseRuntimeState.versions.bundle_id).toBe(31);
});
it("preserves the bootstrap source when later session runtime omits it", () => {
configureReleaseRuntime({
source: "local",
requested_source: "auto",
api_base_url: "/api",
});
configureReleaseRuntime({
trace_id: "trace-session",
channel: { slug: "stable", name: "Stable", default_channel: true },
availability: { configured: true, missing: [], status: "ready" },
});
expect(releaseRuntimeState.source).toBe("local");
expect(releaseRuntimeState.requestedSource).toBe("auto");
expect(releaseRuntimeState.traceId).toBe("trace-session");
});
it("builds display rows for the active release bundle and connected services", () => {
configureReleaseRuntime({
generated_at: "2026-05-19T09:30:00.000Z",
trace_id: "trace-session-release",
channel: { slug: "canary", name: "Canary", default_channel: false },
versions: {
frontend: {
version_label: "frontend-canary",
commit_sha: "c0ffee0000001111222233334444555566667777",
repository: "truckwash/front-end-vue",
branch: "release/canary",
status: "deployed",
},
api: {
version_label: "api-canary",
commit_sha: "feedface00001111222233334444555566667777",
repository: "truckwash/backend-php",
branch: "release/canary",
status: "deployed",
},
service_set: {
id: 53,
name: "Canary isolated stack",
stack: {
frontend: { id: 51, label: "canary-frontend", coolify_service_uuid: "frontend-service-uuid" },
api: { id: 52, label: "canary-api", coolify_service_uuid: "api-service-uuid" },
database: {
id: 54,
resource_name: "canary-db",
resource_uuid: "database-resource-uuid",
deployment_status: "ready",
},
redis: {
id: 55,
resource_name: "canary-redis",
resource_uuid: "redis-resource-uuid",
deployment_status: "ready",
},
minio: {
id: 56,
resource_name: "canary-minio",
resource_uuid: "minio-resource-uuid",
availability_state: "healthy",
},
},
},
bundle_id: 31,
bundle: { id: 31, version_label: "canary-bundle", status: "deployed" },
},
urls: {
frontend_base_url: "https://api-v2.truckwash.io/canary/frontend",
api_base_url: "https://api-v2.truckwash.io/canary/api",
},
availability: { configured: true, missing: [], status: "ready" },
});
const summary = buildReleaseSessionSummary(undefined, { includeInfrastructureDetails: true });
expect(releaseRuntimeState.versions.service_set.name).toBe("Canary isolated stack");
expect(summary.channelLabel).toBe("Canary");
expect(summary.traceId).toBe("trace-session-release");
expect(summary.bundleLabel).toBe("#31 canary-bundle");
expect(summary.serviceSetLabel).toBe("Canary isolated stack");
expect(summary.appRows.find((row) => row.key === "frontend")).toMatchObject({
primaryText: "frontend-canary",
url: "https://api-v2.truckwash.io/canary/frontend",
tone: "ok",
});
expect(summary.appRows.find((row) => row.key === "api")?.secondaryText).toContain("feedface0000");
expect(summary.serviceRows.find((row) => row.key === "database")).toMatchObject({
primaryText: "canary-db #54",
tone: "ok",
});
expect(summary.serviceRows.find((row) => row.key === "redis")?.secondaryText).toContain("redis-resour");
expect(summary.serviceRows.find((row) => row.key === "minio")?.status).toBe("healthy");
});
it("redacts service set infrastructure from release summaries by default", () => {
const summary = buildReleaseSessionSummary({
trace_id: "trace-redacted",
channel: { slug: "canary", name: "Canary", default_channel: false },
versions: {
frontend: {
version_label: "frontend-canary",
commit_sha: "c0ffee0000001111222233334444555566667777",
repository: "truckwash/front-end-vue",
branch: "release/canary",
status: "deployed",
},
api: { version_label: "api-canary", status: "deployed" },
service_set: {
id: 53,
name: "Canary isolated stack",
stack: {
database: {
id: 54,
resource_name: "canary-db",
resource_uuid: "database-resource-uuid",
coolify_service_uuid: "database-service-uuid",
health_url: "https://coolify.internal/health/db",
},
},
},
bundle_id: 31,
},
urls: {
frontend_base_url: "https://api-v2.truckwash.io/canary/frontend",
api_base_url: "https://api-v2.truckwash.io/canary/api",
},
availability: { configured: true, missing: [], status: "ready" },
});
expect(summary.serviceSetLabel).toBe("Restricted to release operators");
expect(summary.serviceRows).toEqual([]);
expect(summary.appRows.find((row) => row.key === "frontend")).toMatchObject({
secondaryText: "",
url: "",
});
expect(JSON.stringify(summary)).not.toContain("canary-db");
expect(JSON.stringify(summary)).not.toContain("database-resource-uuid");
expect(JSON.stringify(summary)).not.toContain("coolify.internal");
expect(JSON.stringify(summary)).not.toContain("truckwash/front-end-vue#release/canary");
});
it("marks stable sessions without release bundle data as shared runtime", () => {
const summary = buildReleaseSessionSummary({
trace_id: "trace-stable",
channel: { slug: "stable", name: "Stable", default_channel: true },
versions: { frontend: null, api: null, bundle_id: null, bundle: null },
availability: { configured: true, missing: [], status: "ready" },
});
expect(summary.bundleLabel).toBe("Default/shared runtime");
expect(summary.serviceSetLabel).toBe("Default/shared runtime");
expect(summary.appRows.every((row) => row.primaryText === "Default/shared runtime")).toBe(true);
expect(summary.appRows.every((row) => row.tone === "ok")).toBe(true);
expect(summary.serviceRows.every((row) => row.primaryText === "Default/shared runtime")).toBe(true);
expect(summary.serviceRows.every((row) => row.tone === "ok")).toBe(true);
});
it("surfaces non-default release runtime missing values as warnings", () => {
const summary = buildReleaseSessionSummary({
trace_id: "trace-internal",
channel: { slug: "internal", name: "Internal", default_channel: false },
versions: {
frontend: null,
api: { version_label: "api-internal" },
bundle_id: null,
bundle: null,
},
availability: {
configured: false,
missing: ["release_bundle", "frontend_version", "api_base_url"],
status: "unconfigured",
},
});
expect(summary.missingLabels).toEqual(["API URL"]);
expect(summary.appRows.find((row) => row.key === "frontend")).toMatchObject({
tone: "ok",
status: "regular",
primaryText: "Regular frontend",
missingLabel: "",
});
expect(summary.appRows.find((row) => row.key === "api")).toMatchObject({
tone: "warning",
missingLabel: "API URL",
});
expect(summary.serviceRows.find((row) => row.key === "database")).toMatchObject({
tone: "ok",
primaryText: "Default/shared runtime",
});
});
it("uses release runtime frontend and API URLs when provided", () => {
configureReleaseRuntime({
trace_id: "trace-urls",
channel: { slug: "canary", name: "Canary" },
versions: {
frontend: { version_label: "frontend-canary" },
api: { version_label: "api-canary" },
bundle_id: 31,
},
urls: {
frontend_base_url: "https://api-v2.truckwash.io/canary/frontend/",
api_base_url: "https://api-v2.truckwash.io/canary/api/",
},
});
expect(releaseRuntimeState.frontendBaseUrl).toBe("https://api-v2.truckwash.io/canary/frontend");
expect(releaseRuntimeState.apiBaseUrl).toBe("https://api-v2.truckwash.io/canary/api");
expect(getReleaseRuntimeApiBaseUrl()).toBe("https://api-v2.truckwash.io/canary/api");
expect(resolveReleaseApiUrl("/orders")).toBe("https://api-v2.truckwash.io/canary/api/orders");
});
it("ignores untrusted runtime frontend and API URLs", () => {
configureReleaseRuntime({
trace_id: "trace-untrusted",
channel: { slug: "canary", name: "Canary" },
versions: {
frontend: { version_label: "frontend-canary" },
api: { version_label: "api-canary" },
bundle_id: 31,
},
urls: {
frontend_base_url: "https://attacker.example/frontend",
api_base_url: "https://attacker.example/api",
},
});
expect(releaseRuntimeState.frontendBaseUrl).toBeNull();
expect(releaseRuntimeState.apiBaseUrl).toBeNull();
expect(resolveReleaseApiUrl("/orders")).not.toBe("https://attacker.example/api/orders");
});
it("keeps same-origin release API URLs visible in privileged session summaries", () => {
configureReleaseRuntime({
trace_id: "trace-local-api",
channel: { slug: "canary", name: "Canary" },
versions: {
frontend: { version_label: "frontend-canary" },
api: { version_label: "api-canary" },
bundle_id: 31,
},
urls: {
api_base_url: "/api/",
},
});
const summary = buildReleaseSessionSummary(undefined, { includeInfrastructureDetails: true });
expect(releaseRuntimeState.apiBaseUrl).toBe("/api");
expect(getReleaseRuntimeApiBaseUrl()).toBe("/api");
expect(resolveReleaseApiUrl("/orders")).toBe("/api/orders");
expect(summary.appRows.find((row) => row.key === "api")?.url).toBe("/api");
});
it("builds release headers from the active runtime state", () => {
configureReleaseRuntime({
trace_id: "trace-headers",
channel: { slug: "internal", name: "Intern" },
versions: {
frontend: { version_label: "frontend-internal", commit_sha: "130cc2fc106a" },
api: { version_label: "api-internal" },
bundle_id: 42,
},
});
expect(buildCurrentReleaseHeaders()).toMatchObject({
"X-Release-Trace": "trace-headers",
"X-Release-Channel": "internal",
"X-Frontend-Version": "130cc2fc106a",
});
});
it("redacts sensitive payload keys recursively", () => {
expect(
redactReleasePayload({
Authorization: "Bearer token",
nested: {
api_key: "secret",
grant: "lbg_sensitive",
safe: "visible",
},
})
).toEqual({
Authorization: "[redacted]",
nested: {
api_key: "[redacted]",
grant: "[redacted]",
safe: "visible",
},
});
});
it("redacts sensitive fields inside serialized request bodies", () => {
expect(
redactReleasePayload({
data: JSON.stringify({
customer_number: "cust-123",
password: "PlaintextP@ssw0rd!",
g_recaptcha_response: "recaptcha-response-secret",
}),
formData: "username=demo&password=PlaintextP%40ssw0rd%21&api_key=secret-key",
})
).toEqual({
data: JSON.stringify({
customer_number: "cust-123",
password: "[redacted]",
g_recaptcha_response: "[redacted]",
}),
formData: "username=demo&password=%5Bredacted%5D&api_key=%5Bredacted%5D",
});
});
it("always sends failure metadata and keeps non-failure events gated by capture policy", async () => {
const sentBodies = [];
__setReleaseTimelineTransportForTests(async (body) => {
sentBodies.push(body);
return { accepted: body.events.length };
});
expect(recordReleaseTimelineEvent("route_change", { to: "/superuser" })).toBe(false);
expect(
recordReleaseTimelineEvent(
"request_failed",
{
request: {
headers: { Authorization: "Bearer token" },
data: JSON.stringify({ password: "PlaintextP@ssw0rd!" }),
},
response: { status: 500, data: { token: "response-token" } },
},
{ severity: "error", moduleKey: "requestqueue" }
)
).toBe(true);
await flushReleaseTimelineEvents();
expect(sentBodies).toHaveLength(1);
expect(sentBodies[0].events[0]).toMatchObject({
type: "request_failed",
severity: "error",
module_key: "requestqueue",
});
expect(sentBodies[0].events[0].payload.request.headers.Authorization).toBe("[redacted]");
expect(sentBodies[0].events[0].payload.request.data).toBe("[capture-disabled]");
expect(sentBodies[0].events[0].payload.response.data).toBe("[capture-disabled]");
});
it("keeps a local redacted frontend failure buffer for error reports", () => {
recordReleaseTimelineEvent(
"vue_component_error",
{
message: "Render failed",
token: "secret",
component: "BrokenWidget",
},
{ severity: "error", moduleKey: "frontend" }
);
recordReleaseTimelineEvent(
"request_failed",
{ message: "Handled by request queue" },
{ severity: "error", moduleKey: "requestqueue" }
);
const events = getRecentFrontendFailureEvents();
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
type: "vue_component_error",
module_key: "frontend",
});
expect(events[0].payload.token).toBe("[redacted]");
});
it("sends non-failure timeline events when replay capture is enabled", async () => {
const sentBodies = [];
__setReleaseTimelineTransportForTests(async (body) => {
sentBodies.push(body);
return { accepted: body.events.length };
});
configureReleaseRuntime({
trace_id: "trace-enabled",
channel: { slug: "beta" },
capture_policy: { enabled: true, capture_level: "full_redacted" },
});
expect(recordReleaseTimelineEvent("route_change", { to: "/dashboard" })).toBe(true);
await flushReleaseTimelineEvents();
expect(sentBodies).toHaveLength(1);
expect(sentBodies[0].context.channel_slug).toBe("beta");
expect(sentBodies[0].context.device.type).toBe("desktop");
expect(sentBodies[0].context.frontend.version_label).toBeTruthy();
expect(sentBodies[0].events[0].type).toBe("route_change");
});
it("never emits login grants in the top-level timeline route", async () => {
const sentBodies = [];
__setReleaseTimelineTransportForTests(async (body) => {
sentBodies.push(body);
return { accepted: body.events.length };
});
configureReleaseRuntime({
trace_id: "trace-route-redaction",
channel: { slug: "beta" },
capture_policy: { enabled: true, capture_level: "full_redacted" },
});
recordReleaseTimelineEvent(
"route_change",
{ to: "/login/qr#grant=lbg_payload_copy" },
{ route: "/login/qr?grant=lbg_query_copy#grant=lbg_fragment_copy" }
);
await flushReleaseTimelineEvents();
expect(sentBodies[0].events[0].route).toBe("/login/qr?grant=%5Bredacted%5D");
expect(JSON.stringify(sentBodies[0])).not.toContain("lbg_query_copy");
expect(JSON.stringify(sentBodies[0])).not.toContain("lbg_fragment_copy");
});
it("builds device and release context for replay sessions", () => {
configureReleaseRuntime({
trace_id: "trace-context",
channel: { slug: "canary" },
versions: {
frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" },
api: { version_label: "api-canary", commit_sha: "def456" },
},
capture_policy: { enabled: true, capture_level: "full_redacted" },
});
const context = buildReleaseTimelineContext();
expect(context.trace_id).toBe("trace-context");
expect(context.channel_slug).toBe("canary");
expect(context.device.type).toBe("desktop");
expect(context.frontend.version_label).toBe("frontend-canary");
expect(context.api.version_label).toBe("api-canary");
expect(context.viewport.width).toBeGreaterThan(0);
});
});