Files
waggle-os/packages/server/tests/channels-pairing.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

128 lines
5.1 KiB
TypeScript

/**
* PairingStore — deny-by-default pairing, code lifecycle, workspace
* overrides, and channels.json persistence (CHANNELS-ARC P1).
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { PairingStore, PAIRING_CODE_TTL_MS } from '../src/local/channels/pairing.js';
let dir: string;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-channels-'));
});
afterEach(() => {
vi.useRealTimers();
fs.rmSync(dir, { recursive: true, force: true });
});
describe('PairingStore codes', () => {
it('generates an 8-char unambiguous code with a 10-minute expiry', () => {
const store = new PairingStore(dir);
const before = Date.now();
const { code, expiresAt } = store.generateCode('telegram');
expect(code).toMatch(/^[A-HJ-NP-Z2-9]{8}$/);
expect(expiresAt).toBeGreaterThanOrEqual(before + PAIRING_CODE_TTL_MS - 1000);
});
it('consumes a valid code exactly once and pairs the sender', () => {
const store = new PairingStore(dir);
const { code } = store.generateCode('telegram');
expect(store.consumeCode('telegram', code, 'user-1', 'marko')).toBe(true);
expect(store.isPaired('telegram', 'user-1')).toBe(true);
// Single-use: second redemption fails.
expect(store.consumeCode('telegram', code, 'user-2')).toBe(false);
expect(store.isPaired('telegram', 'user-2')).toBe(false);
});
it('is case-insensitive on redemption (phone keyboards autocapitalize)', () => {
const store = new PairingStore(dir);
const { code } = store.generateCode('telegram');
expect(store.consumeCode('telegram', code.toLowerCase(), 'user-1')).toBe(true);
});
it('rejects a code minted for another platform', () => {
const store = new PairingStore(dir);
const { code } = store.generateCode('discord');
expect(store.consumeCode('telegram', code, 'user-1')).toBe(false);
});
it('rejects expired codes', () => {
vi.useFakeTimers();
const store = new PairingStore(dir);
const { code } = store.generateCode('telegram');
vi.advanceTimersByTime(PAIRING_CODE_TTL_MS + 1000);
expect(store.consumeCode('telegram', code, 'user-1')).toBe(false);
});
it('rejects garbage codes without pairing', () => {
const store = new PairingStore(dir);
expect(store.consumeCode('telegram', 'NOTACODE', 'user-1')).toBe(false);
expect(store.isPaired('telegram', 'user-1')).toBe(false);
});
});
describe('PairingStore persistence', () => {
it('persists allowlist across store instances (channels.json)', () => {
const a = new PairingStore(dir);
const { code } = a.generateCode('telegram');
a.consumeCode('telegram', code, 'user-1', 'marko');
const b = new PairingStore(dir);
expect(b.isPaired('telegram', 'user-1')).toBe(true);
expect(b.listPaired().telegram?.[0]?.senderName).toBe('marko');
});
it('does NOT persist pending codes (in-memory only)', () => {
const a = new PairingStore(dir);
const { code } = a.generateCode('telegram');
const b = new PairingStore(dir);
expect(b.consumeCode('telegram', code, 'user-1')).toBe(false);
});
it('never writes secrets: channels.json contains no token-like keys', () => {
const store = new PairingStore(dir);
store.setConfig('telegram', { enabled: true, defaultWorkspace: 'ws-1' });
const raw = fs.readFileSync(path.join(dir, 'channels', 'channels.json'), 'utf8');
expect(raw).not.toMatch(/token|secret|password/i);
});
it('survives a corrupt channels.json by starting empty', () => {
fs.mkdirSync(path.join(dir, 'channels'), { recursive: true });
fs.writeFileSync(path.join(dir, 'channels', 'channels.json'), '{corrupt', 'utf8');
const store = new PairingStore(dir);
expect(store.isPaired('telegram', 'anyone')).toBe(false);
expect(store.getConfig('telegram')).toEqual({ enabled: false, defaultWorkspace: 'default' });
});
});
describe('PairingStore unpair + overrides + config', () => {
it('unpairs a sender and reports whether anything was removed', () => {
const store = new PairingStore(dir);
const { code } = store.generateCode('telegram');
store.consumeCode('telegram', code, 'user-1');
expect(store.unpair('telegram', 'user-1')).toBe(true);
expect(store.isPaired('telegram', 'user-1')).toBe(false);
expect(store.unpair('telegram', 'user-1')).toBe(false);
});
it('stores, persists, and clears per-chat workspace overrides', () => {
const a = new PairingStore(dir);
a.setWorkspaceOverride('telegram', 'chat-9', 'ws-research');
expect(new PairingStore(dir).getWorkspaceOverride('telegram', 'chat-9')).toBe('ws-research');
a.setWorkspaceOverride('telegram', 'chat-9', null);
expect(a.getWorkspaceOverride('telegram', 'chat-9')).toBeUndefined();
});
it('defaults config to disabled/default workspace and persists updates', () => {
const a = new PairingStore(dir);
expect(a.getConfig('slack')).toEqual({ enabled: false, defaultWorkspace: 'default' });
a.setConfig('slack', { enabled: true, defaultWorkspace: 'ws-team' });
expect(new PairingStore(dir).getConfig('slack')).toEqual({ enabled: true, defaultWorkspace: 'ws-team' });
});
});