Skip to content

Commit 50cbc89

Browse files
authored
fix(validation): added validation for inputs for signup & email, added tests (#438)
* add validation for signup/login fields * added validation for inputs for signup & email, added tests * acknowledged PR comments
1 parent b245053 commit 50cbc89

File tree

9 files changed

+1056
-36
lines changed

9 files changed

+1056
-36
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
6+
import { useRouter, useSearchParams } from 'next/navigation'
7+
import { beforeEach, describe, expect, it, vi } from 'vitest'
8+
import { client } from '@/lib/auth-client'
9+
import LoginPage from './login-form'
10+
11+
vi.mock('next/navigation', () => ({
12+
useRouter: vi.fn(),
13+
useSearchParams: vi.fn(),
14+
}))
15+
16+
vi.mock('@/lib/auth-client', () => ({
17+
client: {
18+
signIn: {
19+
email: vi.fn(),
20+
},
21+
emailOtp: {
22+
sendVerificationOtp: vi.fn(),
23+
},
24+
},
25+
}))
26+
27+
vi.mock('@/app/(auth)/components/social-login-buttons', () => ({
28+
SocialLoginButtons: () => <div data-testid='social-login-buttons'>Social Login Buttons</div>,
29+
}))
30+
31+
const mockRouter = {
32+
push: vi.fn(),
33+
replace: vi.fn(),
34+
back: vi.fn(),
35+
}
36+
37+
const mockSearchParams = {
38+
get: vi.fn(),
39+
}
40+
41+
describe('LoginPage', () => {
42+
beforeEach(() => {
43+
vi.clearAllMocks()
44+
;(useRouter as any).mockReturnValue(mockRouter)
45+
;(useSearchParams as any).mockReturnValue(mockSearchParams)
46+
mockSearchParams.get.mockReturnValue(null)
47+
})
48+
49+
const defaultProps = {
50+
githubAvailable: true,
51+
googleAvailable: true,
52+
isProduction: false,
53+
}
54+
55+
describe('Basic Rendering', () => {
56+
it('should render login form with all required elements', () => {
57+
render(<LoginPage {...defaultProps} />)
58+
59+
expect(screen.getByPlaceholderText(/enter your email/i)).toBeInTheDocument()
60+
expect(screen.getByPlaceholderText(/enter your password/i)).toBeInTheDocument()
61+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument()
62+
expect(screen.getByText(/forgot password/i)).toBeInTheDocument()
63+
expect(screen.getByText(/sign up/i)).toBeInTheDocument()
64+
})
65+
66+
it('should render social login buttons', () => {
67+
render(<LoginPage {...defaultProps} />)
68+
69+
expect(screen.getByTestId('social-login-buttons')).toBeInTheDocument()
70+
})
71+
})
72+
73+
describe('Password Visibility Toggle', () => {
74+
it('should toggle password visibility when button is clicked', () => {
75+
render(<LoginPage {...defaultProps} />)
76+
77+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
78+
const toggleButton = screen.getByLabelText(/show password/i)
79+
80+
expect(passwordInput).toHaveAttribute('type', 'password')
81+
82+
fireEvent.click(toggleButton)
83+
expect(passwordInput).toHaveAttribute('type', 'text')
84+
85+
fireEvent.click(toggleButton)
86+
expect(passwordInput).toHaveAttribute('type', 'password')
87+
})
88+
})
89+
90+
describe('Form Interaction', () => {
91+
it('should allow users to type in form fields', () => {
92+
render(<LoginPage {...defaultProps} />)
93+
94+
const emailInput = screen.getByPlaceholderText(/enter your email/i)
95+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
96+
97+
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
98+
fireEvent.change(passwordInput, { target: { value: 'password123' } })
99+
100+
expect(emailInput).toHaveValue('test@example.com')
101+
expect(passwordInput).toHaveValue('password123')
102+
})
103+
104+
it('should show loading state during form submission', async () => {
105+
const mockSignIn = vi.mocked(client.signIn.email)
106+
mockSignIn.mockImplementation(
107+
() => new Promise((resolve) => resolve({ data: { user: { id: '1' } }, error: null }))
108+
)
109+
110+
render(<LoginPage {...defaultProps} />)
111+
112+
const emailInput = screen.getByPlaceholderText(/enter your email/i)
113+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
114+
const submitButton = screen.getByRole('button', { name: /sign in/i })
115+
116+
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
117+
fireEvent.change(passwordInput, { target: { value: 'password123' } })
118+
fireEvent.click(submitButton)
119+
120+
expect(screen.getByText('Signing in...')).toBeInTheDocument()
121+
expect(submitButton).toBeDisabled()
122+
})
123+
})
124+
125+
describe('Form Submission', () => {
126+
it('should call signIn with correct credentials', async () => {
127+
const mockSignIn = vi.mocked(client.signIn.email)
128+
mockSignIn.mockResolvedValue({ data: { user: { id: '1' } }, error: null })
129+
130+
render(<LoginPage {...defaultProps} />)
131+
132+
const emailInput = screen.getByPlaceholderText(/enter your email/i)
133+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
134+
const submitButton = screen.getByRole('button', { name: /sign in/i })
135+
136+
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
137+
fireEvent.change(passwordInput, { target: { value: 'password123' } })
138+
fireEvent.click(submitButton)
139+
140+
await waitFor(() => {
141+
expect(mockSignIn).toHaveBeenCalledWith(
142+
{
143+
email: 'test@example.com',
144+
password: 'password123',
145+
callbackURL: '/w',
146+
},
147+
expect.objectContaining({
148+
onError: expect.any(Function),
149+
})
150+
)
151+
})
152+
})
153+
154+
it('should handle authentication errors', async () => {
155+
const mockSignIn = vi.mocked(client.signIn.email)
156+
157+
mockSignIn.mockImplementation((credentials, options) => {
158+
if (options?.onError) {
159+
options.onError({
160+
error: {
161+
code: 'INVALID_CREDENTIALS',
162+
message: 'Invalid credentials',
163+
} as any,
164+
response: {} as any,
165+
request: {} as any,
166+
} as any)
167+
}
168+
return Promise.resolve({ data: null, error: 'Invalid credentials' })
169+
})
170+
171+
render(<LoginPage {...defaultProps} />)
172+
173+
const emailInput = screen.getByPlaceholderText(/enter your email/i)
174+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
175+
const submitButton = screen.getByRole('button', { name: /sign in/i })
176+
177+
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
178+
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } })
179+
fireEvent.click(submitButton)
180+
181+
await waitFor(() => {
182+
expect(screen.getByText('Invalid email or password')).toBeInTheDocument()
183+
})
184+
})
185+
})
186+
187+
describe('Forgot Password', () => {
188+
it('should open forgot password dialog', () => {
189+
render(<LoginPage {...defaultProps} />)
190+
191+
const forgotPasswordButton = screen.getByText(/forgot password/i)
192+
fireEvent.click(forgotPasswordButton)
193+
194+
expect(screen.getByText('Reset Password')).toBeInTheDocument()
195+
})
196+
})
197+
198+
describe('URL Parameters', () => {
199+
it('should handle invite flow parameter in signup link', () => {
200+
mockSearchParams.get.mockImplementation((param) => {
201+
if (param === 'invite_flow') return 'true'
202+
if (param === 'callbackUrl') return '/invite/123'
203+
return null
204+
})
205+
206+
render(<LoginPage {...defaultProps} />)
207+
208+
const signupLink = screen.getByText(/sign up/i)
209+
expect(signupLink).toHaveAttribute('href', '/signup?invite_flow=true&callbackUrl=/invite/123')
210+
})
211+
212+
it('should default to regular signup link when no invite flow', () => {
213+
render(<LoginPage {...defaultProps} />)
214+
215+
const signupLink = screen.getByText(/sign up/i)
216+
expect(signupLink).toHaveAttribute('href', '/signup')
217+
})
218+
})
219+
220+
describe('Email Verification Flow', () => {
221+
it('should redirect to verification page when email not verified', async () => {
222+
const mockSignIn = vi.mocked(client.signIn.email)
223+
const mockSendOtp = vi.mocked(client.emailOtp.sendVerificationOtp)
224+
225+
mockSignIn.mockRejectedValue({
226+
message: 'Email not verified',
227+
code: 'EMAIL_NOT_VERIFIED',
228+
})
229+
230+
mockSendOtp.mockResolvedValue({ data: null, error: null })
231+
232+
render(<LoginPage {...defaultProps} />)
233+
234+
const emailInput = screen.getByPlaceholderText(/enter your email/i)
235+
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
236+
const submitButton = screen.getByRole('button', { name: /sign in/i })
237+
238+
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
239+
fireEvent.change(passwordInput, { target: { value: 'password123' } })
240+
fireEvent.click(submitButton)
241+
242+
await waitFor(() => {
243+
expect(mockSendOtp).toHaveBeenCalledWith({
244+
email: 'test@example.com',
245+
type: 'email-verification',
246+
})
247+
expect(mockRouter.push).toHaveBeenCalledWith('/verify')
248+
})
249+
})
250+
})
251+
})

0 commit comments

Comments
 (0)