diff --git a/packages/shared/__tests__/auth-schema.test.ts b/packages/shared/__tests__/auth-schema.test.ts new file mode 100644 index 0000000..b00c3b7 --- /dev/null +++ b/packages/shared/__tests__/auth-schema.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' +import { registerSchema, clientRegisterSchema } from '../auth-schema' + +describe('registerSchema', () => { + const validBody = { + username: 'testuser', + password: 'password123', + confirmPassword: 'password123', + captchaToken: 'token-abc', + captchaText: 'abcde', + } + + it('passes with valid input', () => { + expect(registerSchema.safeParse(validBody).success).toBe(true) + }) + + it('fails on short username', () => { + const r = registerSchema.safeParse({ ...validBody, username: 'ab' }) + expect(r.success).toBe(false) + if (!r.success) { + expect(r.error.issues[0].path).toContain('username') + } + }) + + it('fails on short password', () => { + const r = registerSchema.safeParse({ ...validBody, password: '1234567' }) + expect(r.success).toBe(false) + if (!r.success) { + expect(r.error.issues[0].path).toContain('password') + } + }) + + it('fails on password mismatch', () => { + const r = registerSchema.safeParse({ + ...validBody, + confirmPassword: 'different', + }) + expect(r.success).toBe(false) + if (!r.success) { + expect(r.error.issues[0].path).toContain('confirmPassword') + } + }) + + it('fails on empty captchaText', () => { + const r = registerSchema.safeParse({ ...validBody, captchaText: '' }) + expect(r.success).toBe(false) + if (!r.success) { + expect(r.error.issues[0].path).toContain('captchaText') + } + }) + + it('fails on empty captchaToken', () => { + const r = registerSchema.safeParse({ ...validBody, captchaToken: '' }) + expect(r.success).toBe(false) + if (!r.success) { + expect(r.error.issues[0].path).toContain('captchaToken') + } + }) +}) + +describe('clientRegisterSchema', () => { + it('still requires captchaText field', () => { + const r = clientRegisterSchema.safeParse({ + username: 'testuser', + password: 'password123', + confirmPassword: 'password123', + }) + expect(r.success).toBe(false) + }) + + it('validates without captchaToken field', () => { + const r = clientRegisterSchema.safeParse({ + username: 'testuser', + password: 'password123', + confirmPassword: 'password123', + captchaText: 'abcde', + }) + expect(r.success).toBe(true) + }) +}) diff --git a/packages/shared/auth-schema.ts b/packages/shared/auth-schema.ts index d71284b..437d014 100644 --- a/packages/shared/auth-schema.ts +++ b/packages/shared/auth-schema.ts @@ -1,23 +1,38 @@ import { z } from 'zod' +const usernameField = z.string().trim().min(3, '用户名至少需要3个字符').max(30, '用户名最多30个字符') +const passwordField = z.string().min(8, '密码至少需要8个字符') +const confirmPasswordField = z.string() +const captchaTextField = z.string().min(1, '验证码不能为空') +const captchaTokenField = z.string().min(1, '验证码令牌不能为空') + +const passwordRefine = (data: { password: string; confirmPassword: string }) => + data.password === data.confirmPassword + export const registerSchema = z .object({ - username: z - .string() - .trim() - .min(3, '用户名至少需要3个字符') - .max(30, '用户名最多30个字符'), - password: z.string().min(8, '密码至少需要8个字符'), - confirmPassword: z.string(), - captchaToken: z.string().min(1, '验证码令牌不能为空'), - captchaText: z.string().min(1, '验证码不能为空'), + username: usernameField, + password: passwordField, + confirmPassword: confirmPasswordField, + captchaToken: captchaTokenField, + captchaText: captchaTextField, }) - .refine((data) => data.password === data.confirmPassword, { + .refine(passwordRefine, { message: '两次输入的密码不一致', path: ['confirmPassword'], }) -/** Client-side schema: omits captchaToken (managed by page shell) */ -export const clientRegisterSchema = registerSchema.omit({ captchaToken: true }) +/** Client-side schema: excludes captchaToken (managed by page shell) */ +export const clientRegisterSchema = z + .object({ + username: usernameField, + password: passwordField, + confirmPassword: confirmPasswordField, + captchaText: captchaTextField, + }) + .refine(passwordRefine, { + message: '两次输入的密码不一致', + path: ['confirmPassword'], + }) export type RegisterInput = z.infer