* feat: add signup watchlist review feature flag and handler logic - Add 'signup-watchlist-review' global feature flag - Add SIGNUP to WatchlistSource enum in Prisma schema - When flag enabled, lock new signups and add email to watchlist - Show 'account under review' message on signup page - Add i18n strings for review UI - Create seed migration for the feature flag Co-Authored-By: alex@cal.com <me@alexvanandel.com> * test: add isAccountUnderReview tests to fetchSignup test suite Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: address Cubic AI review feedback (confidence >= 9/10) - Remove 'import process from node:process' in signup-view.tsx (P0 bug in 'use client' component) - Move watchlist review check before checkoutSessionId early return in calcomSignupHandler (P1 premium bypass) - Revert selfHostedHandler to original state (out of scope per user request) - Add test mocks for FeaturesRepository and GlobalWatchlistRepository Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: remove node:process import from useFlags.ts (client-side file) Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: remove !token condition from watchlist review check Token is present in normal email-verified signups, so the !token condition was incorrectly skipping watchlist review for verified users. Co-Authored-By: alex@cal.com <me@alexvanandel.com> * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * refactor: move user lock to UserRepository.lockByEmail Co-Authored-By: alex@cal.com <me@alexvanandel.com> * refactor: use cached getFeatureRepository() instead of deprecated FeaturesRepository Co-Authored-By: alex@cal.com <me@alexvanandel.com> * refactor: remove user locking, keep only watchlist addition on signup review Co-Authored-By: alex@cal.com <me@alexvanandel.com> * feat: lock user on signup review, remove watchlist entry on unlock Co-Authored-By: alex@cal.com <me@alexvanandel.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
143 lines
3.6 KiB
TypeScript
143 lines
3.6 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
import { SIGNUP_ERROR_CODES } from "../constants";
|
|
import { fetchSignup, isUserAlreadyExistsError, hasCheckoutSession, isAccountUnderReview } from "./fetchSignup";
|
|
|
|
function createJsonResponse(json: unknown, status = 200) {
|
|
return new Response(JSON.stringify(json), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
describe("fetchSignup", () => {
|
|
let fetchSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
fetchSpy = vi.spyOn(global, "fetch");
|
|
});
|
|
|
|
afterEach(() => {
|
|
fetchSpy.mockRestore();
|
|
});
|
|
|
|
it("returns success when signup completes", async () => {
|
|
fetchSpy.mockResolvedValue(createJsonResponse({ message: "Created user" }));
|
|
|
|
const result = await fetchSignup({
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
language: "en",
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("returns error with status code when signup fails", async () => {
|
|
fetchSpy.mockResolvedValue(createJsonResponse({ message: "Username is already taken" }, 409));
|
|
|
|
const result = await fetchSignup({
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
language: "en",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(result.status).toBe(409);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("isUserAlreadyExistsError", () => {
|
|
it("returns true for 409 with user_already_exists message", () => {
|
|
const result = {
|
|
ok: false as const,
|
|
status: 409,
|
|
error: { message: SIGNUP_ERROR_CODES.USER_ALREADY_EXISTS },
|
|
};
|
|
|
|
expect(isUserAlreadyExistsError(result)).toBe(true);
|
|
});
|
|
|
|
it("returns false for other error messages", () => {
|
|
const result = {
|
|
ok: false as const,
|
|
status: 409,
|
|
error: { message: "Username is already taken" },
|
|
};
|
|
|
|
expect(isUserAlreadyExistsError(result)).toBe(false);
|
|
});
|
|
|
|
it("returns false for success responses", () => {
|
|
const result = {
|
|
ok: true as const,
|
|
data: { message: "Created user" },
|
|
};
|
|
|
|
expect(isUserAlreadyExistsError(result)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("hasCheckoutSession", () => {
|
|
it("returns true when checkoutSessionId exists", () => {
|
|
const result = {
|
|
ok: false as const,
|
|
status: 402,
|
|
error: { message: "Payment required", checkoutSessionId: "cs_123" },
|
|
};
|
|
|
|
expect(hasCheckoutSession(result)).toBe(true);
|
|
});
|
|
|
|
it("returns false when checkoutSessionId is absent", () => {
|
|
const result = {
|
|
ok: false as const,
|
|
status: 409,
|
|
error: { message: "Error" },
|
|
};
|
|
|
|
expect(hasCheckoutSession(result)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isAccountUnderReview", () => {
|
|
it("returns true when accountUnderReview is true in success response", () => {
|
|
const result = {
|
|
ok: true as const,
|
|
data: { message: "Created user", accountUnderReview: true },
|
|
};
|
|
|
|
expect(isAccountUnderReview(result)).toBe(true);
|
|
});
|
|
|
|
it("returns false when accountUnderReview is not set", () => {
|
|
const result = {
|
|
ok: true as const,
|
|
data: { message: "Created user" },
|
|
};
|
|
|
|
expect(isAccountUnderReview(result)).toBe(false);
|
|
});
|
|
|
|
it("returns false when accountUnderReview is false", () => {
|
|
const result = {
|
|
ok: true as const,
|
|
data: { message: "Created user", accountUnderReview: false },
|
|
};
|
|
|
|
expect(isAccountUnderReview(result)).toBe(false);
|
|
});
|
|
|
|
it("returns false for error responses", () => {
|
|
const result = {
|
|
ok: false as const,
|
|
status: 500,
|
|
error: { message: "Internal error" },
|
|
};
|
|
|
|
expect(isAccountUnderReview(result)).toBe(false);
|
|
});
|
|
});
|