Files
Dungeons-Proxy/test/store.test.js

147 lines
7.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AccessStore } from '../src/store.js';
import { fixture, grant } from './helpers.js';
test('the same number works on multiple devices for 15 days; all sessions stop at code expiry', async () => {
const f = fixture();
const { code, request } = await grant(f);
assert.equal(code.length, 10);
assert.equal(request.codeExpiresAt - request.approvedAt, 15 * 86400_000);
const first = f.store.redeem(code);
f.advance(14 * 86400_000);
const second = f.store.redeem(code);
assert.notEqual(first, second);
assert.ok(f.store.session(first));
assert.equal(f.store.session(second).expiresAt, request.codeExpiresAt);
assert.equal(JSON.stringify(f.store.state).includes(code), false);
assert.equal(JSON.stringify(f.store.state).includes(first), false);
f.advance(86400_000);
assert.equal(f.store.redeem(code), null);
assert.equal(f.store.session(first), null);
assert.equal(f.store.session(second), null);
await f.service.request({ platform: 'telegram', user_id: '5' });
assert.equal(f.store.state.requests.length, 2);
assert.equal(f.store.state.requests[1].status, 'pending');
});
test('only configured admins from the correct platform can approve; repeated reviews do not send another code', async () => {
const f = fixture();
await f.service.request({ platform: 'telegram', user_id: '5' });
const id = f.store.state.requests[0].id;
assert.match(await f.service.decide('telegram', '999', id, 'approve'), /Only configured/);
assert.match(await f.service.decide('discord', '200', id, 'approve'), /not found/);
assert.equal(f.store.getRequest(id).status, 'pending');
const results = await Promise.all([f.service.decide('telegram', '100', id, 'approve'), f.service.decide('telegram', '101', id, 'approve')]);
assert.equal(results.filter(r => r.startsWith('Approved.')).length, 1);
assert.equal(f.messages.filter(m => m.text.includes('Your number:')).length, 1);
assert.match(await f.service.decide('telegram', '100', id, 'deny'), /already approved/);
});
test('failed code delivery leaves the request retryable and the attempted number unusable', async () => {
const f = fixture();
await f.service.request({ platform: 'telegram', user_id: '5' });
const id = f.store.state.requests[0].id;
let attemptedCode;
const original = f.provider.sendUser;
f.provider.sendUser = async (_id, message) => {
attemptedCode = message.match(/Your number: (\d+)/)[1];
assert.equal(f.store.redeem(attemptedCode), null);
throw new Error('DM blocked');
};
assert.match(await f.service.decide('telegram', '100', id, 'approve'), /No number was activated/);
assert.equal(f.store.getRequest(id).status, 'pending');
assert.equal(f.store.getRequest(id).codeHash, undefined);
assert.equal(f.store.redeem(attemptedCode), null);
f.provider.sendUser = original;
assert.match(await f.service.decide('telegram', '100', id, 'approve'), /Approved/);
});
test('duplicate requests are coalesced; failed admin notifications can be retried', async () => {
const f = fixture();
const send = f.provider.sendUser;
f.provider.sendUser = async (id, ...args) => { if (id === '100' || id === '101') throw new Error('Unavailable'); return send(id, ...args); };
const identity = { platform: 'telegram', user_id: '5' };
assert.match(await f.service.request(identity), /could not contact/);
f.provider.sendUser = send;
await Promise.all([f.service.request(identity), f.service.request(identity)]);
assert.equal(f.store.state.requests.length, 1);
assert.deepEqual(f.store.state.requests[0].notifiedAdmins, ['100', '101']);
assert.equal(f.messages.filter(m => m.buttons.length === 2).length, 2);
});
test('denial, pending expiry, malformed numbers, logout and revocation fail closed', async () => {
const f = fixture();
await f.service.request({ platform: 'telegram', user_id: '5' });
const id = f.store.state.requests[0].id;
await f.service.decide('telegram', '100', id, 'deny');
assert.equal(f.store.getRequest(id).status, 'denied');
await assert.rejects(f.service.request({ platform: 'telegram', user_id: '5' }), /wait a few minutes/);
await f.service.request({ platform: 'telegram', user_id: '6' });
f.advance(f.config.requestTtlMs);
assert.match(await f.service.decide('telegram', '100', f.store.state.requests[1].id, 'approve'), /expired/);
for (const code of [null, {}, 1234567890, '', '1'.repeat(11), 'abcd123456']) assert.equal(f.store.redeem(code), null);
const { code, request } = await grant(f, '7');
const token = f.store.redeem(code);
f.store.logout(token);
assert.equal(f.store.session(token), null);
const other = f.store.redeem(code);
f.store.updateRequest(request.id, r => { r.status = 'revoked'; });
assert.equal(f.store.session(other), null);
assert.equal(f.store.redeem(code), null);
});
test('per-IP and global login limits use fixed windows and do not store raw IPs', () => {
const f = fixture({ loginAttempts: 2, globalLoginAttempts: 3 });
assert.equal(f.store.attempt('192.0.2.1'), 0);
assert.equal(f.store.attempt('192.0.2.1'), 0);
assert.equal(f.store.attempt('192.0.2.1'), 900);
assert.equal(f.store.attempt('192.0.2.2'), 0);
assert.equal(f.store.attempt('192.0.2.3'), 900);
assert.equal(JSON.stringify(f.store.state).includes('192.0.2.'), false);
f.advance(900_000);
assert.equal(f.store.attempt('192.0.2.1'), 0);
});
test('atomic storage survives restart, preserves limits, locks a second writer, and rejects corrupt data', async () => {
const dir = mkdtempSync(join(tmpdir(), 'dng-store-'));
const f = fixture({ dataFile: join(dir, 'access.json') });
let disk;
try {
disk = new AccessStore(f.config);
const { request } = disk.request({ platform: 'telegram', user_id: '5' });
const code = disk.reserveCode(request.id, '100');
disk.updateRequest(request.id, r => { r.status = 'approved'; });
const token = disk.redeem(code);
disk.attempt('192.0.2.1');
assert.throws(() => new AccessStore(f.config), /locked/);
assert.equal(statSync(f.config.dataFile).mode & 0o777, 0o600);
assert.equal(readFileSync(f.config.dataFile, 'utf8').includes(code), false);
disk.close();
disk = new AccessStore(f.config);
assert.ok(disk.session(token));
assert.ok(disk.redeem(code));
assert.equal(disk.state.limits.global.count, 1);
disk.close();
writeFileSync(f.config.dataFile, 'invalid json');
assert.throws(() => new AccessStore(f.config));
} finally { disk?.close(); rmSync(dir, { recursive: true, force: true }); }
});
test('interrupted approval is reset on restart without activating the undelivered code', () => {
const dir = mkdtempSync(join(tmpdir(), 'dng-recovery-'));
const f = fixture({ dataFile: join(dir, 'access.json') });
let disk = new AccessStore(f.config);
try {
const { request } = disk.request({ platform: 'telegram', user_id: '5' });
const code = disk.reserveCode(request.id, '100');
disk.close();
disk = new AccessStore(f.config);
assert.equal(disk.getRequest(request.id).status, 'pending');
assert.equal(disk.redeem(code), null);
} finally { disk.close(); rmSync(dir, { recursive: true, force: true }); }
});