@@ -84,6 +84,18 @@ jest.mock('@urbackend/common', () => {
8484 }
8585 } ,
8686 sendOtp : jest . fn ( ) . mockResolvedValue ( undefined ) ,
87+ normalizeOnboarding : jest . fn ( ( onboarding = { } ) => {
88+ const steps = onboarding . steps || { } ;
89+ return {
90+ completed : Boolean ( onboarding . completed ) ,
91+ steps : {
92+ projectCreated : Boolean ( steps . projectCreated ) ,
93+ collectionCreated : Boolean ( steps . collectionCreated ) ,
94+ firstApiCall : Boolean ( steps . firstApiCall ) ,
95+ } ,
96+ activationAt : onboarding . activationAt || null ,
97+ } ;
98+ } ) ,
8799 // Use real zod shapes so validation logic is exercised.
88100 loginSchema : z . object ( {
89101 email : z . string ( ) . email ( ) ,
@@ -108,6 +120,26 @@ jest.mock('@urbackend/common', () => {
108120 otp : z . string ( ) ,
109121 newPassword : z . string ( ) . min ( 6 ) ,
110122 } ) ,
123+ updateOnboardingSchema : z . object ( {
124+ completed : z . boolean ( ) . optional ( ) ,
125+ steps : z . object ( {
126+ projectCreated : z . boolean ( ) . optional ( ) ,
127+ collectionCreated : z . boolean ( ) . optional ( ) ,
128+ firstApiCall : z . boolean ( ) . optional ( ) ,
129+ } ) . strict ( ) . optional ( ) ,
130+ } ) . strict ( ) . refine (
131+ ( data ) => data . completed !== undefined || data . steps !== undefined ,
132+ { message : 'At least one onboarding field must be provided.' }
133+ ) ,
134+ updateDeveloperOnboarding : jest . fn ( ) . mockResolvedValue ( {
135+ completed : false ,
136+ steps : {
137+ projectCreated : true ,
138+ collectionCreated : false ,
139+ firstApiCall : false ,
140+ } ,
141+ activationAt : null ,
142+ } ) ,
111143 } ;
112144} ) ;
113145
@@ -117,7 +149,7 @@ jest.mock('@urbackend/common', () => {
117149
118150const jwt = require ( 'jsonwebtoken' ) ;
119151const bcrypt = require ( 'bcryptjs' ) ;
120- const { Developer, Otp, Project, sendOtp, AppError } = require ( '@urbackend/common' ) ;
152+ const { Developer, Otp, Project, sendOtp, AppError, updateDeveloperOnboarding } = require ( '@urbackend/common' ) ;
121153const authController = require ( '../controllers/auth.controller' ) ;
122154
123155// ---------------------------------------------------------------------------
@@ -159,22 +191,43 @@ describe('auth.controller', () => {
159191
160192 // -----------------------------------------------------------------------
161193 describe ( 'register' , ( ) => {
162- test ( 'returns 201 and success message on valid new-user registration' , async ( ) => {
194+ test ( 'returns 201 with session payload on valid new-user registration' , async ( ) => {
163195 Developer . findOne . mockResolvedValue ( null ) ;
164196 bcrypt . genSalt . mockResolvedValue ( 'salt' ) ;
165197 bcrypt . hash . mockResolvedValue ( 'hashed_password' ) ;
198+ jwt . sign . mockReturnValue ( 'signed_token' ) ;
166199
200+ const createdUser = {
201+ _id : 'new_dev_1' ,
202+ email : 'new@example.com' ,
203+ isVerified : false ,
204+ maxProjects : 1 ,
205+ password : 'hashed_password' ,
206+ save : jest . fn ( ) . mockResolvedValue ( undefined ) ,
207+ } ;
167208 const mockSave = jest . fn ( ) . mockResolvedValue ( undefined ) ;
168- Developer . mockImplementation ( ( ) => ( { save : mockSave } ) ) ;
209+ Developer . mockImplementation ( ( data ) => ( { ... createdUser , ... data , save : mockSave } ) ) ;
169210
170211 const req = makeReq ( { email : 'new@example.com' , password : 'password123' } ) ;
171212 const res = makeRes ( ) ;
172213
173214 await authController . register ( req , res , next ) ;
174215
216+
175217 expect ( Developer . findOne ) . toHaveBeenCalledWith ( { email : 'new@example.com' } ) ;
176218 expect ( res . status ) . toHaveBeenCalledWith ( 201 ) ;
177- expect ( res . json ) . toHaveBeenCalledWith ( { success : true , data : { } , message : 'Registered successfully' } ) ;
219+ expect ( res . cookie ) . toHaveBeenCalledWith ( 'accessToken' , expect . any ( String ) , expect . any ( Object ) ) ;
220+ expect ( res . cookie ) . toHaveBeenCalledWith ( 'refreshToken' , expect . any ( String ) , expect . any ( Object ) ) ;
221+ expect ( res . json ) . toHaveBeenCalledWith ( expect . objectContaining ( {
222+ success : true ,
223+ data : expect . objectContaining ( {
224+ redirectTo : '/onboarding' ,
225+ user : expect . objectContaining ( {
226+ email : 'new@example.com' ,
227+ isVerified : false ,
228+ } ) ,
229+ } ) ,
230+ } ) ) ;
178231 } ) ;
179232
180233 test ( 'returns 400 when email already exists' , async ( ) => {
@@ -202,14 +255,15 @@ describe('auth.controller', () => {
202255
203256 // -----------------------------------------------------------------------
204257 describe ( 'login' , ( ) => {
205- const mockUser = ( ) => ( {
258+ const mockUser = ( overrides = { } ) => ( {
206259 _id : 'dev_id_1' ,
207260 email : 'test@example.com' ,
208261 isVerified : true ,
209262 maxProjects : 3 ,
210263 refreshToken : null ,
211264 password : 'hashed_password' ,
212265 save : jest . fn ( ) . mockResolvedValue ( undefined ) ,
266+ ...overrides ,
213267 } ) ;
214268
215269 test ( 'returns 200 with cookie tokens on valid credentials' , async ( ) => {
@@ -232,6 +286,79 @@ describe('auth.controller', () => {
232286 ) ;
233287 } ) ;
234288
289+ test ( 'returns onboarding redirect for authenticated users with incomplete onboarding' , async ( ) => {
290+ const user = mockUser ( ) ;
291+ Developer . findOne . mockReturnValue ( Developer . __mockQuery ( user ) ) ;
292+ bcrypt . compare . mockResolvedValue ( true ) ;
293+ jwt . sign . mockReturnValue ( 'signed_token' ) ;
294+
295+ const req = makeReq ( { email : 'test@example.com' , password : 'correctpass' } ) ;
296+ const res = makeRes ( ) ;
297+
298+ await authController . login ( req , res , next ) ;
299+
300+ expect ( res . json ) . toHaveBeenCalledWith ( expect . objectContaining ( {
301+ data : expect . objectContaining ( {
302+ redirectTo : '/onboarding' ,
303+ user : expect . objectContaining ( {
304+ onboarding : expect . objectContaining ( { completed : false } ) ,
305+ } ) ,
306+ } ) ,
307+ } ) ) ;
308+ } ) ;
309+
310+ test ( 'returns onboarding redirect for unverified users' , async ( ) => {
311+ const user = mockUser ( { isVerified : false } ) ;
312+ Developer . findOne . mockReturnValue ( Developer . __mockQuery ( user ) ) ;
313+ bcrypt . compare . mockResolvedValue ( true ) ;
314+ jwt . sign . mockReturnValue ( 'signed_token' ) ;
315+
316+ const req = makeReq ( { email : 'test@example.com' , password : 'correctpass' } ) ;
317+ const res = makeRes ( ) ;
318+
319+ await authController . login ( req , res , next ) ;
320+
321+ expect ( res . json ) . toHaveBeenCalledWith ( expect . objectContaining ( {
322+ data : expect . objectContaining ( {
323+ redirectTo : '/onboarding' ,
324+ user : expect . objectContaining ( {
325+ isVerified : false ,
326+ } ) ,
327+ } ) ,
328+ } ) ) ;
329+ } ) ;
330+
331+ test ( 'returns dashboard redirect for authenticated users with completed onboarding' , async ( ) => {
332+ const user = mockUser ( {
333+ onboarding : {
334+ completed : true ,
335+ steps : {
336+ projectCreated : true ,
337+ collectionCreated : true ,
338+ firstApiCall : true ,
339+ } ,
340+ activationAt : new Date ( '2026-06-08T00:00:00.000Z' ) ,
341+ } ,
342+ } ) ;
343+ Developer . findOne . mockReturnValue ( Developer . __mockQuery ( user ) ) ;
344+ bcrypt . compare . mockResolvedValue ( true ) ;
345+ jwt . sign . mockReturnValue ( 'signed_token' ) ;
346+
347+ const req = makeReq ( { email : 'test@example.com' , password : 'correctpass' } ) ;
348+ const res = makeRes ( ) ;
349+
350+ await authController . login ( req , res , next ) ;
351+
352+ expect ( res . json ) . toHaveBeenCalledWith ( expect . objectContaining ( {
353+ data : expect . objectContaining ( {
354+ redirectTo : '/dashboard' ,
355+ user : expect . objectContaining ( {
356+ onboarding : expect . objectContaining ( { completed : true } ) ,
357+ } ) ,
358+ } ) ,
359+ } ) ) ;
360+ } ) ;
361+
235362 test ( 'returns 400 when user is not found' , async ( ) => {
236363 Developer . findOne . mockReturnValue ( Developer . __mockQuery ( null ) ) ;
237364
@@ -395,6 +522,37 @@ describe('auth.controller', () => {
395522 ) ;
396523 } ) ;
397524
525+ test ( 'returns default onboarding state for users without stored onboarding' , async ( ) => {
526+ const mockSelect = jest . fn ( ) . mockResolvedValue ( {
527+ _id : 'dev_id_1' ,
528+ email : 'test@example.com' ,
529+ } ) ;
530+ Developer . findById . mockReturnValue ( { select : mockSelect } ) ;
531+
532+ const req = makeReq ( { } , { _id : 'dev_id_1' } ) ;
533+ const res = makeRes ( ) ;
534+
535+ await authController . getMe ( req , res , next ) ;
536+
537+ expect ( res . json ) . toHaveBeenCalledWith ( {
538+ success : true ,
539+ data : {
540+ user : expect . objectContaining ( {
541+ onboarding : {
542+ completed : false ,
543+ steps : {
544+ projectCreated : false ,
545+ collectionCreated : false ,
546+ firstApiCall : false ,
547+ } ,
548+ activationAt : null ,
549+ } ,
550+ } ) ,
551+ } ,
552+ message : 'Success' ,
553+ } ) ;
554+ } ) ;
555+
398556 test ( 'returns 404 when user does not exist' , async ( ) => {
399557 Developer . findById . mockReturnValue ( {
400558 select : jest . fn ( ) . mockResolvedValue ( null ) ,
@@ -411,6 +569,68 @@ describe('auth.controller', () => {
411569 } ) ;
412570 } ) ;
413571
572+ // -----------------------------------------------------------------------
573+ describe ( 'updateOnboarding' , ( ) => {
574+ test ( 'updates only the authenticated developer onboarding state' , async ( ) => {
575+ const req = makeReq (
576+ { completed : false , steps : { projectCreated : true } } ,
577+ { _id : 'dev_id_1' }
578+ ) ;
579+ const res = makeRes ( ) ;
580+
581+ await authController . updateOnboarding ( req , res , next ) ;
582+
583+ expect ( updateDeveloperOnboarding ) . toHaveBeenCalledWith ( 'dev_id_1' , {
584+ completed : false ,
585+ steps : { projectCreated : true } ,
586+ } ) ;
587+ expect ( res . status ) . toHaveBeenCalledWith ( 200 ) ;
588+ expect ( res . json ) . toHaveBeenCalledWith ( {
589+ success : true ,
590+ data : {
591+ onboarding : {
592+ completed : false ,
593+ steps : {
594+ projectCreated : true ,
595+ collectionCreated : false ,
596+ firstApiCall : false ,
597+ } ,
598+ activationAt : null ,
599+ } ,
600+ } ,
601+ message : 'Onboarding updated successfully' ,
602+ } ) ;
603+ } ) ;
604+
605+ test ( 'rejects activationAt updates' , async ( ) => {
606+ const req = makeReq (
607+ { activationAt : '2026-06-08T00:00:00.000Z' } ,
608+ { _id : 'dev_id_1' }
609+ ) ;
610+ const res = makeRes ( ) ;
611+
612+ await authController . updateOnboarding ( req , res , next ) ;
613+
614+ expect ( updateDeveloperOnboarding ) . not . toHaveBeenCalled ( ) ;
615+ expect ( next ) . toHaveBeenCalledWith ( expect . any ( AppError ) ) ;
616+ expect ( next . mock . calls [ 0 ] [ 0 ] . statusCode ) . toBe ( 400 ) ;
617+ } ) ;
618+
619+ test ( 'rejects malformed nested step objects' , async ( ) => {
620+ const req = makeReq (
621+ { steps : { firstApiCall : 'yes' } } ,
622+ { _id : 'dev_id_1' }
623+ ) ;
624+ const res = makeRes ( ) ;
625+
626+ await authController . updateOnboarding ( req , res , next ) ;
627+
628+ expect ( updateDeveloperOnboarding ) . not . toHaveBeenCalled ( ) ;
629+ expect ( next ) . toHaveBeenCalledWith ( expect . any ( AppError ) ) ;
630+ expect ( next . mock . calls [ 0 ] [ 0 ] . statusCode ) . toBe ( 400 ) ;
631+ } ) ;
632+ } ) ;
633+
414634 // -----------------------------------------------------------------------
415635 describe ( 'changePassword' , ( ) => {
416636 test ( 'returns 200 on successful password change' , async ( ) => {
0 commit comments