import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartdb from '../ts/index.js'; import { MongoClient } from 'mongodb'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; let server: smartdb.SmartdbServer; let tmpDir: string; let storagePath: string; let usersPath: string; const port = 27129; const openedClients: MongoClient[] = []; let tenantA: smartdb.ISmartDbDatabaseTenantDescriptor; let tenantB: smartdb.ISmartDbDatabaseTenantDescriptor; let exportedTenantA: smartdb.ISmartDbDatabaseExport; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-tenants-test-')); } function cleanTmpDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } async function connect(uri: string): Promise { const client = new MongoClient(uri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); openedClients.push(client); return client; } async function expectConnectionToFail(uri: string): Promise { const client = new MongoClient(uri, { directConnection: true, serverSelectionTimeoutMS: 5000, }); let threw = false; try { await client.connect(); await client.db('tenant_a').command({ ping: 1 }); } catch { threw = true; } finally { await client.close().catch(() => undefined); } expect(threw).toBeTrue(); } async function closeOpenedClients(): Promise { while (openedClients.length > 0) { const client = openedClients.pop(); await client?.close().catch(() => undefined); } } function createServer(): smartdb.SmartdbServer { return new smartdb.SmartdbServer({ port, storage: 'file', storagePath, auth: { enabled: true, usersPath, scramIterations: 4096, users: [ { username: 'root', password: 'secret', database: 'admin', roles: ['root'], }, ], }, }); } tap.test('tenants: should start durable authenticated service', async () => { tmpDir = makeTmpDir(); storagePath = path.join(tmpDir, 'data'); usersPath = path.join(tmpDir, 'users.json'); server = createServer(); await server.start(); expect(server.running).toBeTrue(); }); tap.test('tenants: should create isolated database tenants', async () => { tenantA = await server.createDatabaseTenant({ databaseName: 'tenant_a', username: 'tenant_a_user', password: 'tenant-a-pass-1', }); tenantB = await server.createDatabaseTenant({ databaseName: 'tenant_b', username: 'tenant_b_user', password: 'tenant-b-pass-1', }); expect(tenantA.databaseName).toEqual('tenant_a'); expect(tenantA.authSource).toEqual('tenant_a'); expect(tenantA.roles.includes('readWrite')).toBeTrue(); expect(tenantA.roles.includes('dbAdmin')).toBeTrue(); expect(typeof tenantA.mongodbUri).toEqual('string'); const tenants = await server.listDatabaseTenants(); expect(tenants.some((tenant) => tenant.databaseName === 'tenant_a')).toBeTrue(); expect(tenants.some((tenant) => tenant.databaseName === 'tenant_b')).toBeTrue(); const descriptor = await server.getDatabaseTenantDescriptor({ databaseName: 'tenant_a', username: 'tenant_a_user', }); expect(descriptor.username).toEqual('tenant_a_user'); }); tap.test('tenants: should work with official MongoDB driver and enforce auth isolation', async () => { const clientA = await connect(tenantA.mongodbUri!); const clientB = await connect(tenantB.mongodbUri!); const ping = await clientA.db('tenant_a').command({ ping: 1 }); expect(ping.ok).toEqual(1); await clientA.db('tenant_a').collection('notes').insertOne({ title: 'tenant a note' }); await clientA.db('tenant_a').collection('notes').createIndex({ title: 1 }); await clientB.db('tenant_b').collection('notes').insertOne({ title: 'tenant b note' }); let threw = false; try { await clientA.db('tenant_b').collection('notes').findOne({ title: 'tenant b note' }); } catch (err: any) { threw = true; expect(err.code).toEqual(13); } expect(threw).toBeTrue(); }); tap.test('tenants: should expose health and metrics for readiness checks', async () => { const health = await server.getHealth(); expect(health.running).toBeTrue(); expect(health.storagePath).toEqual(storagePath); expect(health.authEnabled).toBeTrue(); expect(health.databaseCount >= 2).toBeTrue(); expect(health.collectionCount >= 2).toBeTrue(); const metrics = await server.getMetrics(); expect(metrics.authEnabled).toBeTrue(); expect(metrics.databases >= 2).toBeTrue(); expect(metrics.collections >= 2).toBeTrue(); }); tap.test('tenants: should rotate password without restart', async () => { const oldUri = tenantA.mongodbUri!; await closeOpenedClients(); tenantA = await server.rotateDatabaseTenantPassword({ username: 'tenant_a_user', password: 'tenant-a-pass-2', }); expect(typeof tenantA.mongodbUri).toEqual('string'); await expectConnectionToFail(oldUri); const rotatedClient = await connect(tenantA.mongodbUri!); const doc = await rotatedClient.db('tenant_a').collection('notes').findOne({ title: 'tenant a note' }); expect(doc).toBeTruthy(); }); tap.test('tenants: should persist runtime users and file-backed data across restart', async () => { await closeOpenedClients(); await server.stop(); server = createServer(); await server.start(); const clientA = await connect(tenantA.mongodbUri!); const clientB = await connect(tenantB.mongodbUri!); const docA = await clientA.db('tenant_a').collection('notes').findOne({ title: 'tenant a note' }); const docB = await clientB.db('tenant_b').collection('notes').findOne({ title: 'tenant b note' }); expect(docA).toBeTruthy(); expect(docB).toBeTruthy(); }); tap.test('tenants: should export and restore one database without unrelated tenants', async () => { exportedTenantA = await server.exportDatabase({ databaseName: 'tenant_a' }); expect(exportedTenantA.databaseName).toEqual('tenant_a'); expect(exportedTenantA.collections.length).toEqual(1); expect(JSON.stringify(exportedTenantA).includes('tenant b note')).toBeFalse(); await closeOpenedClients(); const deleteResult = await server.deleteDatabaseTenant({ databaseName: 'tenant_a', username: 'tenant_a_user', }); expect(deleteResult.databaseDropped).toBeTrue(); expect(deleteResult.deletedUsers).toEqual(1); await expectConnectionToFail(tenantA.mongodbUri!); const importResult = await server.importDatabase({ databaseName: 'tenant_a', source: exportedTenantA, }); expect(importResult.databaseName).toEqual('tenant_a'); expect(importResult.documents).toEqual(1); tenantA = await server.createDatabaseTenant({ databaseName: 'tenant_a', username: 'tenant_a_user', password: 'tenant-a-pass-3', }); const restoredClient = await connect(tenantA.mongodbUri!); const restoredDoc = await restoredClient.db('tenant_a').collection('notes').findOne({ title: 'tenant a note' }); expect(restoredDoc).toBeTruthy(); const clientB = await connect(tenantB.mongodbUri!); const unrelatedDoc = await clientB.db('tenant_b').collection('notes').findOne({ title: 'tenant b note' }); expect(unrelatedDoc).toBeTruthy(); }); tap.test('tenants: cleanup', async () => { await closeOpenedClients(); await server.stop(); expect(server.running).toBeFalse(); cleanTmpDir(tmpDir); }); export default tap.start();