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(); }