fix: 改进候选随机性与并发配额稳定性
This commit is contained in:
@@ -10,6 +10,7 @@ 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 { CandidateRepository } from "./candidate.repository.js";
|
||||
|
||||
process.env.NODE_ENV = "test";
|
||||
process.env.WEB_ORIGIN = "http://localhost:3000";
|
||||
@@ -122,6 +123,49 @@ describe("match API with real PostgreSQL", () => {
|
||||
.set("Authorization", authorization)
|
||||
.send({ leaseId, token });
|
||||
|
||||
it("requires authentication for both match endpoints", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post("/api/v1/bottles/pick")
|
||||
.set("Idempotency-Key", randomUUID())
|
||||
.send({})
|
||||
.expect(401);
|
||||
await request(app.getHttpServer())
|
||||
.post(`/api/v1/bottles/${randomUUID()}/return`)
|
||||
.send({ leaseId: randomUUID(), token: "x".repeat(43) })
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it("uses a bounded UUID keyset pivot so every region beyond the oldest 32 is reachable", async () => {
|
||||
const author = await actor("author-device");
|
||||
const picker = await actor("picker-device");
|
||||
const ids = Array.from({ length: 96 }, (_, index) => {
|
||||
const prefix = index.toString(16).padStart(8, "0");
|
||||
return `${prefix}-0000-4000-8000-000000000000`;
|
||||
});
|
||||
await prisma.bottle.createMany({
|
||||
data: ids.map((id, index) => ({
|
||||
id,
|
||||
authorId: author.id,
|
||||
clientRequestId: `pivot-${index}`,
|
||||
contentText: `pivot-${index}`,
|
||||
reviewStatus: "APPROVED" as const,
|
||||
poolStatus: "IN_POOL" as const,
|
||||
approvedAt: new Date(),
|
||||
})),
|
||||
});
|
||||
const repository = app.get(CandidateRepository);
|
||||
const front = await repository.findBatch(prisma, picker.id, 32, ids[0]);
|
||||
const middle = await repository.findBatch(prisma, picker.id, 32, ids[40]);
|
||||
const back = await repository.findBatch(prisma, picker.id, 32, ids[88]);
|
||||
|
||||
expect(front).toHaveLength(32);
|
||||
expect(middle[0]?.id).toBe(ids[40]);
|
||||
expect(back.map(({ id }) => id)).toContain(ids[95]);
|
||||
expect(back.map(({ id }) => id)).toContain(ids[0]);
|
||||
expect(new Set(back.map(({ id }) => id)).size).toBe(back.length);
|
||||
expect(middle.map(({ id }) => id)).not.toEqual(front.map(({ id }) => id));
|
||||
});
|
||||
|
||||
it("picks an approved in-pool bottle and exposes only an anonymous public snapshot", async () => {
|
||||
const author = await actor("author-device");
|
||||
const picker = await actor("picker-device");
|
||||
@@ -191,6 +235,34 @@ describe("match API with real PostgreSQL", () => {
|
||||
poolStatus: "IN_POOL",
|
||||
},
|
||||
});
|
||||
const closed = await bottle(validAuthor.id, "closed");
|
||||
await prisma.bottle.update({
|
||||
where: { id: closed.id },
|
||||
data: { poolStatus: "CLOSED" },
|
||||
});
|
||||
const leased = await bottle(validAuthor.id, "leased");
|
||||
await prisma.bottle.update({
|
||||
where: { id: leased.id },
|
||||
data: { poolStatus: "LEASED" },
|
||||
});
|
||||
const activeLease = await bottle(validAuthor.id, "active lease id");
|
||||
const leaseId = randomUUID();
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.bottle.update({
|
||||
where: { id: activeLease.id },
|
||||
data: { activeLeaseId: leaseId },
|
||||
});
|
||||
await tx.bottlePickLease.create({
|
||||
data: {
|
||||
id: leaseId,
|
||||
bottleId: activeLease.id,
|
||||
pickerId: picker.id,
|
||||
leaseTokenHash: randomUUID(),
|
||||
leaseTokenCiphertext: Buffer.from("ciphertext"),
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
},
|
||||
});
|
||||
});
|
||||
const picked = await pick(picker.authorization).expect(201);
|
||||
expect(picked.body.data.bottle.id).toBe(valid.id);
|
||||
expect(picked.body.data.bottle.id).not.toBe(own.id);
|
||||
@@ -251,6 +323,37 @@ describe("match API with real PostgreSQL", () => {
|
||||
expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesPicked).toBe(1);
|
||||
});
|
||||
|
||||
it.each(["RETURNED", "EXPIRED"] as const)(
|
||||
"returns the original pick response when the idempotent lease is %s",
|
||||
async (status) => {
|
||||
const author = await actor("author-device");
|
||||
const picker = await actor("picker-device");
|
||||
const source = await bottle(author.id);
|
||||
const key = randomUUID();
|
||||
const first = await pick(picker.authorization, key).expect(201);
|
||||
if (status === "RETURNED") {
|
||||
await returnBottle(
|
||||
picker.authorization,
|
||||
source.id,
|
||||
first.body.data.lease.id,
|
||||
first.body.data.lease.token,
|
||||
).expect(201);
|
||||
} else {
|
||||
await prisma.bottlePickLease.update({
|
||||
where: { id: first.body.data.lease.id },
|
||||
data: { status: "EXPIRED", endedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
const retry = await pick(picker.authorization, key).expect(201);
|
||||
expect(retry.body.data).toEqual(first.body.data);
|
||||
expect(await prisma.bottlePickLease.count()).toBe(1);
|
||||
expect((await prisma.dailyUsage.findFirstOrThrow()).bottlesPicked).toBe(
|
||||
1,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("issues independent random opaque tokens for different requests", async () => {
|
||||
const author = await actor("author-device");
|
||||
const picker = await actor("picker-device");
|
||||
@@ -322,9 +425,16 @@ describe("match API with real PostgreSQL", () => {
|
||||
it("enforces twenty successful picks atomically and resets on another UTC+8 day", async () => {
|
||||
const author = await actor("author-device");
|
||||
const picker = await actor("picker-device");
|
||||
await Promise.all(
|
||||
Array.from({ length: 22 }, (_, i) => bottle(author.id, `bottle-${i}`)),
|
||||
);
|
||||
await prisma.bottle.createMany({
|
||||
data: Array.from({ length: 22 }, (_, i) => ({
|
||||
authorId: author.id,
|
||||
clientRequestId: randomUUID(),
|
||||
contentText: `bottle-${i}`,
|
||||
reviewStatus: "APPROVED",
|
||||
poolStatus: "IN_POOL",
|
||||
approvedAt: new Date(),
|
||||
})),
|
||||
});
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 21 }, () => pick(picker.authorization)),
|
||||
);
|
||||
@@ -339,7 +449,7 @@ describe("match API with real PostgreSQL", () => {
|
||||
data: { usageDate: new Date("2020-01-01T00:00:00.000Z") },
|
||||
});
|
||||
await pick(picker.authorization).expect(201);
|
||||
});
|
||||
}, 15_000);
|
||||
|
||||
it.each(["SUSPENSION", "BAN"] as const)(
|
||||
"rejects active %s sanction",
|
||||
|
||||
Reference in New Issue
Block a user