mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
An account that signs up through an invitation works on the inviter's billing, so handing it a trial at signup spent its only trial before it owned anything. The trial is now held back for collaborators and claimed only explicitly: a Start Free Trial button on the new-workspace and billing screens calls the new POST /api/billing/trial endpoint, which grants the once-per-account trial atomically. Nothing starts the clock as a side effect, and pure collaborators no longer see a trial-ending banner about work that is not theirs.
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { db } from '@/lib/db';
|
|
import { POST as startTrialRoute } from '@/app/api/billing/trial/route';
|
|
import { apiRequest, callRoute, readData } from '../helpers/request';
|
|
import { signedInAs, signedOut } from '../helpers/session';
|
|
import { addWorkspaceMember, createExpiredUser, createUser, seedProject } from '../factories';
|
|
|
|
const ORIGIN_HEADERS = { origin: 'http://localhost:3000' };
|
|
|
|
function startTrialRequest() {
|
|
return apiRequest('/api/billing/trial', { method: 'POST', headers: ORIGIN_HEADERS });
|
|
}
|
|
|
|
describe('POST /api/billing/trial', () => {
|
|
it('returns 401 without a session', async () => {
|
|
signedOut();
|
|
|
|
const response = await callRoute(startTrialRoute, startTrialRequest());
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
|
|
it('rejects a cross-origin request', async () => {
|
|
const response = await callRoute(
|
|
startTrialRoute,
|
|
apiRequest('/api/billing/trial', { method: 'POST', headers: { origin: 'https://evil.test' } })
|
|
);
|
|
|
|
expect(response.status).toBe(403);
|
|
});
|
|
|
|
// The whole point of the endpoint: an invited collaborator whose trial was
|
|
// deferred at signup claims it here, explicitly, and nowhere else.
|
|
it('starts the deferred trial for a collaborator who asks for it', async () => {
|
|
const host = await seedProject();
|
|
const invited = await createUser({ trialEndsAt: null, billingTrialConsumedAt: null });
|
|
await addWorkspaceMember({ workspaceId: host.workspace.id, userId: invited.id });
|
|
signedInAs(invited);
|
|
|
|
const response = await callRoute(startTrialRoute, startTrialRequest());
|
|
|
|
expect(response.status).toBe(200);
|
|
const data = await readData<{ trialEndsAt: string | null }>(response);
|
|
expect(data.trialEndsAt).not.toBeNull();
|
|
|
|
const after = await db.user.findUniqueOrThrow({ where: { id: invited.id } });
|
|
expect(after.billingTrialConsumedAt).not.toBeNull();
|
|
expect(after.trialEndsAt!.getTime()).toBeGreaterThan(Date.now());
|
|
});
|
|
|
|
// Once per account. An expired user already spent theirs; asking again must
|
|
// not reset the clock.
|
|
it('refuses a second trial to an account that already spent one', async () => {
|
|
const expired = await createExpiredUser();
|
|
signedInAs(expired);
|
|
|
|
const response = await callRoute(startTrialRoute, startTrialRequest());
|
|
|
|
expect(response.status).toBe(409);
|
|
const after = await db.user.findUniqueOrThrow({ where: { id: expired.id } });
|
|
expect(after.trialEndsAt?.getTime()).toBeLessThan(Date.now());
|
|
});
|
|
});
|