mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 05:12:16 +03:00
* feat(radar): client-side schema + accessor for referral links (D28)
Server already publishes a signed `referrals` section on the Radar feed
({fixed, campaigns}); this adds the client mirror: RadarFeedSchema gains a
`.default()`-backed `referrals` field (old cached feeds without it stay
valid) with https-only url validation, and src/lib/radar/index.ts exposes
getRadarReferrals()/getDefaultReferralFor() (never throw: flag off, no
cache, or a corrupt/old payload all resolve to the empty shape). The
provider-default lookup itself lives in a new DB-free src/lib/radar/
referrals.ts so it stays safe to import from a "use client" component.
* feat(radar): add GET /api/radar/referrals route (D28)
Local-only route mirroring the /api/radar/catalog gate order: RADAR_ENABLED
off => 404 before any auth check (byte-identical flag-off inertia),
unauthenticated => 401, otherwise 200 with {fixed, campaigns, tier} read
straight from the local cache. Never proxies the private feed server.
* feat(dashboard): add "free credits" tab to the Radar page (D28)
Reuses the existing /dashboard/radar page instead of a new route (less
routing/i18n surface): a second tab lists fixed referral links (grouped by
provider, with requiredAction + an external-link button) and temporary
campaigns (with validUntil). When campaigns is empty and the served tier is
community, shows a soft upsell note — never gates the fixed links list,
which stays fully populated on every tier. Adds 10 new radarPage i18n keys
(English fallback) to all 43 locale files to avoid dropping i18n-ui-coverage
below threshold.
* feat(providers): use Radar default referral link on the provider name (D28)
ProviderPageHeader already linked the provider name to providerInfo.website
with a precedent for a monetized link (the Kimi partner-link note); this
lets a Radar default referral override that URL, reusing the exact same
discreet note instead of a new visual treatment.
Loose coupling: resolveProviderHeaderLink() in providerPageUtils.ts is a
pure function with no @/lib/radar or @/lib/db/* import (asserted by the new
test), so the providers dashboard never depends on the DB-touching Radar
module to render. ProviderDetailPageClient (a "use client" component) is
the only place that fetches Radar data, via the local /api/radar/referrals
route (same pattern the Radar page itself uses) and the DB-free
findDefaultReferral() helper. With RADAR_ENABLED off, no cache, or no
default referral for the provider, the header renders byte-identical to
before this feature existed.
* docs(radar): document referral links / free credits (D28)
Adds a "Referral links (free credits)" section covering the referrals feed
shape, the getRadarReferrals()/getDefaultReferralFor() accessors, the new
GET /api/radar/referrals route, the Radar page's "Free credits" tab, and
the loosely-coupled referral link on the provider-name header. Also
corrects the local-routes count (four -> five) now that /api/radar/
referrals exists.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
/**
|
|
* tests/unit/radar-referrals-page-tab.test.ts
|
|
*
|
|
* Structural regression guard for the "Pegue seus créditos grátis" tab (D28)
|
|
* added to the existing /dashboard/radar page (no new dashboard route was
|
|
* created — per spec, less i18n/routing surface). Verifies:
|
|
*
|
|
* - The page source wires a `referrals` tab fetching the LOCAL
|
|
* /api/radar/referrals route only (never the private feed server).
|
|
* - Every `t("...")` key the page references under the new tab exists (with
|
|
* a non-empty value) in en.json — a stand-in for a full i18n-ui-coverage
|
|
* run without needing to execute that whole gate here.
|
|
* - No new dashboard route directory was created for this feature.
|
|
*/
|
|
|
|
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");
|
|
|
|
test("radar page: fetches referrals only from the local /api/radar/referrals route", () => {
|
|
assert.ok(
|
|
PAGE_SRC.includes('fetch("/api/radar/referrals")'),
|
|
"page must fetch the local referrals route"
|
|
);
|
|
// Never a direct call to an external feed-server URL from this component.
|
|
assert.ok(
|
|
!/https?:\/\/(?!localhost)/.test(PAGE_SRC.replace(/\/\*[\s\S]*?\*\//g, "")),
|
|
"page must never fetch an external URL directly (NEVER proxy the private feed server)"
|
|
);
|
|
});
|
|
|
|
test("radar page: renders a referrals tab with catalogTab/freeCreditsTab labels", () => {
|
|
assert.ok(PAGE_SRC.includes('t("catalogTab")'));
|
|
assert.ok(PAGE_SRC.includes('t("freeCreditsTab")'));
|
|
assert.ok(PAGE_SRC.includes('activeTab === "referrals"'));
|
|
});
|
|
|
|
test("radar page: campaigns-empty community upsell is soft (never blocks the fixed links list)", () => {
|
|
// The upsell only gates the campaigns section, never `referrals.fixed`.
|
|
assert.ok(PAGE_SRC.includes("campaignsUpsellCommunity"));
|
|
assert.ok(
|
|
!/fixed[\s\S]{0,80}tier === "community"/.test(PAGE_SRC),
|
|
"fixed links must never be gated on tier client-side (server already gates by artifact)"
|
|
);
|
|
});
|
|
|
|
test("no new dashboard route was created for the free-credits feature (spec: reuse /dashboard/radar)", () => {
|
|
const dashboardDir = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard");
|
|
assert.ok(
|
|
!fs.existsSync(path.join(dashboardDir, "referrals")),
|
|
"must not create src/app/(dashboard)/dashboard/referrals/"
|
|
);
|
|
assert.ok(
|
|
!fs.existsSync(path.join(dashboardDir, "radar", "referrals")),
|
|
"must not create src/app/(dashboard)/dashboard/radar/referrals/ either — same page, new tab"
|
|
);
|
|
});
|
|
|
|
test("every t(\"...\") key referenced in the referrals tab section exists (non-empty) in en.json", () => {
|
|
const enMessages = JSON.parse(
|
|
fs.readFileSync(
|
|
path.resolve(process.cwd(), "src/i18n/messages/en.json"),
|
|
"utf-8"
|
|
)
|
|
);
|
|
const radarPage = enMessages.radarPage as Record<string, unknown>;
|
|
assert.ok(radarPage, "en.json must have a radarPage namespace");
|
|
|
|
const referencedKeys = [
|
|
"catalogTab",
|
|
"freeCreditsTab",
|
|
"freeCreditsSubtitle",
|
|
"fixedLinksEmpty",
|
|
"requiredActionLabel",
|
|
"claimButton",
|
|
"campaignsTitle",
|
|
"campaignsEmpty",
|
|
"campaignsUpsellCommunity",
|
|
"campaignsValidUntil",
|
|
];
|
|
|
|
for (const key of referencedKeys) {
|
|
assert.ok(PAGE_SRC.includes(`t("${key}")` ) || PAGE_SRC.includes(`t("${key}",`), `page.tsx must reference t("${key}")`);
|
|
assert.equal(typeof radarPage[key], "string", `en.json radarPage.${key} must be a string`);
|
|
assert.ok((radarPage[key] as string).length > 0, `en.json radarPage.${key} must be non-empty`);
|
|
}
|
|
});
|
|
|
|
test("all 43 locale message files carry every new radarPage key with a non-empty 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}`);
|
|
|
|
const NEW_KEYS = [
|
|
"catalogTab",
|
|
"freeCreditsTab",
|
|
"freeCreditsSubtitle",
|
|
"fixedLinksEmpty",
|
|
"requiredActionLabel",
|
|
"claimButton",
|
|
"campaignsTitle",
|
|
"campaignsEmpty",
|
|
"campaignsUpsellCommunity",
|
|
"campaignsValidUntil",
|
|
];
|
|
|
|
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) {
|
|
assert.equal(
|
|
typeof radarPage![key],
|
|
"string",
|
|
`${file}: radarPage.${key} must be a non-empty string`
|
|
);
|
|
assert.ok((radarPage![key] as string).length > 0, `${file}: radarPage.${key} is empty`);
|
|
}
|
|
}
|
|
});
|