11import { 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'
33import type { H3Event } from 'h3'
44import crypto from 'node:crypto'
55import { defaultOptions } from '../../src/module'
@@ -8,20 +8,36 @@ import { defaultOptions } from '../../src/module'
88vi . 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
1415vi . mock ( '#imports' , ( ) => ( {
1516 useRuntimeConfig : vi . fn ( )
1617} ) )
1718
1819vi . 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
2336const 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+
2541describe ( '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