test: close remaining quality verification gaps
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..');
|
||||
@@ -96,6 +96,12 @@ if (trackedAlternativeLocks) {
|
||||
`${trackedAlternativeLocks.replace(/\r?\n/g, ', ')}: npm/package-lock.json is the only supported committed dependency lock`,
|
||||
);
|
||||
}
|
||||
const presentAlternativeLocks = ['pnpm-lock.yaml', 'yarn.lock'].filter((file) => existsSync(resolve(root, file)));
|
||||
if (presentAlternativeLocks.length) {
|
||||
violations.push(
|
||||
`${presentAlternativeLocks.join(', ')}: remove alternative lock files; npm/package-lock.json is the only supported dependency lock`,
|
||||
);
|
||||
}
|
||||
if (packageJson.packageManager !== 'npm@10.9.4') {
|
||||
violations.push('package.json: packageManager must pin the supported npm baseline');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const redisUrl = process.env.AUTH_REDIS_INTEGRATION_URL;
|
||||
if (!redisUrl) {
|
||||
throw new Error('AUTH_REDIS_INTEGRATION_URL is required; use an isolated Redis database');
|
||||
}
|
||||
|
||||
process.env.REDIS_URL = redisUrl;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { SessionService } = require('../../api/dist/auth/session.service.js');
|
||||
const IORedis = require('../../api/node_modules/ioredis');
|
||||
|
||||
const redis = new IORedis(redisUrl, { enableReadyCheck: true, maxRetriesPerRequest: 1 });
|
||||
const first = new SessionService();
|
||||
const second = new SessionService();
|
||||
const runId = randomUUID();
|
||||
const digest = (value) => createHash('sha256').update(value.trim()).digest('hex');
|
||||
const keys = new Set();
|
||||
|
||||
function remember(...values) {
|
||||
for (const value of values) keys.add(value);
|
||||
}
|
||||
|
||||
async function ttlInRange(key, minimum, maximum) {
|
||||
const ttl = await redis.ttl(key);
|
||||
assert.ok(ttl >= minimum && ttl <= maximum, `${key} TTL ${ttl} is outside ${minimum}..${maximum}`);
|
||||
}
|
||||
|
||||
try {
|
||||
assert.equal(await redis.ping(), 'PONG');
|
||||
|
||||
const { token } = await first.create(`integration-user-${runId}`, 'client', 1);
|
||||
const sessionKey = `cmpp:auth:session:${digest(token)}`;
|
||||
remember(sessionKey);
|
||||
const crossInstanceSession = await second.validate(token, false);
|
||||
assert.equal(crossInstanceSession.status, 'active');
|
||||
assert.equal(crossInstanceSession.record.userId, `integration-user-${runId}`);
|
||||
|
||||
const captchaId = `integration-captcha-${runId}`;
|
||||
const captchaKey = `cmpp:auth:captcha:${captchaId}`;
|
||||
remember(captchaKey);
|
||||
await first.storeCaptcha(captchaId, '8291', 120);
|
||||
assert.equal(await second.consumeCaptcha(captchaId), '8291');
|
||||
assert.equal(await first.consumeCaptcha(captchaId), null);
|
||||
|
||||
const captchaIp = `198.51.100.${(Number.parseInt(runId.slice(0, 2), 16) % 200) + 1}`;
|
||||
const captchaRateKey = `cmpp:auth:captcha-rate:ip:${digest(captchaIp)}`;
|
||||
remember(captchaRateKey);
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
assert.equal(await (index % 2 === 0 ? first : second).assertCaptchaRequestAllowed(captchaIp), true);
|
||||
}
|
||||
assert.equal(await second.assertCaptchaRequestAllowed(captchaIp), false);
|
||||
await ttlInRange(captchaRateKey, 1, 300);
|
||||
|
||||
const pairLogin = `pair-${runId}@integration.invalid`;
|
||||
const pairIp = '198.51.100.210';
|
||||
const accountDigest = digest(pairLogin.toLowerCase());
|
||||
const ipDigest = digest(pairIp);
|
||||
const pairDigest = digest(`${accountDigest}:${ipDigest}`);
|
||||
const pairKeys = [
|
||||
`cmpp:auth:failure:${accountDigest}`,
|
||||
`cmpp:auth:failure:ip:${ipDigest}`,
|
||||
`cmpp:auth:failure:pair:${pairDigest}`,
|
||||
`cmpp:auth:lock:${accountDigest}`,
|
||||
`cmpp:auth:lock:ip:${ipDigest}`,
|
||||
`cmpp:auth:lock:pair:${pairDigest}`,
|
||||
];
|
||||
remember(...pairKeys);
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
await (index % 2 === 0 ? first : second).recordAnonymousLoginFailure(pairLogin, pairIp);
|
||||
}
|
||||
assert.equal(await second.anonymousLoginLockScope(pairLogin, pairIp), 'account');
|
||||
await ttlInRange(pairKeys[0], 86_000, 86_400);
|
||||
await ttlInRange(pairKeys[3], 86_000, 86_400);
|
||||
await ttlInRange(pairKeys[2], 86_000, 86_400);
|
||||
await ttlInRange(pairKeys[5], 86_000, 86_400);
|
||||
|
||||
const scanIp = '198.51.100.211';
|
||||
const scanIpDigest = digest(scanIp);
|
||||
const scanFailureKey = `cmpp:auth:failure:ip:${scanIpDigest}`;
|
||||
const scanLockKey = `cmpp:auth:lock:ip:${scanIpDigest}`;
|
||||
remember(scanFailureKey, scanLockKey);
|
||||
for (let index = 0; index < 30; index += 1) {
|
||||
const login = `scan-${runId}-${index}@integration.invalid`;
|
||||
const loginDigest = digest(login.toLowerCase());
|
||||
const loginPairDigest = digest(`${loginDigest}:${scanIpDigest}`);
|
||||
remember(
|
||||
`cmpp:auth:failure:${loginDigest}`,
|
||||
`cmpp:auth:failure:pair:${loginPairDigest}`,
|
||||
`cmpp:auth:lock:${loginDigest}`,
|
||||
`cmpp:auth:lock:pair:${loginPairDigest}`,
|
||||
);
|
||||
await (index % 2 === 0 ? first : second).recordAnonymousLoginFailure(login, scanIp);
|
||||
}
|
||||
assert.equal(await first.anonymousLoginLockScope(`fresh-${runId}@integration.invalid`, scanIp), 'ip');
|
||||
await ttlInRange(scanFailureKey, 600, 900);
|
||||
await ttlInRange(scanLockKey, 600, 900);
|
||||
|
||||
const listedKeys = await redis.keys(`cmpp:auth:*${runId}*`);
|
||||
assert.equal(listedKeys.length, 0, 'raw login/run identifiers must not appear in Redis keys');
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
status: 'passed',
|
||||
sharedSession: true,
|
||||
oneTimeCaptcha: true,
|
||||
captchaRateLimit: true,
|
||||
accountAndPairLock: true,
|
||||
randomAccountIpLock: true,
|
||||
ttlVerified: true,
|
||||
hashedKeysOnly: true,
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
if (keys.size > 0) await redis.del(...keys);
|
||||
await first.onModuleDestroy();
|
||||
await second.onModuleDestroy();
|
||||
redis.disconnect();
|
||||
}
|
||||
Reference in New Issue
Block a user