Files
2026-07-13 13:39:12 +08:00

337 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
isKeyActive,
isExpired,
isRestricted,
classifyKeyStatus,
classifyKeyType,
computeApiKeyCounts,
} from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.js";
import type { ApiKeyShape } from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const futureDate = new Date(Date.now() + 86_400_000).toISOString(); // +1 day
const pastDate = new Date(Date.now() - 86_400_000).toISOString(); // -1 day
function makeKey(overrides: Partial<ApiKeyShape> = {}): ApiKeyShape {
return {
isActive: true,
isBanned: false,
expiresAt: null,
scopes: [],
allowedModels: null,
allowedConnections: null,
...overrides,
};
}
// ---------------------------------------------------------------------------
// Scenario 1 — isKeyActive
// ---------------------------------------------------------------------------
describe("isKeyActive", () => {
it("returns true for a fully active key", () => {
assert.equal(isKeyActive(makeKey()), true);
});
it("returns false when isBanned is true", () => {
assert.equal(isKeyActive(makeKey({ isBanned: true })), false);
});
it("returns false when isActive is false", () => {
assert.equal(isKeyActive(makeKey({ isActive: false })), false);
});
it("returns false when expiresAt is in the past", () => {
assert.equal(isKeyActive(makeKey({ expiresAt: pastDate })), false);
});
it("returns true when expiresAt is in the future", () => {
assert.equal(isKeyActive(makeKey({ expiresAt: futureDate })), true);
});
it("returns false when banned even if isActive true and not expired", () => {
assert.equal(
isKeyActive(makeKey({ isBanned: true, isActive: true, expiresAt: futureDate })),
false
);
});
});
// ---------------------------------------------------------------------------
// Scenario 2 — isExpired
// ---------------------------------------------------------------------------
describe("isExpired", () => {
it("returns false when expiresAt is null", () => {
assert.equal(isExpired(makeKey({ expiresAt: null })), false);
});
it("returns false when expiresAt is in the future", () => {
assert.equal(isExpired(makeKey({ expiresAt: futureDate })), false);
});
it("returns true when expiresAt is in the past", () => {
assert.equal(isExpired(makeKey({ expiresAt: pastDate })), true);
});
it("returns false for an invalid date string (NaN)", () => {
assert.equal(isExpired(makeKey({ expiresAt: "not-a-date" })), false);
});
});
// ---------------------------------------------------------------------------
// Scenario 3 — isRestricted
// ---------------------------------------------------------------------------
describe("isRestricted", () => {
it("returns false when both allowedModels and allowedConnections are null", () => {
assert.equal(isRestricted(makeKey()), false);
});
it("returns false when allowedModels is an empty array", () => {
assert.equal(isRestricted(makeKey({ allowedModels: [] })), false);
});
it("returns true when allowedModels has entries", () => {
assert.equal(isRestricted(makeKey({ allowedModels: ["gpt-4"] })), true);
});
it("returns true when allowedConnections has entries", () => {
assert.equal(isRestricted(makeKey({ allowedConnections: ["conn-1"] })), true);
});
it("returns true when both have entries", () => {
assert.equal(
isRestricted(makeKey({ allowedModels: ["gpt-4"], allowedConnections: ["conn-1"] })),
true
);
});
});
// ---------------------------------------------------------------------------
// Scenario 4 — classifyKeyStatus (banned takes highest priority)
// ---------------------------------------------------------------------------
describe("classifyKeyStatus", () => {
it("returns 'banned' for a banned key (highest priority)", () => {
assert.equal(classifyKeyStatus(makeKey({ isBanned: true })), "banned");
});
it("returns 'banned' even if also expired", () => {
assert.equal(classifyKeyStatus(makeKey({ isBanned: true, expiresAt: pastDate })), "banned");
});
it("returns 'expired' before 'disabled' when key has past expiry and isActive false", () => {
// expiresAt in the past AND isActive false — expired is checked before disabled
assert.equal(classifyKeyStatus(makeKey({ expiresAt: pastDate, isActive: false })), "expired");
});
it("returns 'expired' for a non-banned, expired key", () => {
assert.equal(classifyKeyStatus(makeKey({ expiresAt: pastDate })), "expired");
});
it("returns 'disabled' when isActive false and not expired/banned", () => {
assert.equal(classifyKeyStatus(makeKey({ isActive: false })), "disabled");
});
it("returns 'active' for a healthy key", () => {
assert.equal(classifyKeyStatus(makeKey()), "active");
});
it("returns 'active' for key with future expiry and no bans", () => {
assert.equal(classifyKeyStatus(makeKey({ expiresAt: futureDate })), "active");
});
});
// ---------------------------------------------------------------------------
// Scenario 4 continued — classifyKeyType
// ---------------------------------------------------------------------------
describe("classifyKeyType", () => {
it("returns 'standard' for a plain key", () => {
assert.equal(classifyKeyType(makeKey()), "standard");
});
it("returns 'manage' when scopes includes manage", () => {
assert.equal(classifyKeyType(makeKey({ scopes: ["manage"] })), "manage");
});
it("returns 'manage' even if also restricted (manage takes priority)", () => {
assert.equal(
classifyKeyType(makeKey({ scopes: ["manage"], allowedModels: ["gpt-4"] })),
"manage"
);
});
it("returns 'restricted' when has allowedModels and no manage scope", () => {
assert.equal(classifyKeyType(makeKey({ allowedModels: ["gpt-4"] })), "restricted");
});
it("returns 'restricted' when has allowedConnections and no manage scope", () => {
assert.equal(classifyKeyType(makeKey({ allowedConnections: ["c1"] })), "restricted");
});
});
// ---------------------------------------------------------------------------
// Scenario 5 — computeApiKeyCounts with 10 varied keys
// ---------------------------------------------------------------------------
describe("computeApiKeyCounts", () => {
it("returns all zeros for empty array (edge case)", () => {
const counts = computeApiKeyCounts([]);
assert.equal(counts.total, 0);
assert.equal(counts.active, 0);
assert.equal(counts.banned, 0);
assert.equal(counts.expired, 0);
assert.equal(counts.disabled, 0);
assert.equal(counts.standard, 0);
assert.equal(counts.manage, 0);
assert.equal(counts.restricted, 0);
});
it("correctly tallies 10 mixed keys", () => {
const keys: ApiKeyShape[] = [
makeKey(), // active + standard
makeKey({ expiresAt: futureDate }), // active + standard
makeKey({ scopes: ["manage"] }), // active + manage
makeKey({ allowedModels: ["gpt-4"] }), // active + restricted
makeKey({ isActive: false }), // disabled + standard
makeKey({ isBanned: true }), // banned + standard
makeKey({ expiresAt: pastDate }), // expired + standard
makeKey({ expiresAt: pastDate, allowedModels: ["gpt-4"] }), // expired + restricted (type=restricted)
makeKey({ scopes: ["manage"], allowedConnections: ["c1"] }), // active + manage (manage priority)
makeKey({ allowedConnections: ["c1"] }), // active + restricted
];
const counts = computeApiKeyCounts(keys);
assert.equal(counts.total, 10);
// statuses: active(6: keys 1,2,3,4,9,10), disabled(1), banned(1), expired(2)
assert.equal(counts.active, 6);
assert.equal(counts.disabled, 1);
assert.equal(counts.banned, 1);
assert.equal(counts.expired, 2);
// types: manage(2), restricted(3), standard(5 — active×2, disabled, banned, expired×1)
assert.equal(counts.manage, 2);
assert.equal(counts.restricted, 3);
assert.equal(counts.standard, 5);
});
});
// ---------------------------------------------------------------------------
// Scenario 6 — filteredKeys logic (inline simulation of useMemo)
// ---------------------------------------------------------------------------
function applyFilters(
keys: ApiKeyShape[],
opts: {
activeOnly?: boolean;
statusFilter?: string | null;
typeFilter?: string | null;
searchQuery?: string;
}
): ApiKeyShape[] {
let list = keys;
if (opts.activeOnly) list = list.filter(isKeyActive);
if (opts.statusFilter === "active") list = list.filter(isKeyActive);
else if (opts.statusFilter === "disabled") list = list.filter((k) => k.isActive === false);
else if (opts.statusFilter === "banned") list = list.filter((k) => k.isBanned === true);
else if (opts.statusFilter === "expired") list = list.filter(isExpired);
if (opts.typeFilter === "manage") list = list.filter((k) => k.scopes?.includes("manage"));
else if (opts.typeFilter === "restricted") list = list.filter(isRestricted);
else if (opts.typeFilter === "standard")
list = list.filter((k) => !k.scopes?.includes("manage") && !isRestricted(k));
if (opts.searchQuery?.trim()) {
const q = opts.searchQuery.toLowerCase();
// We cast to any to allow name/key fields for the filter simulation
list = list.filter(
(k) =>
((k as Record<string, unknown>)["name"] as string | undefined)?.toLowerCase().includes(q) ||
((k as Record<string, unknown>)["key"] as string | undefined)?.toLowerCase().includes(q)
);
}
return list;
}
interface TestApiKey extends ApiKeyShape {
name: string;
key: string;
}
function makeTestKey(name: string, key: string, overrides: Partial<ApiKeyShape> = {}): TestApiKey {
return { ...makeKey(overrides), name, key };
}
describe("filter composition", () => {
const testKeys: TestApiKey[] = [
makeTestKey("Alpha", "sk-alpha", {}),
makeTestKey("Beta", "sk-beta", { isBanned: true }),
makeTestKey("Gamma", "sk-gamma", { isActive: false }),
makeTestKey("Delta", "sk-delta", { expiresAt: pastDate }),
makeTestKey("Epsilon", "sk-epsilon", { scopes: ["manage"] }),
];
it("activeOnly=true excludes banned and inactive keys", () => {
const result = applyFilters(testKeys, { activeOnly: true });
const names = result.map((k) => (k as TestApiKey).name);
assert.ok(names.includes("Alpha"), "Active key should be included");
assert.ok(names.includes("Epsilon"), "Manage key should be included");
assert.ok(!names.includes("Beta"), "Banned should be excluded");
assert.ok(!names.includes("Gamma"), "Disabled should be excluded");
assert.ok(!names.includes("Delta"), "Expired should be excluded");
});
it("statusFilter='banned' returns only banned keys", () => {
const result = applyFilters(testKeys, { statusFilter: "banned" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Beta");
});
it("statusFilter='disabled' returns only disabled keys", () => {
const result = applyFilters(testKeys, { statusFilter: "disabled" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Gamma");
});
it("typeFilter='manage' returns only manage-scoped keys", () => {
const result = applyFilters(testKeys, { typeFilter: "manage" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Epsilon");
});
});
// ---------------------------------------------------------------------------
// Scenario 7 — searchQuery filters by name case-insensitively
// ---------------------------------------------------------------------------
describe("searchQuery filtering", () => {
const testKeys: TestApiKey[] = [
makeTestKey("Production Key", "sk-prod-abc123", {}),
makeTestKey("Development Key", "sk-dev-xyz789", {}),
makeTestKey("Staging", "sk-stg-qwerty", {}),
];
it("matches by name case-insensitively", () => {
const result = applyFilters(testKeys, { searchQuery: "production" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Production Key");
});
it("matches by key prefix case-insensitively", () => {
const result = applyFilters(testKeys, { searchQuery: "SK-DEV" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Development Key");
});
it("returns all keys when searchQuery is empty", () => {
const result = applyFilters(testKeys, { searchQuery: "" });
assert.equal(result.length, 3);
});
it("returns empty when no key matches", () => {
const result = applyFilters(testKeys, { searchQuery: "zzznomatch" });
assert.equal(result.length, 0);
});
it("matches multiple keys with partial substring", () => {
const result = applyFilters(testKeys, { searchQuery: "key" });
assert.equal(result.length, 2);
});
});