import { afterEach, beforeEach, describe, expect, mock, spyOn, it } from 'bun:test' const originalConsoleLog = console.log const mockGetAccount = mock(() => Promise.resolve(null)) const mockListAccounts = mock(() => Promise.resolve([])) const mockSetCurrent = mock(() => Promise.resolve(false)) const mockRemoveAccount = mock(() => Promise.resolve(false)) const mockGetAccountPaths = mock(() => ({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' })) mock.module('../credential-manager', () => ({ WhatsAppCredentialManager: class { getAccount = mockGetAccount listAccounts = mockListAccounts setCurrent = mockSetCurrent removeAccount = mockRemoveAccount getAccountPaths = mockGetAccountPaths ensureAccountPaths = mock(() => Promise.resolve({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' })) setAccount = mock(() => Promise.resolve()) }, })) const mockConnect = mock(() => Promise.resolve()) const mockClose = mock(() => Promise.resolve()) const mockGetSocket = mock(() => null) const mockLogin = mock(function (this: unknown) { return Promise.resolve(this) }) mock.module('../client', () => ({ WhatsAppClient: class { login = mockLogin connect = mockConnect close = mockClose getSocket = mockGetSocket }, })) import { authCommand } from './auth' describe('auth commands', () => { let consoleLogSpy: ReturnType let consoleErrorSpy: ReturnType let processExitSpy: ReturnType beforeEach(() => { mockGetAccount.mockReset() mockListAccounts.mockReset() mockSetCurrent.mockReset() mockRemoveAccount.mockReset() mockGetAccountPaths.mockReset() mockConnect.mockReset() mockClose.mockReset() mockGetSocket.mockReset() mockLogin.mockReset() mockGetAccount.mockImplementation(() => Promise.resolve(null)) mockListAccounts.mockImplementation(() => Promise.resolve([])) mockSetCurrent.mockImplementation(() => Promise.resolve(false)) mockRemoveAccount.mockImplementation(() => Promise.resolve(false)) mockGetAccountPaths.mockImplementation(() => ({ account_dir: '/tmp/test', auth_dir: '/tmp/test/auth' })) mockConnect.mockImplementation(() => Promise.resolve()) mockClose.mockImplementation(() => Promise.resolve()) mockGetSocket.mockImplementation(() => null) mockLogin.mockImplementation(function (this: unknown) { return Promise.resolve(this) }) consoleLogSpy = mock((..._args: unknown[]) => {}) console.log = consoleLogSpy consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {}) processExitSpy = spyOn(process, 'exit').mockImplementation((_code?: number) => undefined as never) processExitSpy.mockClear() }) afterEach(() => { console.log = originalConsoleLog consoleErrorSpy.mockRestore() processExitSpy.mockRestore() }) // list has no throwing tests — run first to avoid Commander state corruption describe('list', () => { it('outputs empty array when no accounts', async () => { mockListAccounts.mockImplementation(() => Promise.resolve([])) await authCommand.parseAsync(['list'], { from: 'user' }) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toEqual([]) }) it('outputs accounts list with is_current flag', async () => { mockListAccounts.mockImplementation(() => Promise.resolve([ { account_id: 'plus-12025551234', phone_number: '+12025551234', name: 'Alice', created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', is_current: true, }, { account_id: 'plus-19995551234', phone_number: '+19995551234', name: null, created_at: '2024-01-02T00:00:00.000Z', updated_at: '2024-01-02T00:00:00.000Z', is_current: false, }, ]), ) await authCommand.parseAsync(['list'], { from: 'user' }) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output).toHaveLength(2) expect(output[0].account_id).toBe('plus-12025551234') expect(output[0].is_current).toBe(true) expect(output[1].account_id).toBe('plus-19995551234') expect(output[1].is_current).toBe(false) }) }) // use: success test first, then throwing test last describe('use', () => { it('switches to specified account and outputs success', async () => { mockSetCurrent.mockImplementation(() => Promise.resolve(true)) mockGetAccount.mockImplementation(() => Promise.resolve({ account_id: 'plus-12025551234', phone_number: '+12025551234', name: 'Alice', created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }), ) await authCommand.parseAsync(['use', 'plus-12025551234'], { from: 'user' }) expect(mockSetCurrent).toHaveBeenCalledWith('plus-12025551234') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.success).toBe(true) expect(output.account_id).toBe('plus-12025551234') }) it('outputs error and exits when account not found', async () => { mockSetCurrent.mockImplementation(() => Promise.resolve(false)) await authCommand.parseAsync(['use', 'nonexistent'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(1) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.error).toContain('nonexistent') }) }) // status: no-account tests first, --account tests last (avoids Commander option caching) describe('status', () => { it('outputs account info when account exists', async () => { mockGetAccount.mockImplementation(() => Promise.resolve({ account_id: 'plus-12025551234', phone_number: '+12025551234', name: 'Test User', created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }), ) await authCommand.parseAsync(['status'], { from: 'user' }) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.account_id).toBe('plus-12025551234') expect(output.phone_number).toBe('+12025551234') expect(output.name).toBe('Test User') }) it('outputs error and exits when no account configured', async () => { mockGetAccount.mockImplementation(() => Promise.resolve(null)) await authCommand.parseAsync(['status'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(1) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.error).toBeDefined() expect(output.error).toContain('No WhatsApp account configured') }) it('passes --account option to getAccount', async () => { mockGetAccount.mockImplementation((id?: string) => { if (id === 'plus-19995551234') { return Promise.resolve({ account_id: 'plus-19995551234', phone_number: '+19995551234', name: null, created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }) } return Promise.resolve(null) }) await authCommand.parseAsync(['status', '--account', 'plus-19995551234'], { from: 'user' }) expect(mockGetAccount).toHaveBeenCalledWith('plus-19995551234') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.account_id).toBe('plus-19995551234') }) it('outputs error for specific missing account', async () => { mockGetAccount.mockImplementation(() => Promise.resolve(null)) await authCommand.parseAsync(['status', '--account', 'missing-id'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(1) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.error).toContain('missing-id') }) }) // logout: no-account tests first, --account test last (avoids Commander option caching) describe('logout', () => { it('removes current account and outputs success', async () => { mockGetAccount.mockImplementation(() => Promise.resolve({ account_id: 'plus-12025551234', phone_number: '+12025551234', name: 'Alice', created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }), ) mockRemoveAccount.mockImplementation(() => Promise.resolve(true)) await authCommand.parseAsync(['logout'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.success).toBe(true) expect(output.account_id).toBe('plus-12025551234') expect(output.logged_out).toBe(true) }) it('outputs error and exits when no account configured', async () => { mockGetAccount.mockImplementation(() => Promise.resolve(null)) await authCommand.parseAsync(['logout'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(1) const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.error).toBeDefined() }) it('proceeds with local cleanup even when client connection fails', async () => { mockGetAccount.mockImplementation(() => Promise.resolve({ account_id: 'plus-12025551234', phone_number: '+12025551234', name: 'Alice', created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }), ) mockConnect.mockImplementation(() => Promise.reject(new Error('Connection failed'))) mockRemoveAccount.mockImplementation(() => Promise.resolve(true)) await authCommand.parseAsync(['logout'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockRemoveAccount).toHaveBeenCalledWith('plus-12025551234') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.success).toBe(true) }) it('removes specific account with --account flag', async () => { mockGetAccount.mockImplementation((id?: string) => { if (id === 'plus-19995551234') { return Promise.resolve({ account_id: 'plus-19995551234', phone_number: '+19995551234', name: null, created_at: '2024-01-01T00:00:00.000Z', updated_at: '2024-01-01T00:00:00.000Z', }) } return Promise.resolve(null) }) mockRemoveAccount.mockImplementation(() => Promise.resolve(true)) await authCommand.parseAsync(['logout', '--account', 'plus-19995551234'], { from: 'user' }) expect(processExitSpy).toHaveBeenCalledWith(0) expect(mockRemoveAccount).toHaveBeenCalledWith('plus-19995551234') const output = JSON.parse(consoleLogSpy.mock.calls[0][0]) expect(output.success).toBe(true) expect(output.account_id).toBe('plus-19995551234') }) }) })