1- import { describe , it , expect , vi , beforeEach } from 'vitest' ;
1+ /* eslint-disable @typescript-eslint/unbound-method */
2+ import { describe , it , expect , vi , beforeEach , type Mock } from 'vitest' ;
23import {
34 handleInfo ,
45 testApi ,
@@ -9,9 +10,18 @@ import {
910 cancelApplicationRun ,
1011 listRunResults ,
1112 createApplicationRun ,
13+ handleLogin ,
14+ handleLogout ,
15+ handleStatus ,
1216} from './cli-functions.js' ;
1317import { PlatformSDK , PlatformSDKHttp } from '@aignostics/sdk' ;
14- import { AuthService } from './utils/auth.js' ;
18+ import { AuthService , AuthState } from './utils/auth.js' ;
19+ import { startCallbackServer , waitForCallback } from './utils/oauth-callback-server.js' ;
20+ import crypto from 'crypto' ;
21+
22+ // Mock external dependencies
23+ vi . mock ( './utils/oauth-callback-server' ) ;
24+ vi . mock ( 'crypto' ) ;
1525
1626// Mock process.exit to prevent test runner from exiting
1727const mockExit = vi . fn ( ) ;
@@ -43,10 +53,8 @@ const mockAuthService = {
4353 getValidAccessToken : vi . fn ( ) . mockResolvedValue ( 'mock-token' ) ,
4454 loginWithCallback : vi . fn ( ) ,
4555 completeLogin : vi . fn ( ) ,
46- refreshToken : vi . fn ( ) ,
4756 logout : vi . fn ( ) ,
48- getStoredToken : vi . fn ( ) ,
49- isAuthenticated : vi . fn ( ) ,
57+ getAuthState : vi . fn ( ) ,
5058} as unknown as AuthService ;
5159
5260// Mock package.json
@@ -455,4 +463,276 @@ describe('CLI Functions Unit Tests', () => {
455463 expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
456464 } ) ;
457465 } ) ;
466+
467+ describe ( 'handleLogin' , ( ) => {
468+ // Mock console methods
469+ const mockConsole = {
470+ log : vi . fn ( ) ,
471+ error : vi . fn ( ) ,
472+ } ;
473+
474+ beforeEach ( ( ) => {
475+ vi . spyOn ( console , 'log' ) . mockImplementation ( mockConsole . log ) ;
476+ vi . spyOn ( console , 'error' ) . mockImplementation ( mockConsole . error ) ;
477+ } ) ;
478+
479+ it ( 'should complete login flow successfully' , async ( ) => {
480+ const mockServer = {
481+ address : vi . fn ( ) . mockReturnValue ( { port : 8989 } ) ,
482+ close : vi . fn ( ) ,
483+ } ;
484+ const mockAuthCode = 'test-auth-code' ;
485+ const mockCodeVerifier = 'test-code-verifier' ;
486+ const mockCodeVerifierHex = Buffer . from ( mockCodeVerifier , 'utf-8' ) . toString ( 'hex' ) ;
487+
488+ ( crypto . randomBytes as Mock ) . mockReturnValue ( Buffer . from ( mockCodeVerifier , 'utf-8' ) ) ;
489+ ( startCallbackServer as Mock ) . mockResolvedValue ( mockServer ) ;
490+ ( waitForCallback as Mock ) . mockResolvedValue ( mockAuthCode ) ;
491+ vi . mocked ( mockAuthService . loginWithCallback ) . mockResolvedValue ( '' ) ;
492+ vi . mocked ( mockAuthService . completeLogin ) . mockResolvedValue ( undefined ) ;
493+
494+ await handleLogin ( 'https://test-issuer.com' , 'test-client-id' , mockAuthService ) ;
495+
496+ // Verify the flow
497+ expect ( crypto . randomBytes ) . toHaveBeenCalledWith ( 32 ) ;
498+ expect ( startCallbackServer ) . toHaveBeenCalled ( ) ;
499+ expect ( mockAuthService . loginWithCallback ) . toHaveBeenCalledWith ( {
500+ issuerURL : 'https://test-issuer.com' ,
501+ clientID : 'test-client-id' ,
502+ redirectUri : 'http://localhost:8989' ,
503+ codeVerifier : mockCodeVerifierHex ,
504+ audience : 'https://aignostics-platform-samia' ,
505+ scope : 'openid profile email offline_access' ,
506+ } ) ;
507+ expect ( waitForCallback ) . toHaveBeenCalledWith ( mockServer ) ;
508+ expect ( mockAuthService . completeLogin ) . toHaveBeenCalledWith (
509+ {
510+ issuerURL : 'https://test-issuer.com' ,
511+ clientID : 'test-client-id' ,
512+ redirectUri : 'http://localhost:8989' ,
513+ codeVerifier : mockCodeVerifierHex ,
514+ audience : 'https://aignostics-platform-samia' ,
515+ scope : 'openid profile email offline_access' ,
516+ } ,
517+ mockAuthCode
518+ ) ;
519+ expect ( mockServer . close ) . toHaveBeenCalled ( ) ;
520+ } ) ;
521+
522+ it ( 'should handle server address as number' , async ( ) => {
523+ const mockServer = {
524+ address : vi . fn ( ) . mockReturnValue ( 8990 ) ,
525+ close : vi . fn ( ) ,
526+ } ;
527+ const mockCodeVerifier = 'test-code-verifier' ;
528+
529+ ( crypto . randomBytes as Mock ) . mockReturnValue ( {
530+ toString : vi . fn ( ) . mockReturnValue ( mockCodeVerifier ) ,
531+ } ) ;
532+ ( startCallbackServer as Mock ) . mockResolvedValue ( mockServer ) ;
533+ ( waitForCallback as Mock ) . mockResolvedValue ( 'auth-code' ) ;
534+ vi . mocked ( mockAuthService . loginWithCallback ) . mockResolvedValue ( '' ) ;
535+ vi . mocked ( mockAuthService . completeLogin ) . mockResolvedValue ( undefined ) ;
536+
537+ await handleLogin ( 'https://test-issuer.com' , 'test-client-id' , mockAuthService ) ;
538+
539+ expect ( mockAuthService . loginWithCallback ) . toHaveBeenCalledWith (
540+ expect . objectContaining ( {
541+ redirectUri : 'http://localhost:8989' , // Should fallback to 8989
542+ } )
543+ ) ;
544+ } ) ;
545+
546+ it ( 'should handle authentication errors and close server' , async ( ) => {
547+ const mockServer = {
548+ address : vi . fn ( ) . mockReturnValue ( { port : 8989 } ) ,
549+ close : vi . fn ( ) ,
550+ } ;
551+ const mockError = new Error ( 'Authentication failed' ) ;
552+
553+ ( crypto . randomBytes as Mock ) . mockReturnValue ( {
554+ toString : vi . fn ( ) . mockReturnValue ( 'test-code-verifier' ) ,
555+ } ) ;
556+ ( startCallbackServer as Mock ) . mockResolvedValue ( mockServer ) ;
557+ vi . mocked ( mockAuthService . loginWithCallback ) . mockRejectedValue ( mockError ) ;
558+
559+ await expect (
560+ handleLogin ( 'https://test-issuer.com' , 'test-client-id' , mockAuthService )
561+ ) . rejects . toThrow ( 'Authentication failed' ) ;
562+
563+ expect ( mockConsole . error ) . toHaveBeenCalledWith ( '❌ Authentication failed:' , mockError ) ;
564+ expect ( mockServer . close ) . toHaveBeenCalled ( ) ;
565+ } ) ;
566+
567+ it ( 'should handle callback wait errors and close server' , async ( ) => {
568+ const mockServer = {
569+ address : vi . fn ( ) . mockReturnValue ( { port : 8989 } ) ,
570+ close : vi . fn ( ) ,
571+ } ;
572+ const mockError = new Error ( 'Callback timeout' ) ;
573+
574+ ( crypto . randomBytes as Mock ) . mockReturnValue ( {
575+ toString : vi . fn ( ) . mockReturnValue ( 'test-code-verifier' ) ,
576+ } ) ;
577+ ( startCallbackServer as Mock ) . mockResolvedValue ( mockServer ) ;
578+ vi . mocked ( mockAuthService . loginWithCallback ) . mockResolvedValue ( '' ) ;
579+ ( waitForCallback as Mock ) . mockRejectedValue ( mockError ) ;
580+
581+ await expect (
582+ handleLogin ( 'https://test-issuer.com' , 'test-client-id' , mockAuthService )
583+ ) . rejects . toThrow ( 'Callback timeout' ) ;
584+
585+ expect ( mockConsole . error ) . toHaveBeenCalledWith ( '❌ Authentication failed:' , mockError ) ;
586+ expect ( mockServer . close ) . toHaveBeenCalled ( ) ;
587+ } ) ;
588+
589+ it ( 'should handle token exchange errors and close server' , async ( ) => {
590+ const mockServer = {
591+ address : vi . fn ( ) . mockReturnValue ( { port : 8989 } ) ,
592+ close : vi . fn ( ) ,
593+ } ;
594+ const mockError = new Error ( 'Token exchange failed' ) ;
595+
596+ ( crypto . randomBytes as Mock ) . mockReturnValue ( {
597+ toString : vi . fn ( ) . mockReturnValue ( 'test-code-verifier' ) ,
598+ } ) ;
599+ ( startCallbackServer as Mock ) . mockResolvedValue ( mockServer ) ;
600+ ( waitForCallback as Mock ) . mockResolvedValue ( 'auth-code' ) ;
601+ vi . mocked ( mockAuthService . loginWithCallback ) . mockResolvedValue ( '' ) ;
602+ vi . mocked ( mockAuthService . completeLogin ) . mockRejectedValue ( mockError ) ;
603+
604+ await expect (
605+ handleLogin ( 'https://test-issuer.com' , 'test-client-id' , mockAuthService )
606+ ) . rejects . toThrow ( 'Token exchange failed' ) ;
607+
608+ expect ( mockConsole . error ) . toHaveBeenCalledWith ( '❌ Authentication failed:' , mockError ) ;
609+ expect ( mockServer . close ) . toHaveBeenCalled ( ) ;
610+ } ) ;
611+ } ) ;
612+
613+ describe ( 'handleLogout' , ( ) => {
614+ // Mock console methods
615+ const mockConsole = {
616+ log : vi . fn ( ) ,
617+ error : vi . fn ( ) ,
618+ } ;
619+
620+ beforeEach ( ( ) => {
621+ vi . spyOn ( console , 'log' ) . mockImplementation ( mockConsole . log ) ;
622+ vi . spyOn ( console , 'error' ) . mockImplementation ( mockConsole . error ) ;
623+ } ) ;
624+
625+ it ( 'should call logout function' , async ( ) => {
626+ vi . mocked ( mockAuthService . logout ) . mockResolvedValue ( undefined ) ;
627+
628+ await handleLogout ( mockAuthService ) ;
629+
630+ expect ( mockAuthService . logout ) . toHaveBeenCalled ( ) ;
631+ } ) ;
632+
633+ it ( 'should handle logout errors' , async ( ) => {
634+ const mockError = new Error ( 'Logout failed' ) ;
635+ vi . mocked ( mockAuthService . logout ) . mockRejectedValue ( mockError ) ;
636+
637+ await expect ( handleLogout ( mockAuthService ) ) . rejects . toThrow ( 'Logout failed' ) ;
638+ } ) ;
639+ } ) ;
640+
641+ describe ( 'handleStatus' , ( ) => {
642+ // Mock console methods
643+ const mockConsole = {
644+ log : vi . fn ( ) ,
645+ error : vi . fn ( ) ,
646+ } ;
647+
648+ beforeEach ( ( ) => {
649+ vi . spyOn ( console , 'log' ) . mockImplementation ( mockConsole . log ) ;
650+ vi . spyOn ( console , 'error' ) . mockImplementation ( mockConsole . error ) ;
651+
652+ // Mock process.exit
653+ vi . spyOn ( process , 'exit' ) . mockImplementation ( ( ) => {
654+ throw new Error ( 'process.exit called' ) ;
655+ } ) ;
656+ } ) ;
657+
658+ it ( 'should display authenticated status with expiring token' , async ( ) => {
659+ const mockExpiresAt = new Date ( '2025-01-01T12:59:59.000Z' ) ;
660+ const mockStoredAt = new Date ( '2024-12-01T10:00:00.000Z' ) ;
661+
662+ const mockAuthState : AuthState = {
663+ isAuthenticated : true ,
664+ token : {
665+ type : 'Bearer' ,
666+ scope : 'openid profile email offline_access' ,
667+ expiresAt : mockExpiresAt ,
668+ storedAt : mockStoredAt ,
669+ } ,
670+ } ;
671+
672+ vi . mocked ( mockAuthService . getAuthState ) . mockResolvedValue ( mockAuthState ) ;
673+
674+ await handleStatus ( mockAuthService ) ;
675+
676+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( '✅ Authenticated' ) ;
677+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( 'Token details:' ) ;
678+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( ' - Type: Bearer' ) ;
679+ expect ( mockConsole . log ) . toHaveBeenCalledWith (
680+ ' - Scope: openid profile email offline_access'
681+ ) ;
682+ expect ( mockConsole . log ) . toHaveBeenCalledWith (
683+ ` - Expires: ${ mockExpiresAt . toLocaleString ( ) } `
684+ ) ;
685+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( ` - Stored: ${ mockStoredAt . toLocaleString ( ) } ` ) ;
686+ } ) ;
687+
688+ it ( 'should display authenticated status with non-expiring token' , async ( ) => {
689+ const mockStoredAt = new Date ( '2024-12-01T10:00:00.000Z' ) ;
690+
691+ const mockAuthState : AuthState = {
692+ isAuthenticated : true ,
693+ token : {
694+ type : 'Bearer' ,
695+ scope : 'openid profile email offline_access' ,
696+ expiresAt : undefined ,
697+ storedAt : mockStoredAt ,
698+ } ,
699+ } ;
700+
701+ vi . mocked ( mockAuthService . getAuthState ) . mockResolvedValue ( mockAuthState ) ;
702+
703+ await handleStatus ( mockAuthService ) ;
704+
705+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( '✅ Authenticated' ) ;
706+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( 'Token details:' ) ;
707+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( ' - Type: Bearer' ) ;
708+ expect ( mockConsole . log ) . toHaveBeenCalledWith (
709+ ' - Scope: openid profile email offline_access'
710+ ) ;
711+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( ' - Expires: Never' ) ;
712+ expect ( mockConsole . log ) . toHaveBeenCalledWith ( ` - Stored: ${ mockStoredAt . toLocaleString ( ) } ` ) ;
713+ } ) ;
714+
715+ it ( 'should display not authenticated status' , async ( ) => {
716+ const mockAuthState : AuthState = {
717+ isAuthenticated : false ,
718+ } ;
719+
720+ vi . mocked ( mockAuthService . getAuthState ) . mockResolvedValue ( mockAuthState ) ;
721+
722+ await handleStatus ( mockAuthService ) ;
723+
724+ expect ( mockConsole . log ) . toHaveBeenCalledWith (
725+ '❌ Not authenticated. Run "aignostics-platform login" to authenticate.'
726+ ) ;
727+ } ) ;
728+
729+ it ( 'should handle auth state check errors' , async ( ) => {
730+ const mockError = new Error ( 'Failed to check auth state' ) ;
731+ vi . mocked ( mockAuthService . getAuthState ) . mockRejectedValue ( mockError ) ;
732+
733+ await expect ( handleStatus ( mockAuthService ) ) . rejects . toThrow ( 'process.exit called' ) ;
734+
735+ expect ( mockConsole . error ) . toHaveBeenCalledWith ( '❌ Error checking status:' , mockError ) ;
736+ } ) ;
737+ } ) ;
458738} ) ;
0 commit comments