Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion components/auth/register-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function RegisterForm({ whitelistOnly = false }: RegisterFormProps) {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [displayName, setDisplayName] = useState("");
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
Expand All @@ -27,7 +28,7 @@ export function RegisterForm({ whitelistOnly = false }: RegisterFormProps) {
setError(null);

startTransition(async () => {
const result = await register({ email, password, displayName: displayName || undefined });
const result = await register({ email, password, confirmPassword, displayName: displayName || undefined });
if (result.success) {
if (process.env.NEXT_PUBLIC_E2E_SKIP_EMAIL_VERIFICATION === "true") {
router.push("/account");
Expand Down Expand Up @@ -114,6 +115,19 @@ export function RegisterForm({ whitelistOnly = false }: RegisterFormProps) {
</p>
</div>

<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Repeat your password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
disabled={isPending}
/>
</div>

<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? (
<>
Expand Down
29 changes: 24 additions & 5 deletions e2e/auth/register.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ test.describe("User Registration", () => {
await expect(page.getByRole("heading", { name: "Create an account" })).toBeVisible();
await expect(page.getByLabel("Display Name")).toBeVisible();
await expect(page.getByLabel("Email")).toBeVisible();
await expect(page.getByLabel("Password")).toBeVisible();
await expect(page.getByLabel("Password", { exact: true })).toBeVisible();
await expect(page.getByLabel("Confirm Password")).toBeVisible();
Comment on lines +10 to +11
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other E2E specs use the shared registerUser helper (e2e/fixtures/test-helpers.ts) which still does getByLabel("Password") and does not fill the now-required Confirm Password field. With two password-labeled inputs, this will likely become ambiguous and the submission will be blocked by HTML required validation. Update the helper (and any other E2E tests) to use getByLabel("Password", { exact: true }) and fill Confirm Password so the full E2E suite continues to pass.

Copilot uses AI. Check for mistakes.
await expect(page.getByRole("button", { name: "Create account" })).toBeVisible();
});

Expand All @@ -18,7 +19,8 @@ test.describe("User Registration", () => {

await page.getByLabel("Display Name").fill("Test User");
await page.getByLabel("Email").fill(uniqueEmail);
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByLabel("Password", { exact: true }).fill("TestPassword123!");
await page.getByLabel("Confirm Password").fill("TestPassword123!");

await page.getByRole("button", { name: "Create account" }).click();

Expand All @@ -34,7 +36,8 @@ test.describe("User Registration", () => {
await page.goto("/register");
await page.getByLabel("Display Name").fill("Test User");
await page.getByLabel("Email").fill(uniqueEmail);
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByLabel("Password", { exact: true }).fill("TestPassword123!");
await page.getByLabel("Confirm Password").fill("TestPassword123!");
await page.getByRole("button", { name: "Create account" }).click();
await expect(page).toHaveURL("/account");

Expand All @@ -50,7 +53,8 @@ test.describe("User Registration", () => {
// Try to register with same email
await page.getByLabel("Display Name").fill("Test User 2");
await page.getByLabel("Email").fill(uniqueEmail);
await page.getByLabel("Password").fill("TestPassword123!");
await page.getByLabel("Password", { exact: true }).fill("TestPassword123!");
await page.getByLabel("Confirm Password").fill("TestPassword123!");
await page.getByRole("button", { name: "Create account" }).click();

// Should show error
Expand All @@ -62,14 +66,29 @@ test.describe("User Registration", () => {

await page.getByLabel("Display Name").fill("Test User");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("short"); // Less than 8 characters
await page.getByLabel("Password", { exact: true }).fill("short"); // Less than 8 characters
await page.getByLabel("Confirm Password").fill("short");

await page.getByRole("button", { name: "Create account" }).click();

// Should show validation error
await expect(page.getByText(/at least 8 characters/i)).toBeVisible();
});

test("should show error when passwords do not match", async ({ page }) => {
await page.goto("/register");

await page.getByLabel("Display Name").fill("Test User");
await page.getByLabel("Email").fill("test-mismatch@example.com");
await page.getByLabel("Password", { exact: true }).fill("TestPassword123!");
await page.getByLabel("Confirm Password").fill("DifferentPassword123!");

await page.getByRole("button", { name: "Create account" }).click();

// Should show password mismatch error
await expect(page.getByText(/passwords do not match/i)).toBeVisible();
});

test("should have link to login page", async ({ page }) => {
await page.goto("/register");

Expand Down
63 changes: 63 additions & 0 deletions lib/validations/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test";
import { registerSchema } from "./auth";

describe("registerSchema", () => {
test("should accept valid registration with matching passwords", () => {
const result = registerSchema.safeParse({
email: "test@example.com",
password: "password123",
confirmPassword: "password123",
displayName: "Test User",
});

expect(result.success).toBe(true);
});

test("should accept valid registration without display name", () => {
const result = registerSchema.safeParse({
email: "test@example.com",
password: "password123",
confirmPassword: "password123",
});

expect(result.success).toBe(true);
});

test("should reject when passwords do not match", () => {
const result = registerSchema.safeParse({
email: "test@example.com",
password: "password123",
confirmPassword: "different456",
displayName: "Test User",
});

expect(result.success).toBe(false);
if (!result.success) {
const confirmPasswordError = result.error.issues.find(
(issue) => issue.path.includes("confirmPassword")
);
expect(confirmPasswordError).toBeDefined();
expect(confirmPasswordError!.message).toBe("Passwords do not match");
}
});

test("should reject when confirmPassword is missing", () => {
const result = registerSchema.safeParse({
email: "test@example.com",
password: "password123",
displayName: "Test User",
});

expect(result.success).toBe(false);
});

test("should reject when password is too short", () => {
const result = registerSchema.safeParse({
email: "test@example.com",
password: "short",
confirmPassword: "short",
});

expect(result.success).toBe(false);
});
});
4 changes: 4 additions & 0 deletions lib/validations/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export const displayNameSchema = z
export const registerSchema = z.object({
email: emailSchema,
password: passwordSchema,
confirmPassword: z.string(),
displayName: displayNameSchema.optional(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
Comment on lines 27 to 35
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmPassword uses z.string() without a .min(1, ...), so when the field is missing/empty the returned Zod message will be the generic "Required" (and this is surfaced to users via parsed.error.issues[0]?.message). Consider adding an explicit required message (e.g., .min(1, "Confirm password is required")) to keep error messaging consistent with emailSchema / displayNameSchema.

Copilot uses AI. Check for mistakes.

export const loginSchema = z.object({
Expand Down
Loading