Files
gitmost/apps/client/src/features/api-key/utils.test.ts
T
agent_coder d6411424c1 feat(api-key): distinguish expired from expiring-soon + cover service unwrap
Fix 1: an already-expired key rendered the forward-looking "Expiring soon"
badge/tooltip ("expires within 30 days"), which is false for a past expiry.
Add a pure isExpired() helper and, in ApiKeysManager, render a red "Expired"
badge ("This key has expired") for past expiries; keep the orange "Expiring
soon" badge only for keys expiring in the future within 30 days (the two
states are made mutually exclusive at the call site). Extend utils.test.ts
with isExpired coverage (past/future/null/exact-now boundary) and add a
component assertion that an expired key shows "Expired" and NOT "Expiring
soon".

Fix 2: the response-contract unwrap (res.data turning the server envelope
{data:{token,apiKey},success,status} into {token,apiKey}) was exercised by no
test — component tests fully mock the service. Add a service-layer unit test
that mocks the api-client (axios instance) and asserts createApiKey unwraps to
the inner {token,apiKey} payload and getApiKeys resolves to the row array.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:20:17 +03:00

101 lines
3.4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
DEFAULT_LIFETIME,
EXPIRY_WARNING_DAYS,
isExpired,
isExpiringSoon,
lastUsedBucket,
lifetimeToExpiresAt,
} from "./utils";
const NOW = new Date("2026-07-11T12:00:00.000Z");
const daysFromNow = (n: number) =>
new Date(NOW.getTime() + n * 24 * 60 * 60 * 1000).toISOString();
describe("lifetimeToExpiresAt", () => {
it("default lifetime is 1 year (acceptance #7)", () => {
expect(DEFAULT_LIFETIME).toBe("1y");
const iso = lifetimeToExpiresAt("1y", NOW);
expect(iso).toBe("2027-07-11T12:00:00.000Z");
});
it('"never" sends null (acceptance #7)', () => {
expect(lifetimeToExpiresAt("never", NOW)).toBeNull();
});
it("30d / 90d map to the exact future instant", () => {
expect(lifetimeToExpiresAt("30d", NOW)).toBe(daysFromNow(30));
expect(lifetimeToExpiresAt("90d", NOW)).toBe(daysFromNow(90));
});
});
describe("isExpiringSoon (acceptance #3 highlight)", () => {
it("an unlimited key is never 'soon'", () => {
expect(isExpiringSoon(null, NOW)).toBe(false);
});
it("highlights a key expiring within the 30-day window", () => {
expect(isExpiringSoon(daysFromNow(EXPIRY_WARNING_DAYS - 1), NOW)).toBe(true);
expect(isExpiringSoon(daysFromNow(10), NOW)).toBe(true);
});
it("does not highlight a key well outside the window", () => {
expect(isExpiringSoon(daysFromNow(EXPIRY_WARNING_DAYS + 1), NOW)).toBe(
false,
);
expect(isExpiringSoon(daysFromNow(200), NOW)).toBe(false);
});
it("an already-expired key is highlighted", () => {
expect(isExpiringSoon(daysFromNow(-3), NOW)).toBe(true);
});
});
describe("isExpired (past vs future expiry)", () => {
it("an unlimited key is never expired", () => {
expect(isExpired(null, NOW)).toBe(false);
});
it("a past expiry is expired", () => {
expect(isExpired(daysFromNow(-3), NOW)).toBe(true);
expect(isExpired(daysFromNow(-1), NOW)).toBe(true);
});
it("a future expiry is not expired (even within the warning window)", () => {
expect(isExpired(daysFromNow(1), NOW)).toBe(false);
expect(isExpired(daysFromNow(EXPIRY_WARNING_DAYS - 1), NOW)).toBe(false);
expect(isExpired(daysFromNow(200), NOW)).toBe(false);
});
it("an expiry exactly at 'now' counts as expired (boundary is inclusive)", () => {
expect(isExpired(NOW.toISOString(), NOW)).toBe(true);
});
it("is mutually distinguishable from isExpiringSoon: expired vs soon-but-future", () => {
// A key 3 days in the past: expired, and (by design) also matches
// isExpiringSoon — the UI resolves this by checking isExpired first.
expect(isExpired(daysFromNow(-3), NOW)).toBe(true);
// A key 10 days in the future: NOT expired, but expiring soon.
expect(isExpired(daysFromNow(10), NOW)).toBe(false);
expect(isExpiringSoon(daysFromNow(10), NOW)).toBe(true);
});
});
describe("lastUsedBucket (within-the-last-hour semantics)", () => {
const minutesAgo = (n: number) =>
new Date(NOW.getTime() - n * 60 * 1000).toISOString();
it("null last-used is 'never'", () => {
expect(lastUsedBucket(null, NOW)).toBe("never");
});
it("under an hour is 'recent' (no sub-hour precision promised)", () => {
expect(lastUsedBucket(minutesAgo(5), NOW)).toBe("recent");
expect(lastUsedBucket(minutesAgo(59), NOW)).toBe("recent");
});
it("over an hour is 'stale'", () => {
expect(lastUsedBucket(minutesAgo(90), NOW)).toBe("stale");
});
});