godcrm/backend/__tests__/api-keys/api-keys-postgres.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

327 lines
11 KiB
JavaScript

// @vitest-environment node
/**
* API Keys PostgreSQL Integration Tests
* TDD: 🔴 RED → 🟢 GREEN → 🔵 REFACTOR
*
* Run: DATABASE_TYPE=postgres npm test -- backend/__tests__/api-keys/api-keys-postgres.test.js
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import crypto from 'crypto';
import pg from 'pg';
const { Pool } = pg;
// PostgreSQL connection for tests
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME || 'godcrm_prod',
user: process.env.DB_USER || 'godcrm',
password: process.env.DB_PASSWORD || 'godcrm_dev_2026'
});
// Helper functions
const dbQuery = async (sql, params = []) => {
const result = await pool.query(sql, params);
return result.rows;
};
const dbGet = async (sql, params = []) => {
const rows = await dbQuery(sql, params);
return rows[0];
};
const dbRun = async (sql, params = []) => {
const result = await pool.query(sql, params);
return { rowCount: result.rowCount, rows: result.rows };
};
// Hash API key
const hashApiKey = (key) => {
return crypto.createHash('sha256').update(key).digest('hex');
};
// Generate random API key
const generateApiKey = () => {
return 'sk-' + crypto.randomBytes(16).toString('hex');
};
describe('API Keys PostgreSQL Integration', () => {
beforeAll(async () => {
// Verify connection
try {
await pool.query('SELECT 1');
console.log('✅ PostgreSQL connected');
} catch (err) {
console.error('❌ PostgreSQL connection failed:', err.message);
throw err;
}
});
afterAll(async () => {
// Cleanup test data
await pool.query(`DELETE FROM api_keys WHERE name LIKE 'TEST_%'`).catch(() => {});
await pool.end();
});
beforeEach(async () => {
// Cleanup before each test
await pool.query(`DELETE FROM api_keys WHERE name LIKE 'TEST_%'`).catch(() => {});
});
describe('System api_keys table', () => {
it('should have api_keys table with required columns', async () => {
const columns = await dbQuery(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'api_keys'
ORDER BY ordinal_position
`);
const columnNames = columns.map(c => c.column_name);
expect(columnNames).toContain('id');
expect(columnNames).toContain('key_prefix');
expect(columnNames).toContain('key_hash');
expect(columnNames).toContain('is_active');
});
it('should insert API key with prefix and hash', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
const result = await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
RETURNING id
`, [1, 'TEST_insert_key', keyPrefix, keyHash, 'all', 1]);
expect(result.rowCount).toBe(1);
expect(result.rows[0].id).toBeDefined();
});
it('should find API key by prefix and hash', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
// Insert
await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`, [1, 'TEST_find_key', keyPrefix, keyHash, 'all', 1]);
// Find by prefix + hash
const found = await dbGet(`
SELECT * FROM api_keys WHERE key_prefix = $1 AND key_hash = $2
`, [keyPrefix, keyHash]);
expect(found).toBeDefined();
expect(found.name).toBe('TEST_find_key');
expect(found.key_prefix).toBe(keyPrefix);
expect(found.is_active).toBe(1);
});
it('should NOT find key with wrong hash', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
const wrongHash = hashApiKey('wrong-key');
// Insert
await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`, [1, 'TEST_wrong_hash', keyPrefix, keyHash, 'all', 1]);
// Try to find with wrong hash
const notFound = await dbGet(`
SELECT * FROM api_keys WHERE key_prefix = $1 AND key_hash = $2
`, [keyPrefix, wrongHash]);
expect(notFound).toBeUndefined();
});
it('should NOT authenticate inactive key', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
// Insert inactive key
await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`, [1, 'TEST_inactive_key', keyPrefix, keyHash, 'all', 0]);
// Try to find active key
const notFound = await dbGet(`
SELECT * FROM api_keys WHERE key_prefix = $1 AND key_hash = $2 AND is_active = 1
`, [keyPrefix, keyHash]);
expect(notFound).toBeUndefined();
});
it('should update last_used_at and request_count', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
// Insert
const insertResult = await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, request_count, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
RETURNING id
`, [1, 'TEST_usage_key', keyPrefix, keyHash, 'all', 1, 0]);
const keyId = insertResult.rows[0].id;
// Update usage stats
await dbRun(`
UPDATE api_keys
SET last_used_at = NOW(), request_count = request_count + 1
WHERE id = $1
`, [keyId]);
// Verify
const updated = await dbGet(`SELECT * FROM api_keys WHERE id = $1`, [keyId]);
expect(updated.request_count).toBe(1);
expect(updated.last_used_at).not.toBeNull();
});
});
describe('api_keys_list CRM table', () => {
it('should find api_keys_list table', async () => {
const table = await dbGet(`
SELECT ut.id, ut.name, p.name as project_name, p.space_id
FROM universal_tables ut
JOIN projects p ON ut.project_id = p.id
WHERE ut.name = 'api_keys_list'
LIMIT 1
`);
// Table should exist in System Data
if (table) {
expect(table.name).toBe('api_keys_list');
console.log(`✅ Found api_keys_list in project: ${table.project_name} (space ${table.space_id})`);
} else {
console.log('⚠️ api_keys_list table not found - will be created on first use');
}
});
it('should be able to store key data in table_rows', async () => {
// Find api_keys_list table
const table = await dbGet(`
SELECT ut.id, ut.base_id FROM universal_tables ut WHERE ut.name = 'api_keys_list' LIMIT 1
`);
if (!table) {
console.log('⚠️ Skipping - no api_keys_list table');
return;
}
const apiKey = generateApiKey();
const keyData = {
name: 'TEST_crm_key',
key_prefix: apiKey.substring(0, 7),
scopes: ['*'],
is_active: true,
created_at: new Date().toISOString()
};
// Insert with base_id (required field)
const baseId = table.base_id || table.id;
const result = await dbRun(`
INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW())
RETURNING id
`, [table.id, baseId, JSON.stringify(keyData)]);
expect(result.rowCount).toBe(1);
// Verify
const inserted = await dbGet(`
SELECT id, data FROM table_rows WHERE id = $1
`, [result.rows[0].id]);
expect(inserted).toBeDefined();
const data = typeof inserted.data === 'string' ? JSON.parse(inserted.data) : inserted.data;
expect(data.name).toBe('TEST_crm_key');
// Cleanup
await dbRun(`DELETE FROM table_rows WHERE id = $1`, [result.rows[0].id]);
});
it('should list keys from table_rows with correct data', async () => {
const table = await dbGet(`
SELECT id FROM universal_tables WHERE name = 'api_keys_list' LIMIT 1
`);
if (!table) {
console.log('⚠️ Skipping - no api_keys_list table');
return;
}
// Get existing keys
const keys = await dbQuery(`
SELECT id, data FROM table_rows WHERE table_id = $1
`, [table.id]);
console.log(`📊 Found ${keys.length} keys in api_keys_list`);
// Each key should have required fields
for (const key of keys) {
const data = typeof key.data === 'string' ? JSON.parse(key.data) : key.data;
expect(data.name || data.key_prefix).toBeDefined();
}
});
});
describe('Authentication flow', () => {
it('should authenticate with valid API key (full flow)', async () => {
const apiKey = generateApiKey();
const keyPrefix = apiKey.substring(0, 7);
const keyHash = hashApiKey(apiKey);
// 1. Create key in database
await dbRun(`
INSERT INTO api_keys (user_id, name, key_prefix, key_hash, scopes, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
`, [1, 'TEST_auth_flow', keyPrefix, keyHash, 'all', 1]);
// 2. Simulate authentication (what middleware does)
const inputPrefix = apiKey.substring(0, 7);
const inputHash = hashApiKey(apiKey);
const keyRecord = await dbGet(`
SELECT ak.*, u.id as uid, u.email, u.name as user_name, u.role
FROM api_keys ak
JOIN users u ON ak.user_id = u.id
WHERE ak.key_prefix = $1 AND ak.key_hash = $2
`, [inputPrefix, inputHash]);
// 3. Verify authentication succeeded
expect(keyRecord).toBeDefined();
expect(keyRecord.is_active).toBe(1);
expect(keyRecord.uid).toBe(1);
});
it('should reject authentication with invalid key', async () => {
const fakeKey = 'sk-' + crypto.randomBytes(16).toString('hex');
const fakePrefix = fakeKey.substring(0, 7);
const fakeHash = hashApiKey(fakeKey);
// Try to authenticate non-existent key
const keyRecord = await dbGet(`
SELECT * FROM api_keys WHERE key_prefix = $1 AND key_hash = $2
`, [fakePrefix, fakeHash]);
expect(keyRecord).toBeUndefined();
});
});
});