86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { MockAmapAdapter } from "../../apps/api/src/amap-adapter.js";
|
|
import { createApp } from "../../apps/api/src/app.js";
|
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
|
|
|
const roots: string[] = [];
|
|
const registrations: RegistrationService[] = [];
|
|
|
|
afterEach(() => {
|
|
for (const registration of registrations.splice(0)) registration.close();
|
|
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
|
});
|
|
|
|
function createRegistration() {
|
|
const now = Date.parse("2026-08-04T09:00:00.000Z");
|
|
const root = mkdtempSync(join(tmpdir(), "dada-wp7-04-amap-"));
|
|
roots.push(root);
|
|
const registration = new RegistrationService({
|
|
challengePepper: Buffer.alloc(32, 0xa1),
|
|
clock: () => now,
|
|
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
|
databasePath: join(root, "dada.sqlite3"),
|
|
invitePepper: Buffer.alloc(32, 0xa2),
|
|
resend: new MockResendAdapter(),
|
|
sessionPepper: Buffer.alloc(32, 0xa3),
|
|
});
|
|
registrations.push(registration);
|
|
|
|
const userId = randomUUID();
|
|
registration.database.prepare(`
|
|
INSERT INTO users (user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at)
|
|
VALUES (?, ?, 'user', 'active', 1, ?, ?)
|
|
`).run(userId, `${userId}@example.invalid`, randomUUID(), now);
|
|
registration.database.prepare("INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'Release Gate User', '@release_gate')").run(userId);
|
|
registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?)").run(userId, now);
|
|
return { registration, session: registration.issueAuthenticatedSession(userId, "user") };
|
|
}
|
|
|
|
describe("TDD-WP7-EXT-003 Amap release hard stop", () => {
|
|
it("admits the simulated 1000th request and blocks the 1001st before provider egress", async () => {
|
|
const { registration, session } = createRegistration();
|
|
registration.database.prepare(`
|
|
UPDATE external_service_usage
|
|
SET used_count = 999, service_status = 'active', pause_reason = NULL
|
|
WHERE service_id = 'amap_web_service' AND period_type = 'monthly'
|
|
`).run();
|
|
|
|
const amap = new MockAmapAdapter();
|
|
const app = await createApp({ amap, browserGate: false, networkBoundary: { allowTestPort: true }, registration });
|
|
const headers = {
|
|
cookie: `dada_session=${session.sessionToken}`,
|
|
host: "127.0.0.1:43121",
|
|
origin: "http://127.0.0.1:43121",
|
|
"x-csrf-token": registration.issueUserCsrfToken(session.sessionToken),
|
|
};
|
|
|
|
const thousandth = await app.inject({
|
|
headers,
|
|
method: "POST",
|
|
payload: { latitude: 0, longitude: 0 },
|
|
url: "/api/v1/location/reverse-geocode",
|
|
});
|
|
const thousandAndFirst = await app.inject({
|
|
headers,
|
|
method: "POST",
|
|
payload: { latitude: 0, longitude: 0 },
|
|
url: "/api/v1/location/reverse-geocode",
|
|
});
|
|
|
|
expect(thousandth.statusCode).toBe(200);
|
|
expect(thousandAndFirst.statusCode).toBe(503);
|
|
expect(amap.calls).toHaveLength(1);
|
|
expect(registration.serviceUsage.read("amap_web_service")).toEqual([
|
|
expect.objectContaining({ hardLimit: 1_000, status: "paused_quota", usedCount: 1_000 }),
|
|
]);
|
|
await app.close();
|
|
});
|
|
});
|