|
|
|
|
@@ -6,10 +6,12 @@ import { createPrismaGenerationStore } from "./generation-store.js";
|
|
|
|
|
|
|
|
|
|
test("createGenerationRequest rejects an expired active subscription and marks it expired", async () => {
|
|
|
|
|
const database = createGenerationDatabase({
|
|
|
|
|
subscription: createSubscriptionFixture({
|
|
|
|
|
subscriptions: [
|
|
|
|
|
createSubscriptionFixture({
|
|
|
|
|
status: "active",
|
|
|
|
|
currentPeriodEnd: new Date("2026-03-10T11:59:59.000Z"),
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
const store = createPrismaGenerationStore(database.client);
|
|
|
|
|
|
|
|
|
|
@@ -29,11 +31,67 @@ test("createGenerationRequest rejects an expired active subscription and marks i
|
|
|
|
|
|
|
|
|
|
assert.equal(database.calls.subscriptionUpdateMany.length, 1);
|
|
|
|
|
assert.equal(database.calls.generationRequestCreate.length, 0);
|
|
|
|
|
assert.equal(database.state.subscription?.status, "expired");
|
|
|
|
|
assert.equal(database.state.subscriptions[0]?.status, "expired");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("createGenerationRequest scopes idempotency-key reuse per user", async () => {
|
|
|
|
|
const database = createGenerationDatabase({
|
|
|
|
|
subscriptions: [
|
|
|
|
|
createSubscriptionFixture({
|
|
|
|
|
userId: "user_1",
|
|
|
|
|
status: "active",
|
|
|
|
|
currentPeriodEnd: new Date("2026-04-10T11:59:59.000Z"),
|
|
|
|
|
}),
|
|
|
|
|
createSubscriptionFixture({
|
|
|
|
|
id: "subscription_2",
|
|
|
|
|
userId: "user_2",
|
|
|
|
|
status: "active",
|
|
|
|
|
currentPeriodEnd: new Date("2026-04-10T11:59:59.000Z"),
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
generationRequests: [
|
|
|
|
|
createGenerationRequestFixture({
|
|
|
|
|
id: "request_existing",
|
|
|
|
|
userId: "user_1",
|
|
|
|
|
idempotencyKey: "shared-key",
|
|
|
|
|
}),
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
const store = createPrismaGenerationStore(database.client);
|
|
|
|
|
|
|
|
|
|
const reused = await createGenerationRequest(store, {
|
|
|
|
|
userId: "user_1",
|
|
|
|
|
mode: "text_to_image",
|
|
|
|
|
providerModel: "nano-banana",
|
|
|
|
|
prompt: "hello",
|
|
|
|
|
resolutionPreset: "1024",
|
|
|
|
|
batchSize: 1,
|
|
|
|
|
idempotencyKey: "shared-key",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(reused.reusedExistingRequest, true);
|
|
|
|
|
assert.equal(reused.request.id, "request_existing");
|
|
|
|
|
|
|
|
|
|
const created = await createGenerationRequest(store, {
|
|
|
|
|
userId: "user_2",
|
|
|
|
|
mode: "text_to_image",
|
|
|
|
|
providerModel: "nano-banana",
|
|
|
|
|
prompt: "hello",
|
|
|
|
|
resolutionPreset: "1024",
|
|
|
|
|
batchSize: 1,
|
|
|
|
|
idempotencyKey: "shared-key",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(created.reusedExistingRequest, false);
|
|
|
|
|
assert.equal(created.request.userId, "user_2");
|
|
|
|
|
assert.equal(created.request.idempotencyKey, "shared-key");
|
|
|
|
|
assert.equal(database.calls.generationRequestCreate.length, 1);
|
|
|
|
|
assert.equal(database.state.generationRequests.length, 2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function createGenerationDatabase(input: {
|
|
|
|
|
subscription: ReturnType<typeof createSubscriptionFixture> | null;
|
|
|
|
|
subscriptions: Array<ReturnType<typeof createSubscriptionFixture>>;
|
|
|
|
|
generationRequests?: Array<ReturnType<typeof createGenerationRequestFixture>>;
|
|
|
|
|
}) {
|
|
|
|
|
const calls = {
|
|
|
|
|
subscriptionUpdateMany: [] as Array<Record<string, unknown>>,
|
|
|
|
|
@@ -41,7 +99,8 @@ function createGenerationDatabase(input: {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
|
subscription: input.subscription,
|
|
|
|
|
subscriptions: [...input.subscriptions],
|
|
|
|
|
generationRequests: [...(input.generationRequests ?? [])],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const client = {
|
|
|
|
|
@@ -51,15 +110,19 @@ function createGenerationDatabase(input: {
|
|
|
|
|
}: {
|
|
|
|
|
where: { userId: string; status?: "active" };
|
|
|
|
|
}) => {
|
|
|
|
|
if (!state.subscription || state.subscription.userId !== where.userId) {
|
|
|
|
|
return null;
|
|
|
|
|
return (
|
|
|
|
|
state.subscriptions.find((subscription) => {
|
|
|
|
|
if (subscription.userId !== where.userId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (where.status && state.subscription.status !== where.status) {
|
|
|
|
|
return null;
|
|
|
|
|
if (where.status && subscription.status !== where.status) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return state.subscription;
|
|
|
|
|
return true;
|
|
|
|
|
}) ?? null
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
updateMany: async ({
|
|
|
|
|
where,
|
|
|
|
|
@@ -70,19 +133,21 @@ function createGenerationDatabase(input: {
|
|
|
|
|
}) => {
|
|
|
|
|
calls.subscriptionUpdateMany.push({ where, data });
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
state.subscription &&
|
|
|
|
|
state.subscription.id === where.id &&
|
|
|
|
|
state.subscription.status === where.status
|
|
|
|
|
) {
|
|
|
|
|
state.subscription = {
|
|
|
|
|
...state.subscription,
|
|
|
|
|
status: data.status,
|
|
|
|
|
};
|
|
|
|
|
return { count: 1 };
|
|
|
|
|
let updatedCount = 0;
|
|
|
|
|
|
|
|
|
|
state.subscriptions = state.subscriptions.map((subscription) => {
|
|
|
|
|
if (subscription.id !== where.id || subscription.status !== where.status) {
|
|
|
|
|
return subscription;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { count: 0 };
|
|
|
|
|
updatedCount += 1;
|
|
|
|
|
return {
|
|
|
|
|
...subscription,
|
|
|
|
|
status: data.status,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { count: updatedCount };
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
usageLedgerEntry: {
|
|
|
|
|
@@ -95,28 +160,37 @@ function createGenerationDatabase(input: {
|
|
|
|
|
generationRequest: {
|
|
|
|
|
create: async ({ data }: { data: Record<string, unknown> }) => {
|
|
|
|
|
calls.generationRequestCreate.push({ data });
|
|
|
|
|
return {
|
|
|
|
|
id: "request_1",
|
|
|
|
|
const request = createGenerationRequestFixture({
|
|
|
|
|
id: `request_${state.generationRequests.length + 1}`,
|
|
|
|
|
userId: data.userId as string,
|
|
|
|
|
mode: data.mode as string,
|
|
|
|
|
status: "queued",
|
|
|
|
|
providerModel: data.providerModel as string,
|
|
|
|
|
prompt: data.prompt as string,
|
|
|
|
|
sourceImageKey: null,
|
|
|
|
|
resolutionPreset: data.resolutionPreset as string,
|
|
|
|
|
batchSize: data.batchSize as number,
|
|
|
|
|
imageStrength: null,
|
|
|
|
|
idempotencyKey: null,
|
|
|
|
|
terminalErrorCode: null,
|
|
|
|
|
terminalErrorText: null,
|
|
|
|
|
requestedAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
startedAt: null,
|
|
|
|
|
completedAt: null,
|
|
|
|
|
createdAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
...(data.sourceImageKey !== undefined
|
|
|
|
|
? { sourceImageKey: data.sourceImageKey as string }
|
|
|
|
|
: {}),
|
|
|
|
|
...(data.imageStrength !== undefined
|
|
|
|
|
? { imageStrength: data.imageStrength as Prisma.Decimal }
|
|
|
|
|
: {}),
|
|
|
|
|
...(data.idempotencyKey !== undefined
|
|
|
|
|
? { idempotencyKey: data.idempotencyKey as string }
|
|
|
|
|
: {}),
|
|
|
|
|
});
|
|
|
|
|
state.generationRequests.push(request);
|
|
|
|
|
return request;
|
|
|
|
|
},
|
|
|
|
|
findFirst: async () => null,
|
|
|
|
|
findFirst: async ({
|
|
|
|
|
where,
|
|
|
|
|
}: {
|
|
|
|
|
where: { userId: string; idempotencyKey: string };
|
|
|
|
|
}) =>
|
|
|
|
|
state.generationRequests.find(
|
|
|
|
|
(request) =>
|
|
|
|
|
request.userId === where.userId &&
|
|
|
|
|
request.idempotencyKey === where.idempotencyKey,
|
|
|
|
|
) ?? null,
|
|
|
|
|
},
|
|
|
|
|
} as unknown as Parameters<typeof createPrismaGenerationStore>[0];
|
|
|
|
|
|
|
|
|
|
@@ -128,12 +202,14 @@ function createGenerationDatabase(input: {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSubscriptionFixture(input: {
|
|
|
|
|
id?: string;
|
|
|
|
|
userId?: string;
|
|
|
|
|
status: "active" | "expired" | "past_due";
|
|
|
|
|
currentPeriodEnd: Date;
|
|
|
|
|
}) {
|
|
|
|
|
return {
|
|
|
|
|
id: "subscription_1",
|
|
|
|
|
userId: "user_1",
|
|
|
|
|
id: input.id ?? "subscription_1",
|
|
|
|
|
userId: input.userId ?? "user_1",
|
|
|
|
|
planId: "plan_1",
|
|
|
|
|
status: input.status,
|
|
|
|
|
renewsManually: true,
|
|
|
|
|
@@ -156,3 +232,38 @@ function createSubscriptionFixture(input: {
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createGenerationRequestFixture(input: {
|
|
|
|
|
id: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
mode?: string;
|
|
|
|
|
status?: string;
|
|
|
|
|
providerModel?: string;
|
|
|
|
|
prompt?: string;
|
|
|
|
|
sourceImageKey?: string;
|
|
|
|
|
resolutionPreset?: string;
|
|
|
|
|
batchSize?: number;
|
|
|
|
|
imageStrength?: Prisma.Decimal;
|
|
|
|
|
idempotencyKey?: string;
|
|
|
|
|
}) {
|
|
|
|
|
return {
|
|
|
|
|
id: input.id,
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
mode: input.mode ?? "text_to_image",
|
|
|
|
|
status: input.status ?? "queued",
|
|
|
|
|
providerModel: input.providerModel ?? "nano-banana",
|
|
|
|
|
prompt: input.prompt ?? "hello",
|
|
|
|
|
sourceImageKey: input.sourceImageKey ?? null,
|
|
|
|
|
resolutionPreset: input.resolutionPreset ?? "1024",
|
|
|
|
|
batchSize: input.batchSize ?? 1,
|
|
|
|
|
imageStrength: input.imageStrength ?? null,
|
|
|
|
|
idempotencyKey: input.idempotencyKey ?? null,
|
|
|
|
|
terminalErrorCode: null,
|
|
|
|
|
terminalErrorText: null,
|
|
|
|
|
requestedAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
startedAt: null,
|
|
|
|
|
completedAt: null,
|
|
|
|
|
createdAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-10T12:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|