const assert = require('assert'); const { createRedis } = require('./redis'); function loadEnv() { const fs = require('fs'); const path = require('path'); const file = path.join(__dirname, '.env'); if (!fs.existsSync(file)) return; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); if (m && !(m[1] in process.env)) process.env[m[1]] = m[2]; } } async function main() { loadEnv(); const url = process.env.REDIS_URL || `redis://:${process.env.REDIS_PASSWORD || ''}@127.0.0.1:6379`; const prefix = 'test-' + Date.now(); const cache = createRedis({ url, prefix }); const connected = await cache.connect(); console.log('connect:', connected, 'ready:', cache.isReady()); assert.strictEqual(connected, true, 'must connect'); assert.strictEqual(cache.isReady(), true, 'must be ready'); await cache.set('setting:foo', 'bar', 60000); assert.strictEqual(await cache.get('setting:foo'), 'bar', 'string roundtrip'); await cache.set('obj', { a: 1, b: [2, 3], c: null }, 60000); assert.deepStrictEqual(await cache.get('obj'), { a: 1, b: [2, 3], c: null }, 'json roundtrip'); assert.strictEqual(await cache.get('missing'), undefined, 'miss returns undefined'); await cache.set('setting:ttl', 'x', 120); await new Promise(r => setTimeout(r, 250)); assert.strictEqual(await cache.get('setting:ttl'), undefined, 'ttl expiry'); await cache.set('groups:1', 'g1', 60000); await cache.set('groups:2', 'g2', 60000); await cache.set('students:1', 's1', 60000); await cache.dropPrefix('groups:'); assert.strictEqual(await cache.get('groups:1'), undefined, 'dropPrefix removes groups:1'); assert.strictEqual(await cache.get('groups:2'), undefined, 'dropPrefix removes groups:2'); assert.strictEqual(await cache.get('students:1'), 's1', 'dropPrefix keeps students:1'); await cache.set('fail:login:1.2.3.4', 3, 60000); await cache.set('fail:login:5.6.7.8', 9, 60000); await cache.dropMatch('fail:*:1.2.3.4'); assert.strictEqual(await cache.get('fail:login:1.2.3.4'), undefined, 'dropMatch suffix removes target'); assert.strictEqual(await cache.get('fail:login:5.6.7.8'), 9, 'dropMatch keeps other ip'); const c1 = await cache.incr('counter:a', 60000); const c2 = await cache.incr('counter:a', 60000); assert.strictEqual(c1, 1, 'incr first = 1'); assert.strictEqual(c2, 2, 'incr second = 2'); await cache.incr('counter:short', 120); await new Promise(r => setTimeout(r, 250)); assert.strictEqual(await cache.incr('counter:short', 60000), 1, 'incr ttl set on first call'); const hits = []; const received = new Promise(resolve => { cache.on('test:chan', msg => { hits.push(msg); resolve(msg); }); }); await new Promise(r => setTimeout(r, 200)); await cache.publish('test:chan', { hello: 'world' }); const got = await Promise.race([received, new Promise(r => setTimeout(() => r('TIMEOUT'), 3000))]); assert.notStrictEqual(got, 'TIMEOUT', 'pubsub must deliver'); assert.deepStrictEqual(JSON.parse(got), { hello: 'world' }, 'pubsub payload'); assert.strictEqual(hits.length, 1, 'pubsub delivered exactly once (no double delivery)'); let wraps = 0; const v1 = await cache.wrap('wrap:key', 60000, async () => { wraps += 1; return 'computed'; }); const v2 = await cache.wrap('wrap:key', 60000, async () => { wraps += 1; return 'other'; }); assert.strictEqual(v1, 'computed', 'wrap returns computed'); assert.strictEqual(v2, 'computed', 'wrap returns cached'); assert.strictEqual(wraps, 1, 'wrap computed once'); const store = cache.rateLimitStore('test', 60000); await store.init({ windowMs: 60000 }); const r1 = await store.increment('ip1'); const r2 = await store.increment('ip1'); const r3 = await store.increment('ip2'); assert.strictEqual(r1.totalHits, 1, 'rl first'); assert.strictEqual(r2.totalHits, 2, 'rl second'); assert.strictEqual(r3.totalHits, 1, 'rl separate key'); assert.ok(r2.resetTime > Date.now(), 'rl resetTime in future'); assert.ok(r2.resetTime instanceof Date, 'rl resetTime is a Date'); assert.strictEqual(r1.resetTime.getTime(), r2.resetTime.getTime(), 'rl same window'); const ttl = await cache.info(); await store.decrement('ip1'); assert.strictEqual((await store.increment('ip1')).totalHits, 2, 'decrement then increment'); await store.resetKey('ip1'); assert.strictEqual((await store.increment('ip1')).totalHits, 1, 'resetKey clears'); await store.resetAll(); assert.strictEqual((await store.increment('ip1')).totalHits, 1, 'resetAll clears'); await cache.del('obj'); assert.strictEqual(await cache.get('obj'), undefined, 'del'); const info = await cache.info(); console.log('info:', JSON.stringify(info)); assert.strictEqual(info.driver, 'redis', 'info driver is redis'); assert.ok(info.used_memory_bytes > 0, 'info has memory'); await cache.clear(); assert.strictEqual(await cache.get('students:1'), undefined, 'clear removes all'); const other = createRedis({ url, prefix: prefix + '-other' }); await other.connect(); await other.set('iso', 'yes', 60000); assert.strictEqual(await cache.get('iso'), undefined, 'namespaces are isolated'); await other.close(); await cache.close(); assert.strictEqual(cache.isReady(), false, 'closed'); const offline = createRedis({ url: 'redis://127.0.0.1:1/', prefix: 'off-' + Date.now() }); const okConn = await offline.connect(); assert.strictEqual(okConn, false, 'bad url must not throw'); await offline.set('k', 'v', 1000); assert.strictEqual(await offline.get('k'), 'v', 'fallback set/get works'); await offline.set('groups:a', 1, 1000); await offline.set('students:a', 1, 1000); await offline.dropPrefix('groups:'); assert.strictEqual(await offline.get('groups:a'), undefined, 'fallback dropPrefix'); assert.strictEqual(await offline.get('students:a'), 1, 'fallback dropPrefix isolation'); assert.strictEqual(await offline.incr('c', 1000), 1, 'fallback incr'); assert.strictEqual(await offline.incr('c', 1000), 2, 'fallback incr 2'); const offStore = offline.rateLimitStore('t', 60000); assert.strictEqual((await offStore.increment('k1')).totalHits, 1, 'fallback rl'); assert.strictEqual((await offStore.increment('k1')).totalHits, 2, 'fallback rl 2'); let localHit = null; const localRecv = new Promise(r => { offline.on('c', m => { localHit = m; r(m); }); }); await offline.publish('c', { x: 1 }); assert.deepStrictEqual(JSON.parse(await Promise.race([localRecv, new Promise(r => setTimeout(() => r('T'), 2000))])), { x: 1 }, 'fallback local pubsub'); assert.deepStrictEqual(JSON.parse(localHit), { x: 1 }, 'fallback local payload'); await offline.close(); const disabled = createRedis({}); assert.strictEqual(await disabled.connect(), false, 'disabled connect false'); await disabled.set('a', 1, 1000); assert.strictEqual(await disabled.get('a'), 1, 'disabled memory works'); await disabled.close(); console.log('\nALL REDIS TESTS PASSED'); process.exit(0); } main().catch(e => { console.error('FAILED:', e.message); console.error(e.stack); process.exit(1); });