233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
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<MongoClient> {
|
|
const client = new MongoClient(uri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
openedClients.push(client);
|
|
return client;
|
|
}
|
|
|
|
async function expectConnectionToFail(uri: string): Promise<void> {
|
|
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<void> {
|
|
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();
|