Files
nanoclaw/src/sender-allowlist.test.ts
Claude 30ebcaa61e feat: add ESLint with error-handling rules
Add ESLint v9.35+ with typescript-eslint recommended config and
error-handling rules: preserve-caught-error (enforces { cause } when
re-throwing), no-unused-vars with caughtErrors:all, and
eslint-plugin-no-catch-all (warns on catch blocks that don't rethrow).

Fix existing violations: add error cause to container-runtime rethrow,
prefix unused vars with underscore, remove unused imports.

https://claude.ai/code/session_01JPjzhBp9PR5LtfLWVDrYrH
2026-03-21 11:57:22 +02:00

217 lines
6.0 KiB
TypeScript

import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
isSenderAllowed,
isTriggerAllowed,
loadSenderAllowlist,
SenderAllowlistConfig,
shouldDropMessage,
} from './sender-allowlist.js';
let tmpDir: string;
function cfgPath(name = 'sender-allowlist.json'): string {
return path.join(tmpDir, name);
}
function writeConfig(config: unknown, name?: string): string {
const p = cfgPath(name);
fs.writeFileSync(p, JSON.stringify(config));
return p;
}
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'allowlist-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('loadSenderAllowlist', () => {
it('returns allow-all defaults when file is missing', () => {
const cfg = loadSenderAllowlist(cfgPath());
expect(cfg.default.allow).toBe('*');
expect(cfg.default.mode).toBe('trigger');
expect(cfg.logDenied).toBe(true);
});
it('loads allow=* config', () => {
const p = writeConfig({
default: { allow: '*', mode: 'trigger' },
chats: {},
logDenied: false,
});
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toBe('*');
expect(cfg.logDenied).toBe(false);
});
it('loads allow=[] (deny all)', () => {
const p = writeConfig({
default: { allow: [], mode: 'trigger' },
chats: {},
});
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toEqual([]);
});
it('loads allow=[list]', () => {
const p = writeConfig({
default: { allow: ['alice', 'bob'], mode: 'drop' },
chats: {},
});
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toEqual(['alice', 'bob']);
expect(cfg.default.mode).toBe('drop');
});
it('per-chat override beats default', () => {
const p = writeConfig({
default: { allow: '*', mode: 'trigger' },
chats: { 'group-a': { allow: ['alice'], mode: 'drop' } },
});
const cfg = loadSenderAllowlist(p);
expect(cfg.chats['group-a'].allow).toEqual(['alice']);
expect(cfg.chats['group-a'].mode).toBe('drop');
});
it('returns allow-all on invalid JSON', () => {
const p = cfgPath();
fs.writeFileSync(p, '{ not valid json }}}');
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toBe('*');
});
it('returns allow-all on invalid schema', () => {
const p = writeConfig({ default: { oops: true } });
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toBe('*');
});
it('rejects non-string allow array items', () => {
const p = writeConfig({
default: { allow: [123, null, true], mode: 'trigger' },
chats: {},
});
const cfg = loadSenderAllowlist(p);
expect(cfg.default.allow).toBe('*'); // falls back to default
});
it('skips invalid per-chat entries', () => {
const p = writeConfig({
default: { allow: '*', mode: 'trigger' },
chats: {
good: { allow: ['alice'], mode: 'trigger' },
bad: { allow: 123 },
},
});
const cfg = loadSenderAllowlist(p);
expect(cfg.chats['good']).toBeDefined();
expect(cfg.chats['bad']).toBeUndefined();
});
});
describe('isSenderAllowed', () => {
it('allow=* allows any sender', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: '*', mode: 'trigger' },
chats: {},
logDenied: true,
};
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(true);
});
it('allow=[] denies any sender', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: [], mode: 'trigger' },
chats: {},
logDenied: true,
};
expect(isSenderAllowed('g1', 'anyone', cfg)).toBe(false);
});
it('allow=[list] allows exact match only', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: ['alice', 'bob'], mode: 'trigger' },
chats: {},
logDenied: true,
};
expect(isSenderAllowed('g1', 'alice', cfg)).toBe(true);
expect(isSenderAllowed('g1', 'eve', cfg)).toBe(false);
});
it('uses per-chat entry over default', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: '*', mode: 'trigger' },
chats: { g1: { allow: ['alice'], mode: 'trigger' } },
logDenied: true,
};
expect(isSenderAllowed('g1', 'bob', cfg)).toBe(false);
expect(isSenderAllowed('g2', 'bob', cfg)).toBe(true);
});
});
describe('shouldDropMessage', () => {
it('returns false for trigger mode', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: '*', mode: 'trigger' },
chats: {},
logDenied: true,
};
expect(shouldDropMessage('g1', cfg)).toBe(false);
});
it('returns true for drop mode', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: '*', mode: 'drop' },
chats: {},
logDenied: true,
};
expect(shouldDropMessage('g1', cfg)).toBe(true);
});
it('per-chat mode override', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: '*', mode: 'trigger' },
chats: { g1: { allow: '*', mode: 'drop' } },
logDenied: true,
};
expect(shouldDropMessage('g1', cfg)).toBe(true);
expect(shouldDropMessage('g2', cfg)).toBe(false);
});
});
describe('isTriggerAllowed', () => {
it('allows trigger for allowed sender', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: ['alice'], mode: 'trigger' },
chats: {},
logDenied: false,
};
expect(isTriggerAllowed('g1', 'alice', cfg)).toBe(true);
});
it('denies trigger for disallowed sender', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: ['alice'], mode: 'trigger' },
chats: {},
logDenied: false,
};
expect(isTriggerAllowed('g1', 'eve', cfg)).toBe(false);
});
it('logs when logDenied is true', () => {
const cfg: SenderAllowlistConfig = {
default: { allow: ['alice'], mode: 'trigger' },
chats: {},
logDenied: true,
};
isTriggerAllowed('g1', 'eve', cfg);
// Logger.debug is called — we just verify no crash; logger is a real pino instance
});
});