Skip to content

Commit d6d5425

Browse files
committed
test: add test for password change
1 parent 9b9dbfa commit d6d5425

File tree

1 file changed

+159
-3
lines changed

1 file changed

+159
-3
lines changed

test/unit/api.profile.test.ts

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2-
import type { ModuleOptions, UserWithoutPassword } from '../../src/types'
2+
import type { ModuleOptions, UserWithoutPassword, User } from '../../src/types'
33
import type { H3Event } from 'h3'
44
import crypto from 'node:crypto'
55
import { defaultOptions } from '../../src/module'
@@ -8,20 +8,36 @@ import { defaultOptions } from '../../src/module'
88
vi.mock('h3', () => ({
99
defineEventHandler: vi.fn(handler => handler),
1010
getCookie: vi.fn(),
11-
createError: vi.fn()
11+
createError: vi.fn(),
12+
readBody: vi.fn()
1213
}))
1314

1415
vi.mock('#imports', () => ({
1516
useRuntimeConfig: vi.fn()
1617
}))
1718

1819
vi.mock('../../src/runtime/server/utils', () => ({
19-
getCurrentUserFromToken: vi.fn()
20+
getCurrentUserFromToken: vi.fn(),
21+
updateUserPassword: vi.fn()
22+
}))
23+
24+
vi.mock('bcrypt', () => ({
25+
default: {
26+
compare: vi.fn()
27+
}
28+
}))
29+
30+
vi.mock('../../src/utils', () => ({
31+
validatePassword: vi.fn(),
32+
getPasswordValidationOptions: vi.fn()
2033
}))
2134

2235
// Import the profile api endpoint after mocking
2336
const profileApiEndpoint = await import('../../src/runtime/server/api/nuxt-users/me.get')
2437

