mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
* feat(radar): shared supporter-key format validator
Extract the "omr_" + 40 hex supporter-key regex out of the
POST /api/radar/settings Zod schema into a pure, client-safe helper
(src/lib/radar/supporterKey.ts) so the format rule lives in exactly one
place and the upcoming activation-screen input can reuse it for a
UX-only pre-check. Server-side Zod validation stays authoritative.
Adds regression coverage: both directions of the format check, a
combined opt-in+supporterKey POST persisting both fields with the key
always masked (never raw) in either the POST or GET response body, and
a flag-off inertia case for the same combined payload shape.
* feat(dashboard): paste-key input on the Radar activation screen
The Radar activation screen had opt-in and the two "get a key" claim
buttons, but nowhere to paste a key someone already has — the last
piece of the supporter flow. Add the field to the activation screen
itself, as the primary path: pasting a key and submitting sends
POST /api/radar/settings with { optIn: true, supporterKey } together,
so pasting a valid key both sets it and unlocks the screen in one step.
Client-side format validation (via the shared isValidSupporterKeyFormat
helper) is a UX nicety only; the server's Zod schema already validates
authoritatively. When a key is already set (hasSupporterKey from
GET /api/radar/settings — e.g. set out of band before this UI existed),
the screen shows the masked form instead of an empty input, with a
"change key" control to paste a new one; the raw key is never
displayed. The existing plain "Activate" button (no key, community
tier) and the two claim/plans buttons are unchanged and still present
below, so all three paths to this screen coexist.
Adds 4 new i18n keys (keySectionTitle, keyInvalidFormatError,
activateWithKeyButton, changeKeyButton) with an English fallback across
all 43 locale files (172 entries) — no __MISSING__ sentinel, no price.
* docs(radar): close the paste-key-input known gap
RADAR.md documented a known gap: the activation screen had no
dedicated key-paste input, only the two claim/plans buttons. That gap
is closed — describe the new input, the combined opt-in+supporterKey
submission, and the masked-key "already activated" state instead.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
155 lines
5.7 KiB
TypeScript
155 lines
5.7 KiB
TypeScript
/**
|
|
* tests/unit/radar-key-input.test.ts
|
|
*
|
|
* TDD guard for the paste-key input added to the Radar activation screen
|
|
* (src/app/(dashboard)/dashboard/radar/page.tsx). Backend was already ready
|
|
* (POST /api/radar/settings already accepted `supporterKey`) — this is the
|
|
* missing last piece: an `<input>` on the activation screen so an operator
|
|
* with a key in hand can paste it in, instead of calling the API directly.
|
|
*
|
|
* Structural, source-based — same style as radar-claim-buttons.test.ts —
|
|
* deliberately avoids a full component render (no jsdom harness in this
|
|
* repo's unit runner).
|
|
*
|
|
* Covers:
|
|
* - the page wires the pure isValidSupporterKeyFormat() helper and submits
|
|
* { optIn: true, supporterKey } together (pasting a key both activates
|
|
* opt-in AND sets the key, per spec: "o campo vai na PRÓPRIA tela de
|
|
* ativação, para desbloqueá-la");
|
|
* - the already-activated state shows the masked key (never the raw one)
|
|
* with a "change key" escape hatch;
|
|
* - the 4 new t("...") keys exist (non-empty, no price) in en.json and all
|
|
* 43 locale files;
|
|
* - no OSS file mentions "freellmapi".
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const PAGE_PATH = path.resolve(
|
|
process.cwd(),
|
|
"src/app/(dashboard)/dashboard/radar/page.tsx"
|
|
);
|
|
const PAGE_SRC = fs.readFileSync(PAGE_PATH, "utf-8");
|
|
|
|
const NEW_KEYS = [
|
|
"keySectionTitle",
|
|
"keyInvalidFormatError",
|
|
"activateWithKeyButton",
|
|
"changeKeyButton",
|
|
];
|
|
|
|
test("radar page: imports and calls the shared isValidSupporterKeyFormat() helper", () => {
|
|
assert.ok(
|
|
PAGE_SRC.includes('from "@/lib/radar/supporterKey"'),
|
|
"page must import the pure format-validation helper from src/lib/radar/supporterKey.ts"
|
|
);
|
|
assert.ok(
|
|
PAGE_SRC.includes("isValidSupporterKeyFormat("),
|
|
"page must call isValidSupporterKeyFormat() before submitting"
|
|
);
|
|
});
|
|
|
|
test("radar page: submitting a pasted key sends optIn+supporterKey together", () => {
|
|
const submitFn = PAGE_SRC.match(
|
|
/const handleSubmitKey = useCallback\(async \(\) => \{[\s\S]*?\n {2}\}, \[[^\]]*\]\);/
|
|
)?.[0];
|
|
assert.ok(submitFn, "handleSubmitKey callback must exist");
|
|
assert.ok(
|
|
submitFn!.includes("/api/radar/settings"),
|
|
"must POST to /api/radar/settings"
|
|
);
|
|
assert.ok(
|
|
/optIn:\s*true/.test(submitFn!),
|
|
"pasting a key must also opt in (unlocks the activation screen)"
|
|
);
|
|
assert.ok(
|
|
/supporterKey:\s*trimmed/.test(submitFn!),
|
|
"the trimmed pasted key must be sent as supporterKey"
|
|
);
|
|
});
|
|
|
|
test("radar page: already-activated state shows the masked key, never displays a raw key", () => {
|
|
assert.ok(
|
|
PAGE_SRC.includes("supporterKeyMasked"),
|
|
"page must track supporterKeyMasked state from GET /api/radar/settings"
|
|
);
|
|
assert.ok(
|
|
PAGE_SRC.includes("hasSupporterKey"),
|
|
"page must track hasSupporterKey state from GET /api/radar/settings"
|
|
);
|
|
// The only place a key VALUE renders is the masked one — the raw pasted
|
|
// value only ever flows into the POST body (`trimmed`), never back onto
|
|
// the screen as a rendered node.
|
|
assert.ok(
|
|
PAGE_SRC.includes("{supporterKeyMasked}"),
|
|
"the masked key must be the value rendered when a key is already set"
|
|
);
|
|
assert.ok(
|
|
!/\{keyInput\}[\s\S]{0,5}<\/span>/.test(PAGE_SRC),
|
|
"the raw pasted input value must never be rendered as display text (only as a controlled <input> value)"
|
|
);
|
|
});
|
|
|
|
test("radar page: 'change key' escape hatch exists to replace an already-set key", () => {
|
|
assert.ok(
|
|
PAGE_SRC.includes("setShowKeyForm(true)"),
|
|
"a control must exist to reveal the paste form again to replace an existing key"
|
|
);
|
|
assert.ok(
|
|
PAGE_SRC.includes(`t("changeKeyButton")`),
|
|
"page.tsx must reference t(\"changeKeyButton\")"
|
|
);
|
|
});
|
|
|
|
test("radar page: references the 4 new key-input t(...) keys", () => {
|
|
for (const key of NEW_KEYS) {
|
|
assert.ok(
|
|
PAGE_SRC.includes(`t("${key}")`),
|
|
`page.tsx must reference t("${key}")`
|
|
);
|
|
}
|
|
});
|
|
|
|
test("radar page + all 43 locale files: no price/monetary value in the key-input copy (D14)", () => {
|
|
const PRICE_PATTERN = /\$\s?\d|R\$\s?\d|\d+[.,]\d{2}\s?(USD|BRL|EUR)|\b(lifetime|life-time)\b.{0,20}\$/i;
|
|
assert.ok(!PRICE_PATTERN.test(PAGE_SRC), "page.tsx must not contain a price/monetary value");
|
|
|
|
const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
|
|
const files = fs.readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
|
|
assert.ok(files.length >= 40, `expected ~43 locale files, found ${files.length}`);
|
|
|
|
for (const file of files) {
|
|
const data = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf-8"));
|
|
const radarPage = data.radarPage as Record<string, unknown> | undefined;
|
|
assert.ok(radarPage, `${file}: missing radarPage namespace`);
|
|
for (const key of NEW_KEYS) {
|
|
const value = radarPage![key];
|
|
assert.equal(typeof value, "string", `${file}: radarPage.${key} must be a string`);
|
|
assert.ok((value as string).length > 0, `${file}: radarPage.${key} is empty`);
|
|
assert.ok(
|
|
!value!.toString().startsWith("__MISSING__"),
|
|
`${file}: radarPage.${key} must not be a __MISSING__ sentinel — use a real English fallback`
|
|
);
|
|
assert.ok(
|
|
!PRICE_PATTERN.test(value as string),
|
|
`${file}: radarPage.${key} must not contain a price/monetary value`
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("no OSS file mentions the word 'freellmapi'", () => {
|
|
const filesToCheck = [
|
|
PAGE_PATH,
|
|
path.resolve(process.cwd(), "src/lib/radar/supporterKey.ts"),
|
|
path.resolve(process.cwd(), "src/app/api/radar/settings/route.ts"),
|
|
];
|
|
for (const file of filesToCheck) {
|
|
const src = fs.readFileSync(file, "utf-8");
|
|
assert.ok(!/freellmapi/i.test(src), `${file} must not mention freellmapi`);
|
|
}
|
|
});
|