Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
2.5 KB typescript
import { beforeEach, describe, expect, test, vi } from 'vitest';

const calls: { file: string; args: readonly string[]; input?: string }[] = [];
let gate: Promise<void> = Promise.resolve();

vi.mock('../../server/utils/cmd', () => ({
  execFile: vi.fn(
    async (
      file: string,
      args: readonly string[],
      options: { input?: string } = {}
    ) => {
      calls.push({ file, args, input: options.input });
      await gate;
      return '';
    }
  ),
}));

const { firewall } = await import('../../server/utils/firewall');

function state(name: string) {
  return {
    wgInterface: { name: 'wg0', firewallEnabled: true } as never,
    clients: [
      {
        id: 1,
        name,
        ipv4Address: '10.8.0.2',
        ipv6Address: 'fdcc::2',
        allowedIps: ['10.0.0.1'],
        firewallIps: null,
        enabled: true,
      },
    ],
    userConfig: { defaultAllowedIps: [] } as never,
  };
}

function rebuild(name: string) {
  const s = state(name);
  return firewall.rebuildRules(s.wgInterface, s.clients, s.userConfig, true);
}

function appliedNames() {
  return calls
    .filter((c) => c.file === 'iptables-restore')
    .map((c) => c.input?.match(/client 1: (\w+)/)?.[1]);
}

describe('firewall rebuild queue', () => {
  beforeEach(() => {
    calls.length = 0;
    gate = Promise.resolve();
  });

  test('applies with a single restore per family instead of one process per rule', async () => {
    await rebuild('first');
    expect(calls.map((c) => c.file)).toEqual([
      'iptables-restore',
      'iptables',
      'ip6tables-restore',
      'ip6tables',
    ]);
  });

  test('coalesces requests queued during a running rebuild and applies only the latest state', async () => {
    let release!: () => void;
    gate = new Promise((r) => (release = r));

    const running = rebuild('running');
    await Promise.resolve();
    const stale = rebuild('stale');
    const latest = rebuild('latest');

    expect(stale).toBe(latest);

    release();
    await Promise.all([running, stale, latest]);

    expect(appliedNames()).toEqual(['running', 'latest']);
  });

  test('a caller resolves only after its own state has been applied', async () => {
    let release!: () => void;
    gate = new Promise((r) => (release = r));

    const running = rebuild('running');
    await Promise.resolve();
    let queuedApplied = false;
    const queued = rebuild('queued').then(() => {
      queuedApplied = appliedNames().includes('queued');
    });

    release();
    await Promise.all([running, queued]);
    expect(queuedApplied).toBe(true);
  });
});