Adds TECH_LOG_STUDIO_SOURCE (MOCK | HTTP, default MOCK) to the V2 runtime
config schema so a build can switch createTechLogFeatureInstalledInput
between the mock and HTTP Studio gateways without a rebuild. V1 documents
predate the key and always normalize to MOCK. The HTTP gateway is
constructed with only { operations } per Task 4's actual signature -
no CSRF provider is wired here; that lands with attachCredentials at a
later composition-root task.
Updates every existing Studio test call site to the new required
createTechLogFeatureInstalledInput(context) signature via a shared
tests/helpers/studio-install-context.ts MOCK fixture, so the whole
existing Studio suite keeps exercising the mock adapter unchanged.
220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { loadRuntimeConfig } from "../../src/bootstrap/load-runtime-config.ts";
|
|
import { validateRuntimeConfig } from "../../src/bootstrap/runtime-config-schema.ts";
|
|
import { assertSafeConfigNames } from "../../src/contracts/env.ts";
|
|
|
|
const validConfig = {
|
|
APP_ENV: "local",
|
|
API_BASE_URL: "http://localhost:8080/",
|
|
REQUEST_TIMEOUT_MS: 10_000,
|
|
MAX_RETRY_ATTEMPTS: 2,
|
|
TELEMETRY_ENABLED: false,
|
|
AUTH_MODE: "external",
|
|
CONFIG_SCHEMA_VERSION: "2.0",
|
|
RELEASE_MANIFEST_URL: "/release-manifest.json",
|
|
BUILD_ID: "build-a",
|
|
};
|
|
|
|
const validV1Config = {
|
|
...validConfig,
|
|
CONFIG_SCHEMA_VERSION: "1",
|
|
API_CONTRACT_VERSION: "1.4.0",
|
|
};
|
|
|
|
describe("runtime configuration boundary", () => {
|
|
it.each([
|
|
[{ ...validConfig, API_BASE_URL: undefined }, "required key"],
|
|
[{ ...validConfig, REQUEST_TIMEOUT_MS: 0 }, "integer range"],
|
|
[{ ...validConfig, MAX_RETRY_ATTEMPTS: 3 }, "retry cap"],
|
|
[{ ...validConfig, TELEMETRY_ENABLED: "false" }, "ambiguous boolean"],
|
|
[{ ...validConfig, CONFIG_SCHEMA_VERSION: "next" }, "version"],
|
|
[
|
|
{ ...validConfig, API_CONTRACT_VERSION: "1" },
|
|
"§5.1 scalar contract version is removed from V2",
|
|
],
|
|
[
|
|
{ ...validConfig, API_BASE_URL: "http://localhost:8080/api" },
|
|
"§6.2 base URL path must end with /",
|
|
],
|
|
[
|
|
{ ...validConfig, API_BASE_URL: "http://localhost:8080/#frag" },
|
|
"§6.2 hash in base URL",
|
|
],
|
|
[
|
|
{ ...validConfig, API_BASE_URL: "http://user:pw@localhost:8080/" },
|
|
"§6.2 credentials in URL",
|
|
],
|
|
[
|
|
{ ...validConfig, RELEASE_MANIFEST_URL: "/a/../b.json" },
|
|
"§6.2 dot segment traversal",
|
|
],
|
|
[
|
|
{ ...validConfig, RELEASE_MANIFEST_URL: "/manifest.json?v=1" },
|
|
"§6.2 query in manifest URL",
|
|
],
|
|
[{ ...validConfig, UNKNOWN_KEY: true }, "unknown key"],
|
|
])("rejects invalid config: %s (%s)", (candidate, _reason) => {
|
|
void _reason;
|
|
expect(validateRuntimeConfig(candidate).success).toBe(false);
|
|
});
|
|
|
|
it("rejects secret-like names before schema validation", () => {
|
|
expect(() => assertSafeConfigNames({ CLIENT_SECRET: "not-safe" })).toThrow(
|
|
"Forbidden client configuration key",
|
|
);
|
|
});
|
|
|
|
it("allows demo authentication only for local runtime configuration", () => {
|
|
expect(
|
|
validateRuntimeConfig({ ...validConfig, AUTH_MODE: "demo" }).success,
|
|
).toBe(true);
|
|
expect(
|
|
validateRuntimeConfig({
|
|
...validConfig,
|
|
APP_ENV: "production",
|
|
API_BASE_URL: "https://api.example.test/",
|
|
AUTH_MODE: "demo",
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("accepts only the explicitly supported V1 and V2 boot versions", () => {
|
|
expect(validateRuntimeConfig(validV1Config)).toMatchObject({
|
|
success: true,
|
|
schema: "V1",
|
|
});
|
|
expect(validateRuntimeConfig(validConfig)).toMatchObject({
|
|
success: true,
|
|
schema: "V2",
|
|
});
|
|
|
|
for (const version of ["0", "1.0", "2.0.1", "3.0"]) {
|
|
expect(
|
|
validateRuntimeConfig({
|
|
...validV1Config,
|
|
CONFIG_SCHEMA_VERSION: version,
|
|
}).success,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
"file:///tmp/api/",
|
|
"data:text/plain,/",
|
|
"blob:https://example.test/00000000-0000-0000-0000-000000000000",
|
|
])("rejects a non-HTTP API endpoint in local mode: %s", (API_BASE_URL) => {
|
|
expect(validateRuntimeConfig({ ...validConfig, API_BASE_URL }).success).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it.each(["file:///tmp/telemetry", "data:text/plain,telemetry"])(
|
|
"rejects a non-HTTP telemetry endpoint in local mode: %s",
|
|
(TELEMETRY_ENDPOINT) => {
|
|
expect(
|
|
validateRuntimeConfig({
|
|
...validConfig,
|
|
TELEMETRY_ENABLED: true,
|
|
TELEMETRY_ENDPOINT,
|
|
}).success,
|
|
).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("validates a fetched config under the 500ms budget excluding network", async () => {
|
|
let current = 100;
|
|
const result = await loadRuntimeConfig({
|
|
buildConfig: {
|
|
buildId: "build-a",
|
|
commitSha: "local",
|
|
routerBasePath: "/",
|
|
runtimeConfigUrl: "/config.json",
|
|
},
|
|
fetcher: async () =>
|
|
new Response(JSON.stringify(validConfig), {
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
now: () => (current += 2),
|
|
});
|
|
|
|
expect(result.validationDurationMs).toBeLessThanOrEqual(500);
|
|
expect(result.config.API_BASE_URL).toBe("http://localhost:8080/");
|
|
expect(result.configSchema).toBe("V2");
|
|
expect(result.config.CAPABILITY_OVERRIDES.SERVICE_WORKER).toBe("DEFAULT");
|
|
});
|
|
|
|
it("does not include network acquisition in validationDurationMs", async () => {
|
|
let current = 0;
|
|
const result = await loadRuntimeConfig({
|
|
buildConfig: {
|
|
buildId: "build-a",
|
|
commitSha: "local",
|
|
routerBasePath: "/",
|
|
runtimeConfigUrl: "/config.json",
|
|
},
|
|
fetcher: async () => {
|
|
current = 10_000;
|
|
return new Response(JSON.stringify(validConfig), {
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
},
|
|
now: () => current,
|
|
});
|
|
|
|
expect(result.validationDurationMs).toBe(0);
|
|
});
|
|
|
|
it("accepts TECH_LOG_STUDIO_SOURCE and defaults it to MOCK", () => {
|
|
const defaulted = validateRuntimeConfig(validConfig);
|
|
expect(defaulted.success).toBe(true);
|
|
if (defaulted.success) {
|
|
expect(defaulted.data.TECH_LOG_STUDIO_SOURCE).toBe("MOCK");
|
|
}
|
|
|
|
const explicit = validateRuntimeConfig({
|
|
...validConfig,
|
|
TECH_LOG_STUDIO_SOURCE: "HTTP",
|
|
});
|
|
expect(explicit.success).toBe(true);
|
|
if (explicit.success) {
|
|
expect(explicit.data.TECH_LOG_STUDIO_SOURCE).toBe("HTTP");
|
|
}
|
|
|
|
expect(
|
|
validateRuntimeConfig({
|
|
...validConfig,
|
|
TECH_LOG_STUDIO_SOURCE: "LIVE",
|
|
}).success,
|
|
).toBe(false);
|
|
|
|
// A V1 document has no such key; normalization must still land on MOCK.
|
|
expect(validateRuntimeConfig(validV1Config)).toMatchObject({
|
|
success: true,
|
|
data: { TECH_LOG_STUDIO_SOURCE: "MOCK" },
|
|
});
|
|
});
|
|
|
|
it("returns only safe boot fields on failure", async () => {
|
|
await expect(
|
|
loadRuntimeConfig({
|
|
buildConfig: {
|
|
buildId: "build-a",
|
|
commitSha: "local",
|
|
routerBasePath: "/",
|
|
runtimeConfigUrl: "/config.json",
|
|
},
|
|
fetcher: async () =>
|
|
new Response("{", {
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
}),
|
|
).rejects.toMatchObject({
|
|
safe: {
|
|
kind: "BOOT_CONFIG_FAILURE",
|
|
buildId: "build-a",
|
|
},
|
|
});
|
|
});
|
|
});
|