Files
Jyotisha/frontend/tests/personal-report-entitlement.test.ts
T
Jesse_Chen 17430a1f21
Staging Backend Quality Gate / validate (push) Successful in 12m13s
Staging Backend Quality Gate / publish (push) Has been cancelled
fix(reports): trust verified proxy origin
2026-08-09 00:54:55 +08:00

171 lines
5.8 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
DEFAULT_PERSONAL_REPORT_DAILY_LIMIT,
REPORT_EXPORT_PERSONAL_CAPABILITY_KEY,
checkPersonalReportEntitlement,
checkSameOrigin,
isPersonalReportFeatureEnabled,
readPersonalReportDailyLimit,
resolveAllowedReportOrigins,
} from "../src/lib/personal-report-entitlement.ts";
test("entitlement exposes the report.export.personal capability key", () => {
assert.equal(REPORT_EXPORT_PERSONAL_CAPABILITY_KEY, "report.export.personal");
});
test("feature flag is enabled only by explicit env true", () => {
assert.equal(isPersonalReportFeatureEnabled({}), false);
assert.equal(isPersonalReportFeatureEnabled({ PERSONAL_REPORT_ENABLED: "false" }), false);
assert.equal(isPersonalReportFeatureEnabled({ PERSONAL_REPORT_ENABLED: "TRUE" }), false);
assert.equal(isPersonalReportFeatureEnabled({ PERSONAL_REPORT_ENABLED: "true" }), true);
});
test("daily limit is read from env and never hardcoded in the module UI surface", () => {
assert.equal(readPersonalReportDailyLimit({}), DEFAULT_PERSONAL_REPORT_DAILY_LIMIT);
assert.equal(readPersonalReportDailyLimit({ PERSONAL_REPORT_DAILY_LIMIT: "3" }), 3);
assert.equal(readPersonalReportDailyLimit({ PERSONAL_REPORT_DAILY_LIMIT: "0" }), DEFAULT_PERSONAL_REPORT_DAILY_LIMIT);
assert.equal(readPersonalReportDailyLimit({ PERSONAL_REPORT_DAILY_LIMIT: "-1" }), DEFAULT_PERSONAL_REPORT_DAILY_LIMIT);
assert.equal(readPersonalReportDailyLimit({ PERSONAL_REPORT_DAILY_LIMIT: "abc" }), DEFAULT_PERSONAL_REPORT_DAILY_LIMIT);
});
test("allowed origins are parsed from the comma-separated env list", () => {
assert.deepEqual(resolveAllowedReportOrigins({}), []);
assert.deepEqual(
resolveAllowedReportOrigins({ PERSONAL_REPORT_ALLOWED_ORIGINS: " https://a.example ,https://b.example, " }),
["https://a.example", "https://b.example"],
);
});
test("same-origin check accepts absent origin and same request origin", () => {
assert.deepEqual(checkSameOrigin("https://jyotisha.chat/api/reports", null, []), { ok: true });
assert.deepEqual(
checkSameOrigin("https://jyotisha.chat/api/reports", "https://jyotisha.chat", []),
{ ok: true },
);
});
test("same-origin check rejects cross-origin and accepts a trusted allowlist", () => {
assert.deepEqual(
checkSameOrigin("https://jyotisha.chat/api/reports", "https://evil.example", []),
{ ok: false, code: "cross_origin_forbidden" },
);
assert.deepEqual(
checkSameOrigin(
"https://jyotisha.chat/api/reports",
"https://trusted-proxy.example",
["https://trusted-proxy.example"],
),
{ ok: true },
);
});
test("same-origin check trusts only consistent forwarded host and protocol", () => {
const headers = new Headers({
host: "staging.jyotisha.chat",
"x-forwarded-host": "staging.jyotisha.chat",
"x-forwarded-proto": "https",
});
assert.deepEqual(
checkSameOrigin(
"http://staging.jyotisha.chat/api/reports",
"https://staging.jyotisha.chat",
[],
headers,
),
{ ok: true },
);
assert.deepEqual(
checkSameOrigin(
"http://staging.jyotisha.chat/api/reports",
"https://evil.example",
[],
headers,
),
{ ok: false, code: "cross_origin_forbidden" },
);
assert.deepEqual(
checkSameOrigin(
"http://staging.jyotisha.chat/api/reports",
"https://staging.jyotisha.chat",
[],
new Headers({
host: "staging.jyotisha.chat",
"x-forwarded-host": "evil.example",
"x-forwarded-proto": "https",
}),
),
{ ok: false, code: "cross_origin_forbidden" },
);
assert.deepEqual(
checkSameOrigin(
"http://staging.jyotisha.chat/api/reports",
"https://staging.jyotisha.chat",
[],
new Headers({
host: "staging.jyotisha.chat",
"x-forwarded-host": "staging.jyotisha.chat",
"x-forwarded-proto": "https, http",
}),
),
{ ok: false, code: "cross_origin_forbidden" },
);
});
test("entitlement blocks when the feature is disabled", async () => {
const result = await checkPersonalReportEntitlement({
userId: "u1",
featureEnabled: false,
dailyLimit: 5,
countGenerating: async () => 0,
countCreatedToday: async () => 0,
});
assert.deepEqual(result, { allowed: false, code: "report_export_disabled", httpStatus: 403 });
});
test("entitlement blocks a second concurrent generation with 409", async () => {
const result = await checkPersonalReportEntitlement({
userId: "u1",
featureEnabled: true,
dailyLimit: 5,
countGenerating: async () => 1,
countCreatedToday: async () => 0,
});
assert.deepEqual(result, { allowed: false, code: "report_generation_in_progress", httpStatus: 409 });
});
test("entitlement blocks at the daily limit with 429", async () => {
const result = await checkPersonalReportEntitlement({
userId: "u1",
featureEnabled: true,
dailyLimit: 2,
countGenerating: async () => 0,
countCreatedToday: async () => 2,
});
assert.deepEqual(result, { allowed: false, code: "report_rate_limited", httpStatus: 429 });
});
test("entitlement allows a fresh generation within limits", async () => {
const result = await checkPersonalReportEntitlement({
userId: "u1",
featureEnabled: true,
dailyLimit: 5,
countGenerating: async () => 0,
countCreatedToday: async () => 1,
});
assert.deepEqual(result, { allowed: true });
});
test("report API routes never hardcode the daily limit in the UI-facing module", () => {
const entitlementSource = readFileSync(
new URL("../src/lib/personal-report-entitlement.ts", import.meta.url),
"utf8",
);
assert.match(entitlementSource, /REPORT_DAILY_LIMIT_ENV/);
// The limit must be read from env at request time, not baked as a literal
// default inside the route response mapping.
assert.doesNotMatch(entitlementSource, /每日|上限/);
});