38+
// Import the password change api endpoint after mocking
39+
const passwordChangeApiEndpoint = await import('../../src/runtime/server/api/nuxt-users/password/index.patch')
40+
2541
describe('Profile API Route', () => {
2642
let testOptions: ModuleOptions
2743
let testUser: UserWithoutPassword
@@ -270,3 +286,143 @@ describe('Profile API Route', () => {
270286
expect(response.user.email).toBe('profile@example.com')
271287
})
272288
})
289+
290+
describe('Password Change API Route', () => {
291+
let testOptions: ModuleOptions
292+
let testUserWithPassword: User
293+
let mockEvent: H3Event
294+
let mockGetCookie: ReturnType<typeof vi.fn>
295+
let mockReadBody: ReturnType<typeof vi.fn>
296+
let _mockCreateError: ReturnType<typeof vi.fn>
297+
let _mockDefineEventHandler: ReturnType<typeof vi.fn>
298+
let mockUseRuntimeConfig: ReturnType<typeof vi.fn>
299+
let mockGetCurrentUserFromToken: ReturnType<typeof vi.fn>
300+
let mockUpdateUserPassword: ReturnType<typeof vi.fn>
301+
let mockBcryptCompare: ReturnType<typeof vi.fn>
302+
let mockValidatePassword: ReturnType<typeof vi.fn>
303+
let mockGetPasswordValidationOptions: ReturnType<typeof vi.fn>
304+
305+
beforeEach(async () => {
306+
vi.clearAllMocks()
307+
308+
// Get the mocked functions
309+
const h3 = await import('h3')
310+
const imports = await import('#imports')
311+
const utils = await import('../../src/runtime/server/utils')
312+
const bcrypt = await import('bcrypt')
313+
const passwordUtils = await import('../../src/utils')
314+
315+
mockGetCookie = h3.getCookie as ReturnType<typeof vi.fn>
316+
mockReadBody = h3.readBody as ReturnType<typeof vi.fn>
317+
_mockCreateError = h3.createError as ReturnType<typeof vi.fn>
318+
_mockDefineEventHandler = h3.defineEventHandler as ReturnType<typeof vi.fn>
319+
mockUseRuntimeConfig = imports.useRuntimeConfig as ReturnType<typeof vi.fn>
320+
mockGetCurrentUserFromToken = utils.getCurrentUserFromToken as ReturnType<typeof vi.fn>
321+
mockUpdateUserPassword = utils.updateUserPassword as ReturnType<typeof vi.fn>
322+
mockBcryptCompare = bcrypt.default.compare as ReturnType<typeof vi.fn>
323+
mockValidatePassword = passwordUtils.validatePassword as ReturnType<typeof vi.fn>
324+
mockGetPasswordValidationOptions = passwordUtils.getPasswordValidationOptions as ReturnType<typeof vi.fn>
325+
326+
// Mock test options
327+
testOptions = defaultOptions
328+
329+
// Mock runtime config
330+
mockUseRuntimeConfig.mockReturnValue({
331+
nuxtUsers: testOptions
332+
})
333+
334+
// Create mock test user with password (needed for password change)
335+
testUserWithPassword = {
336+
id: 1,
337+
email: 'profile@example.com',
338+
name: 'Profile Test User',
339+
role: 'user',
340+
password: '$2a$10$hashedPasswordHere',
341+
created_at: '2024-01-01T00:00:00.000Z',
342+
updated_at: '2024-01-01T00:00:00.000Z',
343+
active: true
344+
}
345+
346+
// Create mock event
347+
mockEvent = {} as H3Event
348+
349+
// Mock defineEventHandler to return the handler function
350+
_mockDefineEventHandler.mockImplementation((handler: unknown) => handler)
351+
352+
// Mock password validation options
353+
mockGetPasswordValidationOptions.mockReturnValue({
354+
minLength: 8,
355+
requireUppercase: true,
356+
requireLowercase: true,
357+
requireNumbers: true,
358+
requireSpecialChars: false,
359+
preventCommonPasswords: false
360+
})
361+
})
362+
363+
afterEach(async () => {
364+
vi.clearAllMocks()
365+
})
366+
367+
it('should allow authenticated users to change their password', async () => {
368+
const mockToken = crypto.randomBytes(64).toString('hex')
369+
const currentPassword = 'OldPassword123'
370+
const newPassword = 'NewPassword123'
371+
const requestBody = {
372+
currentPassword,
373+
newPassword,
374+
newPasswordConfirmation: newPassword
375+
}
376+
377+
// Mock getCookie to return the token
378+
mockGetCookie.mockReturnValue(mockToken)
379+
380+
// Mock getCurrentUserFromToken to return user with password (third param true)
381+
mockGetCurrentUserFromToken.mockResolvedValue(testUserWithPassword)
382+
383+
// Mock readBody to return the password change request
384+
mockReadBody.mockResolvedValue(requestBody)
385+
386+
// Mock bcrypt.compare to return true (current password matches)
387+
mockBcryptCompare.mockResolvedValue(true)
388+
389+
// Mock password validation to succeed
390+
mockValidatePassword.mockReturnValue({
391+
isValid: true,
392+
errors: []
393+
})
394+
395+
// Mock updateUserPassword to succeed
396+
mockUpdateUserPassword.mockResolvedValue(undefined)
397+
398+
// Call the handler directly
399+
const response = await passwordChangeApiEndpoint.default(mockEvent)
400+
401+
// Verify getCookie was called correctly
402+
expect(mockGetCookie).toHaveBeenCalledWith(mockEvent, 'auth_token')
403+
404+
// Verify getCurrentUserFromToken was called with includePassword = true
405+
expect(mockGetCurrentUserFromToken).toHaveBeenCalledWith(mockToken, testOptions, true)
406+
407+
// Verify readBody was called
408+
expect(mockReadBody).toHaveBeenCalledWith(mockEvent)
409+
410+
// Verify password validation was called
411+
expect(mockGetPasswordValidationOptions).toHaveBeenCalledWith(testOptions)
412+
expect(mockValidatePassword).toHaveBeenCalledWith(newPassword, expect.any(Object))
413+
414+
// Verify current password was checked
415+
expect(mockBcryptCompare).toHaveBeenCalledWith(currentPassword, testUserWithPassword.password)
416+
417+
// Verify updateUserPassword was called with correct parameters
418+
expect(mockUpdateUserPassword).toHaveBeenCalledWith(
419+
testUserWithPassword.email,
420+
newPassword,
421+
testOptions
422+
)
423+
424+
// Verify response structure
425+
expect(response).toBeDefined()
426+
expect(response.message).toBe('Password updated successfully')
427+
})
428+
})

0 commit comments

Comments
 (0)