项目迁移
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { createAuthStateStore } from '../src/security/auth-state.mjs';
|
||||
|
||||
class FakeRedisClient extends EventEmitter {
|
||||
constructor({ connectError = null } = {}) {
|
||||
super();
|
||||
this.connectError = connectError;
|
||||
this.isOpen = false;
|
||||
this.values = new Map();
|
||||
this.sets = new Map();
|
||||
this.hashes = new Map();
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this.connectError) throw this.connectError;
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
return this.values.has(key) ? this.values.get(key) : null;
|
||||
}
|
||||
|
||||
async set(key, value) {
|
||||
this.values.set(key, String(value));
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
async del(key) {
|
||||
const deleted = Number(this.values.delete(key)) + Number(this.sets.delete(key)) + Number(this.hashes.delete(key));
|
||||
return deleted ? 1 : 0;
|
||||
}
|
||||
|
||||
async sAdd(key, value) {
|
||||
if (!this.sets.has(key)) this.sets.set(key, new Set());
|
||||
const before = this.sets.get(key).size;
|
||||
this.sets.get(key).add(value);
|
||||
return this.sets.get(key).size - before;
|
||||
}
|
||||
|
||||
async sRem(key, value) {
|
||||
return Number(this.sets.get(key)?.delete(value) || false);
|
||||
}
|
||||
|
||||
async sMembers(key) {
|
||||
return [...(this.sets.get(key) || [])];
|
||||
}
|
||||
|
||||
async expire() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
async hSet(key, entries) {
|
||||
if (!this.hashes.has(key)) this.hashes.set(key, new Map());
|
||||
for (const [field, value] of Object.entries(entries)) this.hashes.get(key).set(field, String(value));
|
||||
return Object.keys(entries).length;
|
||||
}
|
||||
|
||||
async hGetAll(key) {
|
||||
return Object.fromEntries(this.hashes.get(key) || []);
|
||||
}
|
||||
|
||||
async eval(_script, { keys, arguments: scriptArguments }) {
|
||||
const hash = this.hashes.get(keys[0]);
|
||||
if (!hash) return -1;
|
||||
const attempts = Number(hash.get('attempts') || 0) + 1;
|
||||
hash.set('attempts', String(attempts));
|
||||
if (attempts >= Number(scriptArguments[0])) this.hashes.delete(keys[0]);
|
||||
return attempts;
|
||||
}
|
||||
|
||||
multi() {
|
||||
const operations = [];
|
||||
const transaction = {};
|
||||
for (const method of ['set', 'del', 'sAdd', 'sRem', 'expire', 'hSet']) {
|
||||
transaction[method] = (...args) => {
|
||||
operations.push(() => this[method](...args));
|
||||
return transaction;
|
||||
};
|
||||
}
|
||||
transaction.exec = async () => Promise.all(operations.map(operation => operation()));
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async quit() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
const silentLogger = { error() {} };
|
||||
|
||||
{
|
||||
const state = await createAuthStateStore({ env: {} });
|
||||
assert.equal(state.status, 'disabled');
|
||||
assert.equal(state.backend, 'memory');
|
||||
await state.createSession('session-1', 'user-1');
|
||||
assert.deepEqual(await state.getSession('session-1'), { userId: 'user-1' });
|
||||
await state.createLoginChallenge('challenge-1', 'user-1');
|
||||
assert.deepEqual(await state.getLoginChallenge('challenge-1'), { userId: 'user-1', attempts: 0 });
|
||||
await state.createTotpSetup('session-1', 'user-1', 'SECRET');
|
||||
assert.deepEqual(await state.getTotpSetup('session-1'), { userId: 'user-1', secret: 'SECRET' });
|
||||
assert.equal(await state.deleteUserSessions('user-1'), 1);
|
||||
assert.equal(await state.getSession('session-1'), null);
|
||||
await state.close();
|
||||
}
|
||||
|
||||
{
|
||||
const client = new FakeRedisClient();
|
||||
let clientOptions;
|
||||
const state = await createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://cache.example:6379/0', REDIS_SESSION_PREFIX: 'test:auth' },
|
||||
logger: silentLogger,
|
||||
clientFactory(options) {
|
||||
clientOptions = options;
|
||||
return client;
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(state.status, 'ready');
|
||||
assert.equal(state.backend, 'redis');
|
||||
assert.equal(state.database, 1, '普通缓存使用 DB 0 时,认证状态应自动使用 DB 1');
|
||||
assert.equal(clientOptions.database, 1);
|
||||
assert.equal(clientOptions.url, 'redis://cache.example:6379/0');
|
||||
|
||||
await state.createSession('session-1', 'user-1');
|
||||
await state.createSession('session-2', 'user-1');
|
||||
assert.deepEqual(await state.getSession('session-1'), { userId: 'user-1' });
|
||||
assert.equal(await state.deleteUserSessions('user-1'), 2);
|
||||
assert.equal(await state.getSession('session-1'), null);
|
||||
assert.equal(await state.getSession('session-2'), null);
|
||||
|
||||
await state.createLoginChallenge('challenge-1', 'user-1');
|
||||
for (let attempts = 1; attempts <= 4; attempts += 1) {
|
||||
assert.deepEqual(await state.recordLoginChallengeFailure('challenge-1', 5), { attempts, exhausted: false });
|
||||
}
|
||||
assert.deepEqual(await state.recordLoginChallengeFailure('challenge-1', 5), { attempts: 5, exhausted: true });
|
||||
assert.equal(await state.getLoginChallenge('challenge-1'), null);
|
||||
|
||||
await state.createTotpSetup('session-3', 'user-1', 'SECRET');
|
||||
assert.deepEqual(await state.getTotpSetup('session-3'), { userId: 'user-1', secret: 'SECRET' });
|
||||
await state.deleteTotpSetup('session-3');
|
||||
assert.equal(await state.getTotpSetup('session-3'), null);
|
||||
await state.close();
|
||||
assert.equal(client.isOpen, false);
|
||||
}
|
||||
|
||||
{
|
||||
const client = new FakeRedisClient();
|
||||
let options;
|
||||
const state = await createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://cache.example:6379/0', REDIS_SESSION_URL: 'rediss://sessions.example:6380/4' },
|
||||
logger: silentLogger,
|
||||
clientFactory(clientOptions) {
|
||||
options = clientOptions;
|
||||
return client;
|
||||
}
|
||||
});
|
||||
assert.equal(options.url, 'rediss://sessions.example:6380/4');
|
||||
assert.equal(options.database, 4);
|
||||
await state.close();
|
||||
}
|
||||
|
||||
{
|
||||
const client = new FakeRedisClient();
|
||||
let options;
|
||||
const state = await createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://cache.example:6379/1', REDIS_SESSION_DB: '0' },
|
||||
logger: silentLogger,
|
||||
clientFactory(clientOptions) {
|
||||
options = clientOptions;
|
||||
return client;
|
||||
}
|
||||
});
|
||||
assert.equal(options.database, 0, '应允许显式选择 DB 0,只要普通缓存使用不同的 DB');
|
||||
await state.close();
|
||||
}
|
||||
|
||||
await assert.rejects(
|
||||
createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://same.example:6379/0', REDIS_SESSION_URL: 'redis://same.example:6379/0' },
|
||||
logger: silentLogger,
|
||||
clientFactory: () => new FakeRedisClient()
|
||||
}),
|
||||
/必须使用与普通缓存不同的逻辑数据库/
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://cache-user@same.example:6379/0', REDIS_SESSION_URL: 'redis://session-user@same.example:6379/0' },
|
||||
logger: silentLogger,
|
||||
clientFactory: () => new FakeRedisClient()
|
||||
}),
|
||||
/必须使用与普通缓存不同的逻辑数据库/
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
createAuthStateStore({
|
||||
env: { REDIS_URL: 'redis://unavailable.example:6379/0' },
|
||||
logger: silentLogger,
|
||||
clientFactory: () => new FakeRedisClient({ connectError: new Error('connection refused') })
|
||||
}),
|
||||
/认证状态存储连接失败/
|
||||
);
|
||||
|
||||
console.log('✓ 认证状态本机回退、独立 Redis DB、会话失效与 TOTP 临时状态');
|
||||
Reference in New Issue
Block a user