Skip to content

Commit 431afd5

Browse files
committed
chore(cli): refactor CLI authentication flow and tests
- move handleLogin, handleLogout, and handleStatus functions to cli-functions.ts for better separation of concerns. - remove deprecated login method from AuthService and streamline the login process using loginWithCallback. - remove redundant tests.
1 parent d57f95a commit 431afd5

9 files changed

Lines changed: 377 additions & 687 deletions

File tree

packages/cli/src/cli-functions.spec.ts

Lines changed: 285 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
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';
23
import {
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';
1317
import { 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
1727
const 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

Comments
 (0)