|
| 1 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 2 | + |
| 3 | +const selectMock = vi.fn() |
| 4 | +const multiselectMock = vi.fn() |
| 5 | +const confirmMock = vi.fn() |
| 6 | +const CANCEL = Symbol('clack.cancel') |
| 7 | +vi.mock('@clack/prompts', () => ({ |
| 8 | + select: (...a: unknown[]) => selectMock(...a), |
| 9 | + multiselect: (...a: unknown[]) => multiselectMock(...a), |
| 10 | + confirm: (...a: unknown[]) => confirmMock(...a), |
| 11 | + isCancel: (v: unknown) => v === CANCEL, |
| 12 | + log: { info: vi.fn(), success: vi.fn(), error: vi.fn() }, |
| 13 | + spinner: () => ({ start: vi.fn(), stop: vi.fn() }), |
| 14 | +})) |
| 15 | + |
| 16 | +const queryMock = vi.fn() |
| 17 | +vi.mock('pg', () => ({ |
| 18 | + default: { |
| 19 | + Client: vi.fn(() => ({ |
| 20 | + connect: vi.fn(async () => {}), |
| 21 | + query: queryMock, |
| 22 | + end: vi.fn(async () => {}), |
| 23 | + })), |
| 24 | + }, |
| 25 | +})) |
| 26 | + |
| 27 | +const { buildSchemasFromDatabase } = await import('../introspect.js') |
| 28 | +const { generateClientFromSchemas } = await import('../../utils.js') |
| 29 | + |
| 30 | +describe('build flow (introspect → pick → codegen)', () => { |
| 31 | + beforeEach(() => { |
| 32 | + selectMock.mockReset() |
| 33 | + multiselectMock.mockReset() |
| 34 | + confirmMock.mockReset() |
| 35 | + queryMock.mockResolvedValue({ |
| 36 | + rows: [ |
| 37 | + { |
| 38 | + table_name: 'users', |
| 39 | + column_name: 'email', |
| 40 | + data_type: 'text', |
| 41 | + udt_name: 'text', |
| 42 | + }, |
| 43 | + { |
| 44 | + table_name: 'orders', |
| 45 | + column_name: 'total', |
| 46 | + data_type: 'integer', |
| 47 | + udt_name: 'int4', |
| 48 | + }, |
| 49 | + ], |
| 50 | + }) |
| 51 | + }) |
| 52 | + |
| 53 | + it('walks two tables and emits a v3 client with the chosen domains', async () => { |
| 54 | + selectMock |
| 55 | + .mockResolvedValueOnce('users') |
| 56 | + .mockResolvedValueOnce('TextEq') // table 1 + email |
| 57 | + .mockResolvedValueOnce('orders') |
| 58 | + .mockResolvedValueOnce('IntegerOrd') // table 2 + total |
| 59 | + multiselectMock |
| 60 | + .mockResolvedValueOnce(['email']) |
| 61 | + .mockResolvedValueOnce(['total']) |
| 62 | + confirmMock.mockResolvedValueOnce(true) // "encrypt columns in another table?" |
| 63 | + |
| 64 | + const schemas = await buildSchemasFromDatabase('postgresql://x') |
| 65 | + expect(schemas).toHaveLength(2) |
| 66 | + if (!schemas) throw new Error('expected schemas to be defined') |
| 67 | + |
| 68 | + const out = generateClientFromSchemas('postgresql', schemas) |
| 69 | + expect(out).toContain("email: types.TextEq('email'),") |
| 70 | + expect(out).toContain("total: types.IntegerOrd('total'),") |
| 71 | + expect(out).not.toMatch(/searchOps|v3DomainFactory/) |
| 72 | + }) |
| 73 | + |
| 74 | + it('stops after the first table when the user declines another', async () => { |
| 75 | + selectMock.mockResolvedValueOnce('users').mockResolvedValueOnce('TextEq') |
| 76 | + multiselectMock.mockResolvedValueOnce(['email']) |
| 77 | + confirmMock.mockResolvedValueOnce(false) // decline "another table?" |
| 78 | + |
| 79 | + const schemas = await buildSchemasFromDatabase('postgresql://x') |
| 80 | + expect(schemas).toEqual([ |
| 81 | + { tableName: 'users', columns: [{ name: 'email', domain: 'TextEq' }] }, |
| 82 | + ]) |
| 83 | + }) |
| 84 | + |
| 85 | + it('returns undefined for an empty public schema', async () => { |
| 86 | + queryMock.mockResolvedValueOnce({ rows: [] }) |
| 87 | + expect(await buildSchemasFromDatabase('postgresql://x')).toBeUndefined() |
| 88 | + }) |
| 89 | + |
| 90 | + it('returns undefined when the database connection/introspection fails', async () => { |
| 91 | + // introspectDatabase throws → buildSchemasFromDatabase catches, stops the |
| 92 | + // spinner, logs the error, and returns undefined rather than propagating. |
| 93 | + queryMock.mockReset() |
| 94 | + queryMock.mockRejectedValueOnce(new Error('ECONNREFUSED')) |
| 95 | + expect(await buildSchemasFromDatabase('postgresql://x')).toBeUndefined() |
| 96 | + }) |
| 97 | + |
| 98 | + it('returns undefined when the "another table?" prompt is cancelled', async () => { |
| 99 | + // Distinct from declining (false): cancelling (Ctrl-C) mid-loop must abort |
| 100 | + // the whole build, not return the tables gathered so far. |
| 101 | + selectMock.mockResolvedValueOnce('users').mockResolvedValueOnce('TextEq') |
| 102 | + multiselectMock.mockResolvedValueOnce(['email']) |
| 103 | + confirmMock.mockResolvedValueOnce(CANCEL) // cancel the "another table?" prompt |
| 104 | + |
| 105 | + expect(await buildSchemasFromDatabase('postgresql://x')).toBeUndefined() |
| 106 | + }) |
| 107 | +}) |
0 commit comments