test(quality): add concurrency and real browser gates
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { openDatabase } from '../../infrastructure/database/database.js';
|
||||
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
|
||||
import type { SecretStore } from '../../infrastructure/secrets/secret-store.js';
|
||||
import type { Instance } from '@multi-simadmin/contracts';
|
||||
@@ -38,8 +42,11 @@ class MemorySecrets implements SecretStore {
|
||||
}
|
||||
}
|
||||
const dbs: Database.Database[] = [];
|
||||
const temporaryDirectories: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const db of dbs.splice(0)) db.close();
|
||||
for (const directory of temporaryDirectories.splice(0))
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
});
|
||||
function fixture(ids = ['id-1', 'slot-1', 'ref-1', 'id-2', 'slot-2', 'ref-2']) {
|
||||
const db = new Database(':memory:');
|
||||
@@ -56,6 +63,30 @@ function fixture(ids = ['id-1', 'slot-1', 'ref-1', 'id-2', 'slot-2', 'ref-2']) {
|
||||
});
|
||||
return { db, store, service };
|
||||
}
|
||||
function crossConnectionFixture() {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'instance-cas-'));
|
||||
temporaryDirectories.push(directory);
|
||||
const path = join(directory, 'state.sqlite');
|
||||
const firstDb = openDatabase(path);
|
||||
migrateDatabase(firstDb);
|
||||
const secondDb = openDatabase(path);
|
||||
dbs.push(firstDb, secondDb);
|
||||
const store = new MemorySecrets();
|
||||
const options = { store, now: () => new Date('2026-07-16T12:00:00.000Z') };
|
||||
const firstIds = ['id-1', 'first-slot', 'first-reference'];
|
||||
const secondIds = ['second-slot', 'second-reference'];
|
||||
const first = new InstanceService({
|
||||
...options,
|
||||
db: firstDb,
|
||||
idFactory: () => firstIds.shift()!,
|
||||
});
|
||||
const second = new InstanceService({
|
||||
...options,
|
||||
db: secondDb,
|
||||
idFactory: () => secondIds.shift()!,
|
||||
});
|
||||
return { firstDb, secondDb, store, first, second };
|
||||
}
|
||||
const basic = { name: ' Alpha ', origin: 'http://192.168.1.10:8080/', tags: [' z ', 'a', 'a'] };
|
||||
const code = async (promise: Promise<unknown>) => {
|
||||
try {
|
||||
@@ -222,6 +253,23 @@ describe('InstanceService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects stale update and delete CAS across managed connections', async () => {
|
||||
const { firstDb, first, second } = crossConnectionFixture();
|
||||
await first.create(basic);
|
||||
await second.update('id-1', 1, { name: 'Committed elsewhere' });
|
||||
|
||||
await expect(first.update('id-1', 1, { name: 'Stale update' })).rejects.toMatchObject({
|
||||
code: 'REVISION_CONFLICT',
|
||||
});
|
||||
await expect(first.delete('id-1', 1)).rejects.toMatchObject({ code: 'REVISION_CONFLICT' });
|
||||
expect(
|
||||
firstDb.prepare('SELECT name,config_revision FROM instances WHERE id=?').get('id-1'),
|
||||
).toEqual({
|
||||
name: 'Committed elsewhere',
|
||||
config_revision: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves, replaces and clears credentials without exposing them', async () => {
|
||||
const { db, store, service } = fixture();
|
||||
await service.create({ ...basic, password: { action: 'set', password: 'first-secret' } });
|
||||
|
||||
Reference in New Issue
Block a user