UNPKG

niledatabase

Version:

Command line interface for Nile databases

261 lines (216 loc) 8.54 kB
import { Command } from 'commander'; import { Client, QueryResult } from 'pg'; import { TenantsCommand } from '../../commands/tenants'; import { NileAPI } from '../../lib/api'; import { ConfigManager } from '../../lib/config'; import { Auth } from '../../lib/auth'; import { GlobalOptions } from '../../lib/globalOptions'; // Mock dependencies jest.mock('pg'); jest.mock('../../lib/api'); jest.mock('../../lib/config'); jest.mock('../../lib/auth'); // Custom error class for process.exit class ProcessExitError extends Error { constructor(code: number | string | null | undefined) { super(`Process.exit called with code: ${code}`); this.name = 'ProcessExitError'; } } // Mock process.exit let mockExit: jest.SpyInstance; beforeAll(() => { mockExit = jest.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { throw new ProcessExitError(code); }); }); afterAll(() => { mockExit.mockRestore(); }); describe('Tenants Command', () => { let program: Command; let mockClient: jest.Mocked<Client>; let mockNileAPI: jest.Mocked<NileAPI>; let mockConfigManager: jest.Mocked<ConfigManager>; let mockAuth: jest.Mocked<Auth>; let getOptions: () => GlobalOptions; beforeEach(() => { // Reset mocks before each test jest.clearAllMocks(); // Setup mock pg client mockClient = { query: jest.fn(), connect: jest.fn().mockResolvedValue(undefined), end: jest.fn().mockResolvedValue(undefined), } as unknown as jest.Mocked<Client>; (Client as unknown as jest.Mock).mockImplementation(() => mockClient); // Setup mock NileAPI mockNileAPI = { getDatabaseConnection: jest.fn().mockResolvedValue({ host: 'test-host', port: 5432, database: 'test-db', user: 'test-user', password: 'test-pass' }), createDatabaseCredentials: jest.fn().mockResolvedValue({ id: 'test-user', password: 'test-pass', database: { region: 'AWS_US_WEST_2', name: 'test-db' } }) } as unknown as jest.Mocked<NileAPI>; (NileAPI as unknown as jest.Mock).mockImplementation(() => mockNileAPI); // Setup mock options getOptions = () => ({ workspace: 'test-workspace', db: 'test-database', dbHost: 'test-host', globalHost: 'test-global-host', apiKey: 'test-token' }); // Setup mock ConfigManager mockConfigManager = { getWorkspace: jest.fn().mockReturnValue('test-workspace'), getDatabase: jest.fn().mockReturnValue('test-database'), getToken: jest.fn().mockReturnValue('test-token'), getDbHost: jest.fn().mockReturnValue('test-host'), getGlobalHost: jest.fn().mockReturnValue('test-global-host'), getAuthUrl: jest.fn().mockReturnValue('test-auth-url'), getDebug: jest.fn().mockReturnValue(false) } as unknown as jest.Mocked<ConfigManager>; (ConfigManager as unknown as jest.Mock).mockImplementation(() => mockConfigManager); // Mock Auth mockAuth = { getAuthorizationToken: jest.fn().mockResolvedValue('test-token') } as unknown as jest.Mocked<Auth>; (Auth as unknown as jest.Mock).mockImplementation(() => mockAuth); // Setup program and options program = new Command(); new TenantsCommand(program, getOptions); }); afterEach(async () => { await mockClient.end(); }); describe('list command', () => { it('should list all tenants', async () => { const mockTenants = [ { id: 'tenant-1', name: 'Tenant 1' }, { id: 'tenant-2', name: 'Tenant 2' } ]; (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: mockTenants } as QueryResult); await program.parseAsync(['node', 'test', 'tenants', 'list']); expect(mockClient.query).toHaveBeenCalledWith('SELECT * FROM tenants'); expect(mockClient.end).toHaveBeenCalled(); }); it('should handle empty tenant list', async () => { (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [] }); await program.parseAsync(['node', 'test', 'tenants', 'list']); expect(mockClient.query).toHaveBeenCalled(); expect(mockClient.end).toHaveBeenCalled(); }); }); describe('create command', () => { it('should create a tenant with auto-generated ID', async () => { const mockTenant = { id: 'auto-id', name: 'New Tenant' }; (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [mockTenant] }); await program.parseAsync(['node', 'test', 'tenants', 'create', '--name', 'New Tenant']); expect(mockClient.query).toHaveBeenCalledWith( 'INSERT INTO tenants (name) VALUES ($1) RETURNING *', ['New Tenant'] ); expect(mockClient.end).toHaveBeenCalled(); }); it('should create a tenant with custom ID', async () => { const mockTenant = { id: 'custom-id', name: 'New Tenant' }; (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [mockTenant] }); await program.parseAsync([ 'node', 'test', 'tenants', 'create', '--name', 'New Tenant', '--id', 'custom-id' ]); expect(mockClient.query).toHaveBeenCalledWith( 'INSERT INTO tenants (id, name) VALUES ($1, $2) RETURNING *', ['custom-id', 'New Tenant'] ); expect(mockClient.end).toHaveBeenCalled(); }); it('should handle creation error', async () => { (mockClient.query as jest.Mock).mockRejectedValueOnce(new Error('Database error')); await expect( program.parseAsync(['node', 'test', 'tenants', 'create', '--name', 'New Tenant']) ).rejects.toThrow('Database error'); expect(mockClient.end).toHaveBeenCalled(); }); }); describe('update command', () => { it('should update tenant name', async () => { const mockTenant = { id: 'tenant-1', name: 'Updated Name' }; (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [mockTenant], rowCount: 1 }); await program.parseAsync([ 'node', 'test', 'tenants', 'update', '--id', 'tenant-1', '--new_name', 'Updated Name' ]); expect(mockClient.query).toHaveBeenCalledWith( 'UPDATE tenants SET name = $1 WHERE id = $2 RETURNING *', ['Updated Name', 'tenant-1'] ); expect(mockClient.end).toHaveBeenCalled(); }); it('should handle non-existent tenant', async () => { (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 }); await expect( program.parseAsync([ 'node', 'test', 'tenants', 'update', '--id', 'non-existent', '--new_name', 'New Name' ]) ).rejects.toThrow(`Tenant with ID 'non-existent' not found`); expect(mockClient.end).toHaveBeenCalled(); }); }); describe('delete command', () => { it('should delete tenant', async () => { const mockTenant = { id: 'tenant-1', name: 'Tenant to Delete' }; const mockQuery = mockClient.query as jest.Mock; mockQuery .mockResolvedValueOnce({ rows: [mockTenant], rowCount: 1 }) // For existence check .mockResolvedValueOnce({ rows: [mockTenant], rowCount: 1 }); // For deletion await program.parseAsync([ 'node', 'test', 'tenants', 'delete', '--id', 'tenant-1' ]); expect(mockClient.query).toHaveBeenCalledWith( 'SELECT id, name FROM tenants WHERE id = $1', ['tenant-1'] ); expect(mockClient.query).toHaveBeenCalledWith( 'DELETE FROM tenants WHERE id = $1', ['tenant-1'] ); expect(mockClient.end).toHaveBeenCalled(); }); it('should handle non-existent tenant deletion', async () => { (mockClient.query as jest.Mock).mockResolvedValueOnce({ rows: [], rowCount: 0 }); await expect( program.parseAsync([ 'node', 'test', 'tenants', 'delete', '--id', 'non-existent' ]) ).rejects.toThrow(`Tenant with ID 'non-existent' not found`); expect(mockClient.end).toHaveBeenCalled(); }); }); describe('error handling', () => { it('should handle database errors gracefully', async () => { (mockClient.query as jest.Mock).mockRejectedValueOnce(new Error('Database error')); await expect( program.parseAsync(['node', 'test', 'tenants', 'create', '--name', 'New Tenant']) ).rejects.toThrow('Database error'); expect(mockClient.end).toHaveBeenCalled(); }); }); });