@@ -3,121 +3,265 @@ import { Response as MockResponse } from 'jest-express/lib/response';
33import { NextFunction as MockNext } from 'jest-express/lib/next' ;
44import { User } from '../../../../models/user' ;
55import { updateSettings } from '../../authManagement' ;
6- import { saveUser , generateToken , userResponse } from '../../helpers' ;
6+ import { saveUser , generateToken } from '../../helpers' ;
77import { createMockUser } from '../../__testUtils__' ;
88
99import { mailerService } from '../../../../utils/mail' ;
10- import { UserDocument } from '../../../../types' ;
10+ import { UpdateSettingsRequestBody , UserDocument } from '../../../../types' ;
1111
1212jest . mock ( '../../../../models/user' ) ;
1313jest . mock ( '../../../../utils/mail' ) ;
14+ jest . mock ( '../../../../views/mail' ) ;
1415jest . mock ( '../../helpers' , ( ) => ( {
15- ...jest . requireActual ( '../../helpers' ) ,
16+ ...jest . requireActual ( '../../helpers' ) , // use actual userResponse
1617 saveUser : jest . fn ( ) ,
1718 generateToken : jest . fn ( )
1819} ) ) ;
1920
20- describe ( 'user.controller > auth management' , ( ) => {
21+ describe ( 'user.controller > auth management > updateSettings (email, username, password) ' , ( ) => {
2122 let request : any ;
2223 let response : any ;
2324 let next : MockNext ;
25+ let requestBody : UpdateSettingsRequestBody ;
26+ let startingUser : Partial < UserDocument > ;
27+
2428 const fixedTime = 100000000 ;
29+ const GENERATED_TOKEN = 'new-token-1io23jijo' ;
30+
31+ const OLD_USERNAME = 'oldusername' ;
32+ const NEW_USERNAME = 'newusername' ;
33+
34+ const OLD_EMAIL = '[email protected] ' ; 35+ const NEW_EMAIL = '[email protected] ' ; 36+
37+ const OLD_PASSWORD = 'oldpassword' ;
38+ const NEW_PASSWORD = 'newpassword' ;
39+
40+ // minimum valid request body to manipulate per test
41+ // from manual testing on the account form:
42+ // both username and email are required & there is client-side validation for valid email & username-taken prior to submit
43+ const minimumValidRequest : UpdateSettingsRequestBody = {
44+ username : OLD_USERNAME ,
45+ email : OLD_EMAIL
46+ } ;
47+
48+ beforeAll ( ( ) => {
49+ jest . useFakeTimers ( ) . setSystemTime ( fixedTime ) ;
50+ } ) ;
51+
52+ afterAll ( ( ) => {
53+ jest . useRealTimers ( ) ;
54+ } ) ;
2555
2656 beforeEach ( ( ) => {
2757 request = new MockRequest ( ) ;
2858 response = new MockResponse ( ) ;
2959 next = jest . fn ( ) ;
60+
61+ startingUser = createMockUser ( {
62+ username : OLD_USERNAME ,
63+ email : OLD_EMAIL ,
64+ password : OLD_PASSWORD ,
65+ id : '123459' ,
66+ comparePassword : jest . fn ( ) . mockResolvedValue ( true )
67+ } ) ;
68+
69+ User . findById = jest . fn ( ) . mockResolvedValue ( startingUser ) ;
70+ User . EmailConfirmation = jest . fn ( ) . mockReturnValue ( { Sent : 'sent' } ) ;
71+ ( saveUser as jest . Mock ) . mockResolvedValue ( null ) ;
72+ ( generateToken as jest . Mock ) . mockResolvedValue ( GENERATED_TOKEN ) ;
73+ ( mailerService . send as jest . Mock ) . mockResolvedValue ( true ) ;
74+
75+ request . user = { id : 'valid-id' } ;
76+ request . headers . host = 'localhost:3000' ;
3077 } ) ;
3178
3279 afterEach ( ( ) => {
3380 request . resetMocked ( ) ;
3481 response . resetMocked ( ) ;
3582 jest . clearAllMocks ( ) ;
83+ jest . restoreAllMocks ( ) ;
3684 } ) ;
3785
38- describe ( 'updateSettings' , ( ) => {
39- beforeAll ( ( ) => {
40- jest . useFakeTimers ( ) . setSystemTime ( fixedTime ) ;
86+ describe ( 'if the user is not found' , ( ) => {
87+ beforeEach ( async ( ) => {
88+ ( User . findById as jest . Mock ) . mockResolvedValue ( null ) ;
89+ request . user = { id : 'nonexistent-id' } ;
90+
91+ await updateSettings ( request , response , next ) ;
4192 } ) ;
4293
43- afterAll ( ( ) => {
44- jest . useRealTimers ( ) ;
94+ it ( 'returns 404 and a user-not-found error' , async ( ) => {
95+ expect ( response . status ) . toHaveBeenCalledWith ( 404 ) ;
96+ expect ( response . json ) . toHaveBeenCalledWith ( {
97+ error : 'User not found'
98+ } ) ;
4599 } ) ;
46100
47- describe ( 'if the user is not found' , ( ) => {
48- beforeEach ( async ( ) => {
49- User . findById = jest . fn ( ) . mockResolvedValue ( null ) ;
101+ it ( 'does not save the user' , ( ) => {
102+ expect ( saveUser ) . not . toHaveBeenCalled ( ) ;
103+ } ) ;
104+ } ) ;
50105
51- request . user = { id : 'nonexistent-id' } ;
106+ // the below tests match the current logic, but logic can be improved
107+ describe ( 'if the user is found' , ( ) => {
108+ // Q: should we add check & logic that if no username or email are on the request,
109+ // we fallback to the username and/or email on the found user for safety?
110+ // not sure if anyone is hitting this api directly, so the client-side checks may not be enough
52111
53- ( saveUser as jest . Mock ) . mockResolvedValue ( null ) ;
54- ( generateToken as jest . Mock ) . mockResolvedValue ( 'token12343' ) ;
112+ // duplicate username check happens client-side before this request is made
113+ it ( 'saves the user with any username in the request' , async ( ) => {
114+ // saves with old username
115+ requestBody = { ...minimumValidRequest , username : OLD_USERNAME } ;
116+ request . setBody ( requestBody ) ;
117+ await updateSettings ( request , response , next ) ;
118+ expect ( saveUser ) . toHaveBeenCalledWith ( response , { ...startingUser } ) ;
55119
56- await updateSettings ( request , response , next ) ;
120+ // saves with new username
121+ requestBody = { ...minimumValidRequest , username : NEW_USERNAME } ;
122+ request . setBody ( requestBody ) ;
123+ await updateSettings ( request , response , next ) ;
124+ expect ( saveUser ) . toHaveBeenCalledWith ( response , {
125+ ...startingUser ,
126+ username : NEW_USERNAME
57127 } ) ;
128+ } ) ;
58129
59- it ( 'returns 404 and a user-not-found error' , async ( ) => {
60- expect ( response . status ) . toHaveBeenCalledWith ( 404 ) ;
61- expect ( response . json ) . toHaveBeenCalledWith ( {
62- error : 'User not found'
130+ // currently frontend doesn't seem to call password-change related things the below
131+ // not sure if we should update the logic to be cleaner?
132+ describe ( 'when there is a new password in the request' , ( ) => {
133+ describe ( 'and the current password is not provided' , ( ) => {
134+ beforeEach ( async ( ) => {
135+ requestBody = { ...minimumValidRequest , newPassword : NEW_PASSWORD } ;
136+ request . setBody ( requestBody ) ;
137+ await updateSettings ( request , response , next ) ;
138+ } ) ;
139+
140+ it ( 'returns 401 with a "current password not provided" message' , ( ) => {
141+ expect ( response . status ) . toHaveBeenCalledWith ( 401 ) ;
142+ expect ( response . json ) . toHaveBeenCalledWith ( {
143+ error : 'Current password is not provided.'
144+ } ) ;
145+ } ) ;
146+
147+ it ( 'does not save the user with the new password' , ( ) => {
148+ expect ( saveUser ) . not . toHaveBeenCalled ( ) ;
63149 } ) ;
64- } ) ;
65- it ( 'does not save the user' , ( ) => {
66- expect ( saveUser ) . not . toHaveBeenCalled ( ) ;
67150 } ) ;
68151 } ) ;
69152
70- // the below tests match the current logic, but logic can be improved
71- describe ( 'if the user is found' , ( ) => {
72- const startingUser = createMockUser ( {
73- username : 'oldusername' ,
74- 75- id : 'valid-id' ,
76- comparePassword : jest . fn ( ) . mockResolvedValue ( true )
77- } ) ;
153+ // this should be nested in the previous block but currently here to match existing logic as-is
154+ // NOTE: will make a PR into this branch to propose the change
155+ describe ( 'and when there is a currentPassword in the request' , ( ) => {
156+ describe ( 'and the current password does not match' , ( ) => {
157+ beforeEach ( async ( ) => {
158+ startingUser . comparePassword = jest . fn ( ) . mockResolvedValue ( false ) ;
78159
79- beforeEach ( ( ) => {
80- User . findById = jest . fn ( ) . mockResolvedValue ( startingUser ) ;
160+ requestBody = {
161+ ...minimumValidRequest ,
162+ newPassword : NEW_PASSWORD ,
163+ currentPassword : 'WRONG_PASSWORD'
164+ } ;
81165
82- request . user = { id : 'valid-id' } ;
166+ request . setBody ( requestBody ) ;
167+ await updateSettings ( request , response , next ) ;
168+ } ) ;
83169
84- ( saveUser as jest . Mock ) . mockResolvedValue ( null ) ;
85- ( generateToken as jest . Mock ) . mockResolvedValue ( 'token12343' ) ;
170+ it ( 'returns 401 with a "current password invalid" message' , ( ) => {
171+ expect ( response . status ) . toHaveBeenCalledWith ( 401 ) ;
172+ expect ( response . json ) . toHaveBeenCalledWith ( {
173+ error : 'Current password is invalid.'
174+ } ) ;
175+ } ) ;
176+ it ( 'does not save the user with the new password' , ( ) => {
177+ expect ( saveUser ) . not . toHaveBeenCalled ( ) ;
178+ } ) ;
86179 } ) ;
87180
88- describe ( 'and when there is a username in the request ' , ( ) => {
181+ describe ( 'and when the current password does match ' , ( ) => {
89182 beforeEach ( async ( ) => {
90- request . setBody ( {
91- username : 'newusername'
92- } ) ;
183+ startingUser . comparePassword = jest . fn ( ) . mockResolvedValue ( true ) ;
184+
185+ requestBody = {
186+ ...minimumValidRequest ,
187+ newPassword : NEW_PASSWORD ,
188+ currentPassword : OLD_PASSWORD
189+ } ;
190+ request . setBody ( requestBody ) ;
191+
93192 await updateSettings ( request , response , next ) ;
94193 } ) ;
95- it ( 'calls saveUser' , ( ) => {
194+ it ( 'calls saveUser with the new password ' , ( ) => {
96195 expect ( saveUser ) . toHaveBeenCalledWith ( response , {
97196 ...startingUser ,
98- username : 'newusername'
197+ password : NEW_PASSWORD
99198 } ) ;
100199 } ) ;
101200 } ) ;
102201
103- // currently frontend doesn't seem to call the below
104- describe ( 'and when there is a newPassword in the request' , ( ) => {
105- beforeEach ( async ( ) => { } ) ;
106- describe ( 'and the current password is not provided' , ( ) => {
107- it ( 'returns 401 with a "current password not provided" message' , ( ) => { } ) ;
108- it ( 'does not save the user with the new password' , ( ) => { } ) ;
202+ // NOTE: This should not pass, but it currently does!!
203+ describe ( 'and when there is no new password on the request' , ( ) => {
204+ beforeEach ( async ( ) => {
205+ startingUser . comparePassword = jest . fn ( ) . mockResolvedValue ( true ) ;
206+
207+ requestBody = {
208+ ...minimumValidRequest ,
209+ newPassword : undefined ,
210+ currentPassword : OLD_PASSWORD
211+ } ;
212+ request . setBody ( requestBody ) ;
213+
214+ await updateSettings ( request , response , next ) ;
109215 } ) ;
110- } ) ;
111- describe ( 'and when there is a currentPassword in the request' , ( ) => {
112- describe ( 'and the current password does not match' , ( ) => {
113- beforeEach ( async ( ) => { } ) ;
114- it ( 'returns 401 with a "current password invalid" message' , ( ) => { } ) ;
115- it ( 'does not save the user with the new password' , ( ) => { } ) ;
216+ it ( 'calls saveUser with the new empty password' , ( ) => {
217+ expect ( saveUser ) . toHaveBeenCalledWith ( response , {
218+ ...startingUser ,
219+ password : undefined
220+ } ) ;
116221 } ) ;
117- describe ( 'and when the current password does match' , ( ) => {
118- beforeEach ( async ( ) => { } ) ;
119- it ( 'calls saveUser with the new password' , ( ) => { } ) ;
222+ } ) ;
223+ } ) ;
224+
225+ describe ( 'and when there is an email in the request' , ( ) => {
226+ it ( 'does not send a verification email if email is unchanged' , async ( ) => {
227+ requestBody = minimumValidRequest ;
228+ request . setBody ( requestBody ) ;
229+ await updateSettings ( request , response , next ) ;
230+
231+ expect ( saveUser ) . toHaveBeenCalledWith ( response , startingUser ) ;
232+ expect ( mailerService . send ) . not . toHaveBeenCalled ( ) ;
233+ } ) ;
234+
235+ it ( 'updates email and sends verification email if email is changed' , async ( ) => {
236+ requestBody = { ...minimumValidRequest , email : NEW_EMAIL } ;
237+ request . setBody ( requestBody ) ;
238+ await updateSettings ( request , response , next ) ;
239+
240+ expect ( saveUser ) . toHaveBeenCalledWith ( response , {
241+ ...startingUser ,
242+ email : NEW_EMAIL ,
243+ verified : 'sent' ,
244+ verifiedToken : GENERATED_TOKEN
120245 } ) ;
246+
247+ expect ( mailerService . send ) . toHaveBeenCalledWith (
248+ expect . objectContaining ( {
249+ subject : 'Mock confirm your email'
250+ } )
251+ ) ;
252+ } ) ;
253+ } ) ;
254+
255+ describe ( 'and when there is any other error' , ( ) => {
256+ beforeEach ( async ( ) => {
257+ User . findById = jest . fn ( ) . mockRejectedValue ( 'db error' ) ;
258+ requestBody = minimumValidRequest ;
259+ request . setBody ( requestBody ) ;
260+ await updateSettings ( request , response , next ) ;
261+ } ) ;
262+ it ( 'returns a 500 error' , ( ) => {
263+ expect ( response . status ) . toHaveBeenCalledWith ( 500 ) ;
264+ expect ( response . json ) . toHaveBeenCalledWith ( { error : 'db error' } ) ;
121265 } ) ;
122266 } ) ;
123267 } ) ;
0 commit comments