/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */ import "reflect-metadata"; import { type INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import { PrismaClient } from "@prisma/client"; import { randomUUID } from "node:crypto"; import request from "supertest"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { assertSafeTestDatabaseUrl } from "../../../../prisma/database-safety.js"; import { AppModule } from "../app.module.js"; import { TokenService } from "../auth/token.service.js"; import { configureApp } from "../main.js"; import { utc8UsageDate } from "./usage-date.js"; process.env.NODE_ENV = "test"; process.env.WEB_ORIGIN = "http://localhost:3000"; process.env.PHONE_ENCRYPTION_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; process.env.PHONE_HMAC_KEY = "test-phone-hmac-key-with-at-least-32-bytes"; process.env.VERIFICATION_CODE_HMAC_KEY = "test-code-hmac-key-with-at-least-32-bytes"; process.env.JWT_SECRET = "test-jwt-secret-with-at-least-thirty-two-bytes"; process.env.REFRESH_TOKEN_HMAC_KEY = "test-refresh-hmac-key-with-at-least-32-bytes"; const prisma = new PrismaClient(); describe("bottles with real PostgreSQL", () => { let app: INestApplication; let accountId: string; let authorization: string; beforeAll(async () => { assertSafeTestDatabaseUrl(process.env.DATABASE_URL ?? ""); await prisma.$connect(); const module = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = module.createNestApplication(); configureApp(app); await app.init(); }); beforeEach(async () => { await prisma.outboxEvent.deleteMany(); await prisma.$executeRawUnsafe(`TRUNCATE TABLE "accounts" CASCADE`); ({ accountId, authorization } = await actor("owner-device")); }); afterAll(async () => { await app?.close(); await prisma.$disconnect(); }); async function actor(deviceId: string) { const account = await prisma.account.create({ data: { phoneCiphertext: Buffer.from("cipher"), phoneHmac: randomUUID() }, }); const session = await prisma.session.create({ data: { accountId: account.id, refreshTokenHash: randomUUID(), deviceId, expiresAt: new Date(Date.now() + 60_000), tokenFamily: randomUUID(), }, }); const token = app.get(TokenService).issueAccess({ sub: account.id, session_id: session.id, device_id: deviceId, scopes: ["user"], token_version: 0, }); return { accountId: account.id, authorization: `Bearer ${token}` }; } const create = (key: string, contentText = "hello sea") => request(app.getHttpServer()) .post("/api/v1/bottles") .set("Authorization", authorization) .set("Idempotency-Key", key) .send({ contentText }); it("creates reviewing/closed bottle, task, and content-free outbox atomically", async () => { const response = await create(randomUUID(), " hello sea ").expect(201); expect(response.body.data).toMatchObject({ contentText: "hello sea", reviewStatus: "REVIEWING", poolStatus: "CLOSED", }); const bottle = await prisma.bottle.findUniqueOrThrow({ where: { id: response.body.data.id }, }); const [task, event, usage] = await Promise.all([ prisma.moderationTask.findFirstOrThrow({ where: { targetId: bottle.id }, }), prisma.outboxEvent.findFirstOrThrow({ where: { aggregateId: bottle.id }, }), prisma.dailyUsage.findFirstOrThrow({ where: { accountId } }), ]); expect(task.payloadHash).toMatch(/^[a-f0-9]{64}$/); expect(event.payload).toEqual({ bottleId: bottle.id, taskId: task.id }); expect(JSON.stringify(event.payload)).not.toContain("hello sea"); expect(usage.bottlesCreated).toBe(1); }); it("deduplicates retry without incrementing quota or creating events", async () => { const key = randomUUID(); const first = await create(key).expect(201); const retry = await create(key).expect(201); expect(retry.body.data.id).toBe(first.body.data.id); expect(await prisma.bottle.count()).toBe(1); expect(await prisma.moderationTask.count()).toBe(1); expect(await prisma.outboxEvent.count()).toBe(1); expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe(1); }); it("deduplicates concurrent retries without incrementing quota", async () => { const key = randomUUID(); const results = await Promise.all( Array.from({ length: 8 }, () => create(key, "same content")), ); expect(results.every((result) => result.status === 201)).toBe(true); expect( new Set(results.map((result) => (result.body.data as { id: string }).id)), ).toHaveLength(1); expect(await prisma.bottle.count()).toBe(1); expect(await prisma.moderationTask.count()).toBe(1); expect(await prisma.outboxEvent.count()).toBe(1); expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe(1); }); it("atomically permits exactly ten of twelve concurrent requests", async () => { const results = await Promise.all( Array.from({ length: 12 }, (_, index) => create(randomUUID(), `b${index}`), ), ); expect(results.filter((x) => x.status === 201)).toHaveLength(10); const denied = results.filter((x) => x.status === 429); expect(denied).toHaveLength(2); expect(denied.every((x) => x.body.code === "BOTTLE_DAILY_LIMIT")).toBe( true, ); expect(await prisma.bottle.count()).toBe(10); expect(await prisma.outboxEvent.count()).toBe(10); expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesCreated).toBe( 10, ); }); it("validates trimmed content and idempotency key", async () => { await create(randomUUID(), " ").expect(400); await create(randomUUID(), "x".repeat(1001)).expect(400); await request(app.getHttpServer()) .post("/api/v1/bottles") .set("Authorization", authorization) .send({ contentText: "hello" }) .expect(400); }); it("allows only owner reads and cursor-paginates own bottles", async () => { const ids: string[] = []; for (let n = 0; n < 3; n += 1) ids.push( (await create(randomUUID(), `message${n}`).expect(201)).body.data.id, ); await request(app.getHttpServer()) .get(`/api/v1/bottles/${ids[0]}`) .set("Authorization", authorization) .expect(200); const stranger = await actor("stranger-device"); await request(app.getHttpServer()) .get(`/api/v1/bottles/${ids[0]}`) .set("Authorization", stranger.authorization) .expect(404); const page1 = await request(app.getHttpServer()) .get("/api/v1/me/bottles?limit=2") .set("Authorization", authorization) .expect(200); expect(page1.body.data.items).toHaveLength(2); expect(page1.body.data.nextCursor).toEqual(expect.any(String)); const page2 = await request(app.getHttpServer()) .get(`/api/v1/me/bottles?limit=2&cursor=${page1.body.data.nextCursor}`) .set("Authorization", authorization) .expect(200); expect(page2.body.data.items).toHaveLength(1); expect(page2.body.data.nextCursor).toBeNull(); }); it("returns 400 for malformed, missing, too-long, and foreign cursors", async () => { await request(app.getHttpServer()) .get("/api/v1/me/bottles?cursor=not-a-uuid") .set("Authorization", authorization) .expect(400); await request(app.getHttpServer()) .get(`/api/v1/me/bottles?cursor=${randomUUID()}`) .set("Authorization", authorization) .expect(400); const stranger = await actor("cursor-stranger"); const foreignId = ( await request(app.getHttpServer()) .post("/api/v1/bottles") .set("Authorization", stranger.authorization) .set("Idempotency-Key", randomUUID()) .send({ contentText: "foreign" }) .expect(201) ).body.data.id; await request(app.getHttpServer()) .get(`/api/v1/me/bottles?cursor=${foreignId}`) .set("Authorization", authorization) .expect(400); }); it("rejects blank and overlong idempotency keys", async () => { await create(" ").expect(400); await create("x".repeat(129)).expect(400); }); it.each(["SUSPENSION", "BAN"] as const)( "blocks active %s sanction", async (type) => { await prisma.sanction.create({ data: { accountId, type, reason: "policy", expiresAt: new Date(Date.now() + 60_000), }, }); const denied = await create(randomUUID()).expect(403); expect(denied.body.code).toBe("ACCOUNT_SANCTIONED"); expect(await prisma.bottle.count()).toBe(0); }, ); it("blocks non-active accounts", async () => { await prisma.account.update({ where: { id: accountId }, data: { status: "SUSPENDED" }, }); await create(randomUUID()).expect(401); expect(await prisma.bottle.count()).toBe(0); }); it("blocks sanctions that have started but ignores future sanctions", async () => { await prisma.sanction.create({ data: { accountId, type: "BAN", reason: "future", startsAt: new Date(Date.now() + 60_000), }, }); await create(randomUUID()).expect(201); }); it("ignores expired and revoked sanctions", async () => { await prisma.sanction.createMany({ data: [ { accountId, type: "BAN", reason: "expired", expiresAt: new Date(Date.now() - 1), }, { accountId, type: "SUSPENSION", reason: "revoked", revokedAt: new Date(), }, ], }); await create(randomUUID()).expect(201); }); it("computes usage day across UTC+8 midnight", () => { expect(utc8UsageDate(new Date("2026-09-15T15:59:59.999Z"))).toBe( "2026-09-15", ); expect(utc8UsageDate(new Date("2026-09-15T16:00:00.000Z"))).toBe( "2026-09-16", ); }); });