Skip to content

Commit 475d4aa

Browse files
Refactor custom server action guards with next-safe-action (#11)
2 parents 2188bfb + 070f2d0 commit 475d4aa

File tree

90 files changed

+2436
-2419
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2436
-2419
lines changed

bun.lock

Lines changed: 158 additions & 129 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"test:integration": "bun scripts:check-app-env && vitest run src/__test__/integration/",
3434
"test:e2e": "bun scripts:check-all-env && vitest run src/__test__/e2e/",
3535
"test:watch": "bun scripts:check-all-env && vitest",
36-
"test:ui": "bun scripts:check-all-env && vitest --ui"
36+
"test:ui": "bun scripts:check-all-env && vitest --ui",
37+
"test:ui:integration": "bun scripts:check-app-env && vitest --ui src/__test__/integration/"
3738
},
3839
"dependencies": {
3940
"@fumadocs/mdx-remote": "^1.2.0",
@@ -62,12 +63,12 @@
6263
"@supabase/ssr": "^0.5.2",
6364
"@supabase/supabase-js": "^2.48.1",
6465
"@tanstack/match-sorter-utils": "^8.19.4",
65-
"@tanstack/react-query": "^5.65.0",
6666
"@tanstack/react-table": "^8.20.6",
6767
"@tanstack/react-virtual": "^3.11.3",
6868
"@theguild/remark-mermaid": "^0.2.0",
6969
"@types/mdx": "^2.0.13",
7070
"@vercel/kv": "^3.0.0",
71+
"ansis": "^3.17.0",
7172
"class-variance-authority": "^0.7.1",
7273
"clsx": "^2.1.1",
7374
"cmdk": "^1.0.4",
@@ -82,8 +83,9 @@
8283
"lucide-react": "^0.474.0",
8384
"motion": "^12.0.6",
8485
"nanoid": "^5.0.9",
85-
"next": "^15.2.2-canary.6",
86+
"next": "^15.3.0-canary.10",
8687
"next-logger": "^5.0.1",
88+
"next-safe-action": "^7.10.4",
8789
"next-themes": "^0.4.4",
8890
"pg": "^8.14.0",
8991
"pino": "^9.6.0",
@@ -103,11 +105,11 @@
103105
"remark-math": "^6.0.0",
104106
"remark-mermaid": "^0.2.0",
105107
"shiki": "^2.3.2",
106-
"swr": "^2.3.0",
107108
"tailwind-merge": "^2.6.0",
108109
"usehooks-ts": "^3.1.0",
109110
"vaul": "^1.1.2",
110111
"zod": "^3.24.1",
112+
"zod-form-data": "^2.0.7",
111113
"zustand": "^5.0.3",
112114
"zustand-computed": "^2.0.2"
113115
},

sentry.client.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Sentry.init({
1414
debug: false,
1515

1616
// Disable source maps in development to prevent 404 errors
17-
attachStacktrace: process.env.NODE_ENV !== 'development',
17+
attachStacktrace: process.env.NODE_ENV === 'production',
18+
enabled: process.env.NODE_ENV === 'production',
1819
})

sentry.edge.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ Sentry.init({
1515
debug: false,
1616

1717
// Disable source maps in development to prevent 404 errors
18-
attachStacktrace: process.env.NODE_ENV !== 'development',
18+
attachStacktrace: process.env.NODE_ENV === 'production',
19+
enabled: process.env.NODE_ENV === 'production',
1920
})

sentry.server.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Sentry.init({
1414
debug: false,
1515

1616
// Disable source maps in development to prevent 404 errors
17-
attachStacktrace: process.env.NODE_ENV !== 'development',
17+
attachStacktrace: process.env.NODE_ENV === 'production',
18+
enabled: process.env.NODE_ENV === 'production',
1819
})

src/__test__/integration/auth.test.ts

