test: cover isolated SQLite invariants and durability

This commit is contained in:
2026-09-13 12:33:17 +03:00
parent 346a6f0057
commit 7fde3ece6e
7 changed files with 166 additions and 3 deletions
+55 -1
View File
@@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
@@ -21,7 +22,7 @@ describe('Authentication (e2e)', () => {
app = moduleFixture.createNestApplication();
configureApp(app);
await app.init();
await app.listen(0, '127.0.0.1');
await app.get(PrismaService).session.deleteMany();
await app.get(PrismaService).tbankConnection.deleteMany();
await app.get(PrismaService).user.deleteMany();
@@ -59,6 +60,59 @@ describe('Authentication (e2e)', () => {
await agent.post('/api/v1/auth/logout').expect(204);
});
it('rotates one concurrent refresh on SQLite and keeps the winner usable', async () => {
const registration = await request(app.getHttpServer())
.post('/api/v1/auth/register')
.send({ email: 'race@example.test', password: 'a secure password' })
.expect(200);
const cookie = registration.headers['set-cookie'][0];
// Both requests read the old hash before either reaches the real SQLite CAS.
const jwt = app.get(JwtService);
const sign = jwt.signAsync.bind(jwt);
let arrived = 0;
let release = () => {};
const ready = new Promise<void>((resolve) => {
release = resolve;
});
const barrier = jest
.spyOn(jwt, 'signAsync')
.mockImplementation(async (...args) => {
const signed = await sign(...args);
if (
typeof args[0] === 'object' &&
'type' in args[0] &&
args[0].type === 'refresh'
) {
if (++arrived === 2) release();
await ready;
}
return signed;
});
const responses = await Promise.all(
Array.from({ length: 2 }, () =>
request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.set('Cookie', cookie),
),
);
barrier.mockRestore();
expect(arrived).toBe(2);
expect(responses.map((response) => response.status).sort()).toEqual([
200, 401,
]);
const winner = responses.find((response) => response.status === 200);
if (!winner) throw new Error('Expected one successful rotation');
await request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.set('Cookie', cookie)
.expect(401);
await request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.set('Cookie', winner.headers['set-cookie'][0])
.expect(200);
expect(await app.get(PrismaService).session.count()).toBe(1);
});
it('rejects duplicate registration and invalid credentials', async () => {
await request(app.getHttpServer())
.post('/api/v1/auth/register')
+10 -1
View File
@@ -1,6 +1,6 @@
const assert = require('node:assert/strict');
const { spawn, execFileSync } = require('node:child_process');
const { mkdtempSync, openSync, closeSync, writeFileSync } = require('node:fs');
const { mkdtempSync, openSync, closeSync, writeFileSync, existsSync, readdirSync, readFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const { join, resolve } = require('node:path');
const { once } = require('node:events');
@@ -9,6 +9,15 @@ const { randomUUID } = require('node:crypto');
const repo = resolve(__dirname, '..');
assert.ok(process.argv[2], 'Pass a separately built baseline repository directory');
const baseline = resolve(process.argv[2]);
assert.notEqual(baseline, repo, 'Baseline must be a separate historical build');
for (const root of [baseline, repo]) {
assert.ok(existsSync(join(root, 'dist/src/main.js')), `Build first: ${root}`);
}
const migrations = root => readdirSync(join(root, 'prisma/migrations')).filter(name => existsSync(join(root, 'prisma/migrations', name, 'migration.sql'))).sort();
const previous = migrations(baseline), current = migrations(repo);
assert.ok(previous.length > 0 && previous.length < current.length, 'Baseline must precede the tested migrations');
assert.deepEqual(previous, current.slice(0, previous.length), 'Baseline migrations must be an unchanged prefix');
for (const name of previous) assert.equal(readFileSync(join(baseline, 'prisma/migrations', name, 'migration.sql'), 'utf8'), readFileSync(join(repo, 'prisma/migrations', name, 'migration.sql'), 'utf8'));
const Database = require(join(repo, 'node_modules/better-sqlite3'));
const dir = mkdtempSync(join(tmpdir(), 'wealthledger-bank-durability-'));
let file = join(dir, 'history.db');
+1 -1
View File
@@ -79,7 +79,7 @@ describe('Manual bank cards configured HTTP and real SQLite', () => {
}).compile();
app = module.createNestApplication();
configureApp(app);
await app.init();
await app.listen(0, '127.0.0.1');
db = app.get(PrismaService);
};
beforeAll(async () => {
+42
View File
@@ -0,0 +1,42 @@
const { spawnSync } = require('node:child_process');
const { mkdtempSync, rmSync } = require('node:fs');
const { join, resolve } = require('node:path');
const cwd = resolve(__dirname, '..');
for (const suite of ['app.e2e-spec.ts', 'bank-cards.e2e-spec.ts']) {
const dir = mkdtempSync('/tmp/bank-cards-');
const env = {
...process.env,
DATABASE_URL: `file:${join(dir, 'test.db')}`,
RUST_LOG: 'info',
NODE_ENV: 'development',
NODE_OPTIONS: '--experimental-vm-modules',
};
try {
for (const args of [
['node_modules/prisma/build/index.js', 'migrate', 'deploy'],
[
'node_modules/jest/bin/jest.js',
'--config',
'test/jest-e2e.json',
'--runInBand',
'--runTestsByPath',
`test/${suite}`,
],
]) {
const result = spawnSync(process.execPath, args, {
cwd,
env,
stdio: 'inherit',
});
if (result.error) throw result.error;
if (result.status !== 0) {
process.exitCode = result.status ?? 1;
break;
}
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
if (process.exitCode) break;
}