Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
255 lines
7.4 KiB
JavaScript
255 lines
7.4 KiB
JavaScript
/**
|
|
* Connection integration tests with PostgreSQL
|
|
* Tests DATABASE_TYPE=postgres switching
|
|
*
|
|
* @requires PostgreSQL running on localhost:5432
|
|
* @requires Database 'godcrm' with user 'godcrm'
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
import { getAdapter, destroyAdapter, AdapterFactory } from '../connection.js';
|
|
|
|
// Skip if no POSTGRES_URL
|
|
const POSTGRES_URL = process.env.POSTGRES_URL || 'postgresql://godcrm:godcrm_dev_2026@localhost:5432/godcrm';
|
|
const runPostgresTests = process.env.TEST_POSTGRES === 'true';
|
|
|
|
describe.skipIf(!runPostgresTests)('Connection with PostgreSQL', () => {
|
|
let adapter;
|
|
|
|
beforeAll(async () => {
|
|
// Force PostgreSQL type
|
|
process.env.DATABASE_TYPE = 'postgres';
|
|
process.env.POSTGRES_URL = POSTGRES_URL;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await destroyAdapter();
|
|
delete process.env.DATABASE_TYPE;
|
|
delete process.env.POSTGRES_URL;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Reset adapter between tests
|
|
await AdapterFactory.destroy();
|
|
});
|
|
|
|
describe('getAdapter()', () => {
|
|
it('should return PostgresAdapter when DATABASE_TYPE=postgres', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
expect(adapter).toBeDefined();
|
|
expect(adapter.constructor.name).toBe('PostgresAdapter');
|
|
});
|
|
|
|
it('should connect and query users table', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const users = await adapter.all('SELECT id, email FROM users LIMIT 5');
|
|
|
|
expect(Array.isArray(users)).toBe(true);
|
|
expect(users.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should get single user with get()', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const user = await adapter.get('SELECT id, email FROM users LIMIT 1');
|
|
|
|
// May be undefined if no users
|
|
if (user) {
|
|
expect(user).toHaveProperty('id');
|
|
expect(user).toHaveProperty('email');
|
|
}
|
|
});
|
|
|
|
it('should return count with get()', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const result = await adapter.get('SELECT COUNT(*) as count FROM users');
|
|
|
|
expect(result).toHaveProperty('count');
|
|
expect(typeof result.count).toBe('string'); // PostgreSQL returns bigint as string
|
|
});
|
|
});
|
|
|
|
describe('CRUD operations', () => {
|
|
const testTableName = 'test_crud_ops_' + Date.now();
|
|
|
|
beforeEach(async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
// Create test table
|
|
await adapter.run(`
|
|
CREATE TABLE IF NOT EXISTS ${testTableName} (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255),
|
|
value INTEGER,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (adapter) {
|
|
await adapter.run(`DROP TABLE IF EXISTS ${testTableName}`);
|
|
}
|
|
});
|
|
|
|
it('should INSERT a row', async () => {
|
|
const result = await adapter.run(
|
|
`INSERT INTO ${testTableName} (name, value) VALUES ($1, $2)`,
|
|
['test-item', 42]
|
|
);
|
|
|
|
expect(result.changes).toBe(1);
|
|
});
|
|
|
|
it('should SELECT inserted row', async () => {
|
|
await adapter.run(
|
|
`INSERT INTO ${testTableName} (name, value) VALUES ($1, $2)`,
|
|
['select-test', 100]
|
|
);
|
|
|
|
const row = await adapter.get(
|
|
`SELECT * FROM ${testTableName} WHERE name = $1`,
|
|
['select-test']
|
|
);
|
|
|
|
expect(row).toBeDefined();
|
|
expect(row.name).toBe('select-test');
|
|
expect(row.value).toBe(100);
|
|
});
|
|
|
|
it('should UPDATE a row', async () => {
|
|
await adapter.run(
|
|
`INSERT INTO ${testTableName} (name, value) VALUES ($1, $2)`,
|
|
['update-test', 50]
|
|
);
|
|
|
|
const updateResult = await adapter.run(
|
|
`UPDATE ${testTableName} SET value = $1 WHERE name = $2`,
|
|
[999, 'update-test']
|
|
);
|
|
|
|
expect(updateResult.changes).toBe(1);
|
|
|
|
const row = await adapter.get(
|
|
`SELECT * FROM ${testTableName} WHERE name = $1`,
|
|
['update-test']
|
|
);
|
|
expect(row.value).toBe(999);
|
|
});
|
|
|
|
it('should DELETE a row', async () => {
|
|
await adapter.run(
|
|
`INSERT INTO ${testTableName} (name, value) VALUES ($1, $2)`,
|
|
['delete-test', 123]
|
|
);
|
|
|
|
const deleteResult = await adapter.run(
|
|
`DELETE FROM ${testTableName} WHERE name = $1`,
|
|
['delete-test']
|
|
);
|
|
|
|
expect(deleteResult.changes).toBe(1);
|
|
|
|
const row = await adapter.get(
|
|
`SELECT * FROM ${testTableName} WHERE name = $1`,
|
|
['delete-test']
|
|
);
|
|
expect(row).toBeFalsy(); // null or undefined
|
|
});
|
|
});
|
|
|
|
describe('Transactions', () => {
|
|
const txTableName = 'test_transactions_' + Date.now();
|
|
|
|
beforeEach(async () => {
|
|
await AdapterFactory.destroy();
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
await adapter.run(`
|
|
CREATE TABLE IF NOT EXISTS ${txTableName} (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255),
|
|
value INTEGER
|
|
)
|
|
`);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (adapter) {
|
|
await adapter.run(`DROP TABLE IF EXISTS ${txTableName}`);
|
|
}
|
|
});
|
|
|
|
it('should commit transaction on success', async () => {
|
|
await adapter.transaction(async (trx) => {
|
|
await trx.run(
|
|
`INSERT INTO ${txTableName} (name, value) VALUES ($1, $2)`,
|
|
['tx-commit', 1]
|
|
);
|
|
await trx.run(
|
|
`INSERT INTO ${txTableName} (name, value) VALUES ($1, $2)`,
|
|
['tx-commit', 2]
|
|
);
|
|
});
|
|
|
|
const rows = await adapter.all(
|
|
`SELECT * FROM ${txTableName} WHERE name = $1`,
|
|
['tx-commit']
|
|
);
|
|
|
|
expect(rows.length).toBe(2);
|
|
});
|
|
|
|
it('should rollback transaction on error', async () => {
|
|
try {
|
|
await adapter.transaction(async (trx) => {
|
|
await trx.run(
|
|
`INSERT INTO ${txTableName} (name, value) VALUES ($1, $2)`,
|
|
['tx-rollback', 100]
|
|
);
|
|
throw new Error('Force rollback');
|
|
});
|
|
} catch (e) {
|
|
// Expected
|
|
}
|
|
|
|
const rows = await adapter.all(
|
|
`SELECT * FROM ${txTableName} WHERE name = $1`,
|
|
['tx-rollback']
|
|
);
|
|
|
|
expect(rows.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('Query real tables', () => {
|
|
it('should query spaces table', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const spaces = await adapter.all('SELECT id, name FROM spaces LIMIT 10');
|
|
|
|
expect(Array.isArray(spaces)).toBe(true);
|
|
});
|
|
|
|
it('should query projects table', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const projects = await adapter.all('SELECT id, name FROM projects LIMIT 10');
|
|
|
|
expect(Array.isArray(projects)).toBe(true);
|
|
});
|
|
|
|
it('should join users and projects', async () => {
|
|
adapter = await getAdapter({ type: 'postgres', url: POSTGRES_URL });
|
|
|
|
const result = await adapter.all(`
|
|
SELECT u.email, p.name as project_name
|
|
FROM users u
|
|
LEFT JOIN projects p ON p.owner_id = u.id
|
|
LIMIT 5
|
|
`);
|
|
|
|
expect(Array.isArray(result)).toBe(true);
|
|
});
|
|
});
|
|
});
|