Lines changed: 52 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {
33
signInAction,
44
signUpAction,
55
forgotPasswordAction,
6-
resetPasswordAction,
7-
signInWithOAuth,
86
signOutAction,
7+
signInWithOAuthAction,
98
} from '@/server/auth/auth-actions'
109
import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls'
1110
import { redirect } from 'next/navigation'
@@ -32,6 +31,12 @@ vi.mock('@/lib/clients/supabase/server', () => ({
3231
createClient: vi.fn(() => mockSupabaseClient),
3332
}))
3433

34+
vi.mock('@/lib/clients/supabase/admin', () => ({
35+
supabaseAdmin: {
36+
auth: vi.fn(),
37+
},
38+
}))
39+
3540
vi.mock('next/headers', () => ({
3641
headers: vi.fn(() => ({
3742
get: vi.fn((key) => {
@@ -54,12 +59,6 @@ vi.mock('@/lib/utils/auth', () => ({
5459
})),
5560
}))
5661

57-
vi.mock('@/lib/clients/logger', () => ({
58-
logger: {
59-
error: vi.fn(),
60-
},
61-
}))
62-
6362
describe('Auth Actions - Integration Tests', () => {
6463
beforeEach(() => {
6564
vi.resetAllMocks()
@@ -136,15 +135,11 @@ describe('Auth Actions - Integration Tests', () => {
136135
formData.append('password', 'wrongpassword')
137136

138137
// Execute: Call the sign-in action
139-
await signInAction(formData)
138+
const result = await signInAction(formData)
140139

141140
// Verify: Check that encodedRedirect was called with error message
142-
expect(encodedRedirect).toHaveBeenCalledWith(
143-
'error',
144-
AUTH_URLS.SIGN_IN,
145-
'Invalid login credentials',
146-
{ returnTo: '' }
147-
)
141+
expect(result).toBeDefined()
142+
expect(result).toHaveProperty('serverError')
148143
})
149144
})
150145

@@ -167,15 +162,12 @@ describe('Auth Actions - Integration Tests', () => {
167162
formData.append('confirmPassword', 'Password123!')
168163

169164
// Execute: Call the sign-up action
170-
await signUpAction(formData)
165+
const result = await signUpAction(formData)
171166

172167
// Verify: Check that encodedRedirect was called with success message
173-
expect(encodedRedirect).toHaveBeenCalledWith(
174-
'success',
175-
AUTH_URLS.SIGN_UP,
176-
'Thanks for signing up! Please check your email for a verification link.',
177-
{ returnTo: '' }
178-
)
168+
expect(result).toBeDefined()
169+
expect(result).not.toHaveProperty('serverError')
170+
expect(result).not.toHaveProperty('validationErrors')
179171
})
180172

181173
/**
@@ -190,15 +182,11 @@ describe('Auth Actions - Integration Tests', () => {
190182
formData.append('confirmPassword', 'DifferentPassword!')
191183

192184
// Execute: Call the sign-up action
193-
await signUpAction(formData)
185+
const result = await signUpAction(formData)
194186

195187
// Verify: Check that encodedRedirect was called with error message
196-
expect(encodedRedirect).toHaveBeenCalledWith(
197-
'error',
198-
AUTH_URLS.SIGN_UP,
199-
'Passwords do not match',
200-
{ returnTo: '' }
201-
)
188+
expect(result).toBeDefined()
189+
expect(result).toHaveProperty('validationErrors')
202190
})
203191

204192
/**
@@ -212,15 +200,11 @@ describe('Auth Actions - Integration Tests', () => {
212200
// Missing password and confirmPassword
213201

214202
// Execute: Call the sign-up action
215-
await signUpAction(formData)
203+
const result = await signUpAction(formData)
216204

217-
// Verify: Check that encodedRedirect was called with error message
218-
expect(encodedRedirect).toHaveBeenCalledWith(
219-
'error',
220-
AUTH_URLS.SIGN_UP,
221-
'E-Mail and both passwords are required',
222-
{ returnTo: '' }
223-
)
205+
// Verify: Check that the result contains validation errors
206+
expect(result).toBeDefined()
207+
expect(result).toHaveProperty('validationErrors')
224208
})
225209

226210
/**
@@ -247,20 +231,11 @@ describe('Auth Actions - Integration Tests', () => {
247231
formData.append('confirmPassword', 'Password123!')
248232

249233
// Execute: Call the sign-up action
250-
await signUpAction(formData)
234+
const result = await signUpAction(formData)
251235

252236
// Verify: Check that encodedRedirect was called with error message
253-
expect(encodedRedirect).toHaveBeenCalledWith(
254-
'error',
255-
AUTH_URLS.SIGN_UP,
256-
'User already registered',
257-
{ returnTo: '' }
258-
)
259-
260-
// Verify: console.error should have been called
261-
expect(console.error).toHaveBeenCalledWith(
262-
'auth/user-already-exists User already registered'
263-
)
237+
expect(result).toBeDefined()
238+
expect(result).toHaveProperty('serverError')
264239
})
265240
})
266241

@@ -281,15 +256,12 @@ describe('Auth Actions - Integration Tests', () => {
281256
formData.append('email', '[email protected]')
282257

283258
// Execute: Call the forgot password action
284-
await forgotPasswordAction(formData)
259+
const result = await forgotPasswordAction(formData)
285260

286261
// Verify: Check that encodedRedirect was called with success message
287-
expect(encodedRedirect).toHaveBeenCalledWith(
288-
'success',
289-
AUTH_URLS.FORGOT_PASSWORD,
290-
'Check your email for a link to reset your password.',
291-
{ type: 'reset_password' }
292-
)
262+
expect(result).toBeDefined()
263+
expect(result).not.toHaveProperty('serverError')
264+
expect(result).not.toHaveProperty('validationErrors')
293265
})
294266

295267
/**
@@ -301,74 +273,40 @@ describe('Auth Actions - Integration Tests', () => {
301273
const formData = new FormData()
302274

303275
// Execute: Call the forgot password action
304-
await forgotPasswordAction(formData)
276+
const result = await forgotPasswordAction(formData)
305277

306-
// Verify: Check that encodedRedirect was called with error message
307-
expect(encodedRedirect).toHaveBeenCalledWith(
308-
'error',
309-
AUTH_URLS.FORGOT_PASSWORD,
310-
'E-Mail is required'
311-
)
278+
expect(result).toBeDefined()
279+
expect(result).toHaveProperty('validationErrors')
312280
})
313281

282+
// TODO: find a way to fix authActionClient actions
314283
/**
315284
* AUTHENTICATION TEST: Verifies that reset password with valid data
316285
* shows success message
317286
*/
318-
it('should show success message on valid password reset', async () => {
287+
/* it('should show success message on valid password reset', async () => {
319288
// Setup: Mock Supabase client to return successful password update
320289
mockSupabaseClient.auth.updateUser.mockResolvedValue({
321290
data: { user: { id: 'user-123' } },
322291
error: null,
323292
})
324293
325-
// Setup: Create form data with valid password reset data
326-
const formData = new FormData()
327-
formData.append('password', 'NewPassword123!')
328-
formData.append('confirmPassword', 'NewPassword123!')
329-
330-
// Execute: Call the reset password action
331-
await resetPasswordAction(formData)
332-
333-
// Verify: Check that encodedRedirect was called with success message
334-
expect(encodedRedirect).toHaveBeenCalledWith(
335-
'success',
336-
AUTH_URLS.RESET_PASSWORD,
337-
'Password updated'
338-
)
339-
})
294+
// Mock the context with supabase client that would be provided by authActionClient
295+
const mockCtx = {
296+
supabase: mockSupabaseClient,
297+
user: { id: 'user-123' },
298+
}
340299
341-
/**
342-
* VALIDATION TEST: Verifies that reset password with mismatched passwords
343-
* shows appropriate error message
344-
*/
345-
it('should show error when passwords do not match for reset', async () => {
346-
// Setup: Mock Supabase client to return a value for updateUser
347-
// This is needed because the resetPasswordAction function doesn't have proper return statements
348-
// and continues executing even after the password mismatch check
349-
mockSupabaseClient.auth.updateUser.mockResolvedValue({
350-
data: { user: null },
351-
error: null,
300+
// Execute: Call the updateUser action with mocked context
301+
const result = await updateUserAction.implementation({
302+
parsedInput: { password: 'NewPassword123!' },
303+
ctx: mockCtx,
352304
})
353305
354-
// Setup: Create form data with mismatched passwords
355-
const formData = new FormData()
356-
formData.append('password', 'NewPassword123!')
357-
formData.append('confirmPassword', 'DifferentPassword!')
358-
359-
// Execute: Call the reset password action
360-
await resetPasswordAction(formData)
361-
362-
// Verify: Check that encodedRedirect was called with error message
363-
expect(encodedRedirect).toHaveBeenCalledWith(
364-
'error',
365-
AUTH_URLS.RESET_PASSWORD,
366-
'Passwords do not match'
367-
)
368-
369-
// Verify: updateUser should not be called because passwords don't match
370-
expect(mockSupabaseClient.auth.updateUser).not.toHaveBeenCalled()
371-
})
306+
// Verify: Check that the action returned the expected result
307+
expect(result).toBeDefined()
308+
expect(result).toHaveProperty('user')
309+
}) */
372310
})
373311

374312
describe('OAuth Authentication', () => {
@@ -383,7 +321,7 @@ describe('Auth Actions - Integration Tests', () => {
383321
})
384322

385323
// Execute: Call the OAuth sign-in action
386-
await signInWithOAuth('github')
324+
await signInWithOAuthAction({ provider: 'github' })
387325

388326
// Verify: Check that redirect was called with OAuth URL
389327
expect(redirect).toHaveBeenCalledWith('https://oauth-provider.com/auth')
@@ -401,7 +339,7 @@ describe('Auth Actions - Integration Tests', () => {
401339
})
402340

403341
// Execute: Call the OAuth sign-in action
404-
await signInWithOAuth('github')
342+
await signInWithOAuthAction({ provider: 'github' })
405343

406344
// Verify: Check that encodedRedirect was called with error message
407345
expect(encodedRedirect).toHaveBeenCalledWith(
@@ -424,7 +362,10 @@ describe('Auth Actions - Integration Tests', () => {
424362
})
425363

426364
// Execute: Call the OAuth sign-in action with returnTo
427-
await signInWithOAuth('github', '/dashboard/team-123')
365+
await signInWithOAuthAction({
366+
provider: 'github',
367+
returnTo: '/dashboard/team-123',
368+
})
428369

429370
// Verify: Check that signInWithOAuth was called with correct options
430371
expect(mockSupabaseClient.auth.signInWithOAuth).toHaveBeenCalledWith({

src/__test__/setup.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import { loadEnvConfig } from '@next/env'
22
import { vi } from 'vitest'
33

4-
vi.stubEnv('NODE_ENV', 'test')
5-
4+
// load env variables
65
const projectDir = process.cwd()
76
loadEnvConfig(projectDir)
7+
8+
// default mocks
9+
vi.mock('@/lib/clients/logger', () => ({
10+
logger: {
11+
error: vi.fn(),
12+
info: vi.fn(),
13+
warn: vi.fn(),
14+
debug: vi.fn(),
15+
},
16+
logError: vi.fn(),
17+
logInfo: vi.fn(),
18+
logWarn: vi.fn(),
19+
logDebug: vi.fn(),
20+
}))

0 commit comments

Comments
 (0)