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
217 lines
6.0 KiB
TypeScript
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
|
|
});
|
|
});
|