2026-09-02 17:42:23 +03:00
|
|
|
import { describe, expect, mock, test } from 'bun:test';
|
|
|
|
|
import type { RelayTunnelStatus } from '@/lib/relay/tunnel-client';
|
|
|
|
|
import type { DesktopHostRelay } from './desktopHosts';
|
|
|
|
|
|
|
|
|
|
type TunnelStub = {
|
|
|
|
|
fetch: (path: string, init?: RequestInit) => Promise<Response>;
|
|
|
|
|
getStatus: () => RelayTunnelStatus;
|
|
|
|
|
close: () => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let nextTunnel: (() => TunnelStub) | null = null;
|
|
|
|
|
const tunnelModule = await import('@/lib/relay/tunnel-client');
|
|
|
|
|
mock.module('@/lib/relay/tunnel-client', () => ({
|
|
|
|
|
...tunnelModule,
|
|
|
|
|
createRelayTunnelClient: () => {
|
|
|
|
|
if (!nextTunnel) throw new Error('no tunnel stub registered');
|
|
|
|
|
return nextTunnel();
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { desktopHostProbe, desktopHostsGet, desktopHostsSet, importDesktopHostPairing, probeRelayDesktopHost, redactSensitiveUrl, resolveDesktopHostUrl } = await import('./desktopHosts');
|
2026-06-30 00:30:48 +03:00
|
|
|
|
|
|
|
|
const withDesktopBridge = async <T>(handler: (cmd: string, args: Record<string, unknown>) => unknown | Promise<unknown>, run: () => Promise<T>): Promise<T> => {
|
|
|
|
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
|
|
|
Object.defineProperty(globalThis, 'window', {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: {
|
|
|
|
|
__OPENCHAMBER_DESKTOP__: {
|
|
|
|
|
invoke: handler,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
return await run();
|
|
|
|
|
} finally {
|
|
|
|
|
if (previousWindow) {
|
|
|
|
|
Object.defineProperty(globalThis, 'window', previousWindow);
|
|
|
|
|
} else {
|
|
|
|
|
Reflect.deleteProperty(globalThis, 'window');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-06-02 00:43:05 +03:00
|
|
|
|
|
|
|
|
describe('resolveDesktopHostUrl', () => {
|
|
|
|
|
test('keeps regular host URLs unchanged', () => {
|
|
|
|
|
expect(resolveDesktopHostUrl('https://example.com/app?x=1')).toEqual({
|
|
|
|
|
persistedUrl: 'https://example.com/app?x=1',
|
|
|
|
|
redeemUrl: null,
|
|
|
|
|
kind: 'normal-host',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('detects tunnel connect links and stores only origin', () => {
|
|
|
|
|
expect(resolveDesktopHostUrl('https://example.trycloudflare.com/connect?t=secret-token')).toEqual({
|
|
|
|
|
persistedUrl: 'https://example.trycloudflare.com',
|
|
|
|
|
redeemUrl: 'https://example.trycloudflare.com/connect?t=secret-token',
|
|
|
|
|
kind: 'tunnel-connect-link',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('detects tunnel connect links with trailing slash', () => {
|
|
|
|
|
expect(resolveDesktopHostUrl('https://example.trycloudflare.com/connect/?t=secret-token#section')).toEqual({
|
|
|
|
|
persistedUrl: 'https://example.trycloudflare.com',
|
|
|
|
|
redeemUrl: 'https://example.trycloudflare.com/connect/?t=secret-token',
|
|
|
|
|
kind: 'tunnel-connect-link',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('redacts tunnel tokens from labels', () => {
|
|
|
|
|
expect(redactSensitiveUrl('https://example.trycloudflare.com/connect?t=secret-token')).toBe(
|
|
|
|
|
'https://example.trycloudflare.com/connect?t=%5BREDACTED%5D',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-30 00:30:48 +03:00
|
|
|
|
2026-07-21 21:11:13 +03:00
|
|
|
describe('importDesktopHostPairing', () => {
|
|
|
|
|
test('rejects malformed pairing links before changing hosts', async () => {
|
|
|
|
|
await expect(importDesktopHostPairing('not-a-connect-link', [])).rejects.toThrow('invalid-connect-link');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-30 00:30:48 +03:00
|
|
|
describe('desktop host runtime headers', () => {
|
|
|
|
|
test('parses persisted request headers from desktop config', async () => {
|
|
|
|
|
await withDesktopBridge(async (cmd) => {
|
|
|
|
|
expect(cmd).toBe('desktop_hosts_get');
|
|
|
|
|
return {
|
|
|
|
|
hosts: [{
|
|
|
|
|
id: 'remote-1',
|
|
|
|
|
label: 'Remote',
|
|
|
|
|
url: 'https://remote.example',
|
|
|
|
|
requestHeaders: {
|
|
|
|
|
' CF-Access-Client-Id ': ' client-id ',
|
|
|
|
|
Authorization: 'Bearer should-not-be-read',
|
|
|
|
|
'Bad:Name': 'bad',
|
|
|
|
|
},
|
|
|
|
|
}],
|
|
|
|
|
defaultHostId: 'remote-1',
|
|
|
|
|
initialHostChoiceCompleted: true,
|
|
|
|
|
};
|
|
|
|
|
}, async () => {
|
|
|
|
|
const config = await desktopHostsGet();
|
|
|
|
|
expect(config.hosts[0]?.requestHeaders).toEqual({
|
|
|
|
|
'CF-Access-Client-Id': 'client-id',
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('passes request headers through host save and probe IPC calls', async () => {
|
|
|
|
|
const calls: Array<{ cmd: string; args: Record<string, unknown> }> = [];
|
|
|
|
|
await withDesktopBridge(async (cmd, args) => {
|
|
|
|
|
calls.push({ cmd, args });
|
|
|
|
|
if (cmd === 'desktop_host_probe') return { status: 'ok', latencyMs: 7 };
|
|
|
|
|
return null;
|
|
|
|
|
}, async () => {
|
|
|
|
|
const requestHeaders = { 'CF-Access-Client-Id': 'client-id' };
|
|
|
|
|
await desktopHostsSet({
|
|
|
|
|
hosts: [{ id: 'remote-1', label: 'Remote', url: 'https://remote.example', requestHeaders }],
|
|
|
|
|
defaultHostId: 'remote-1',
|
|
|
|
|
});
|
|
|
|
|
const probe = await desktopHostProbe('https://remote.example', { requestHeaders });
|
|
|
|
|
expect(probe).toEqual({ status: 'ok', latencyMs: 7 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(calls[0]).toEqual({
|
|
|
|
|
cmd: 'desktop_hosts_set',
|
|
|
|
|
args: {
|
|
|
|
|
input: {
|
|
|
|
|
hosts: [{ id: 'remote-1', label: 'Remote', url: 'https://remote.example', requestHeaders: { 'CF-Access-Client-Id': 'client-id' } }],
|
|
|
|
|
defaultHostId: 'remote-1',
|
|
|
|
|
initialHostChoiceCompleted: undefined,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
expect(calls[1]).toEqual({
|
|
|
|
|
cmd: 'desktop_host_probe',
|
|
|
|
|
args: {
|
|
|
|
|
url: 'https://remote.example',
|
|
|
|
|
requestHeaders: { 'CF-Access-Client-Id': 'client-id' },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-09-02 17:42:23 +03:00
|
|
|
|
|
|
|
|
describe('probeRelayDesktopHost', () => {
|
|
|
|
|
const relay: DesktopHostRelay = {
|
|
|
|
|
relayUrl: 'wss://relay.example',
|
|
|
|
|
serverId: 'server-a',
|
|
|
|
|
hostEncPubJwk: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const withTimerWindow = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
|
|
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
|
|
|
Object.defineProperty(globalThis, 'window', {
|
|
|
|
|
configurable: true,
|
|
|
|
|
value: { setTimeout: setTimeout.bind(globalThis), clearTimeout: clearTimeout.bind(globalThis) },
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
return await run();
|
|
|
|
|
} finally {
|
|
|
|
|
if (previousWindow) {
|
|
|
|
|
Object.defineProperty(globalThis, 'window', previousWindow);
|
|
|
|
|
} else {
|
|
|
|
|
Reflect.deleteProperty(globalThis, 'window');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stubTunnel = (
|
|
|
|
|
responses: Array<Response | Error>,
|
|
|
|
|
state: RelayTunnelStatus['state'] = 'reconnecting',
|
|
|
|
|
) => {
|
|
|
|
|
const calls: string[] = [];
|
|
|
|
|
let closed = false;
|
|
|
|
|
nextTunnel = () => ({
|
|
|
|
|
fetch: async (path) => {
|
|
|
|
|
calls.push(path);
|
|
|
|
|
const next = responses.shift();
|
|
|
|
|
if (!next) throw new Error('relay tunnel reset');
|
|
|
|
|
if (next instanceof Error) throw next;
|
|
|
|
|
return next;
|
|
|
|
|
},
|
|
|
|
|
getStatus: () => ({ state }),
|
|
|
|
|
close: () => { closed = true; },
|
|
|
|
|
});
|
|
|
|
|
return { calls, isClosed: () => closed };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test('a cold first attempt is retried instead of reported unreachable', async () => {
|
|
|
|
|
// The tunnel rejects waiters on its first failed connect and then
|
|
|
|
|
// reconnects; the probe must span that, not read it as an unreachable host.
|
|
|
|
|
const tunnel = stubTunnel([
|
|
|
|
|
new Error('relay tunnel reset: connection failed'),
|
|
|
|
|
new Response('{}', { status: 200 }),
|
|
|
|
|
new Response('{}', { status: 200 }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await withTimerWindow(() => probeRelayDesktopHost(relay, { clientToken: 'token' }));
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe('ok');
|
|
|
|
|
expect(tunnel.calls).toEqual(['/health', '/health', '/auth/session']);
|
|
|
|
|
expect(tunnel.isClosed()).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a terminal tunnel state ends the probe without retrying', async () => {
|
|
|
|
|
// Auth failed / duplicate client / limit reached will not resolve by waiting.
|
|
|
|
|
const tunnel = stubTunnel([new Error('relay connection replaced by another client')], 'error');
|
|
|
|
|
|
|
|
|
|
const result = await withTimerWindow(() => probeRelayDesktopHost(relay, { clientToken: 'token' }));
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe('unreachable');
|
|
|
|
|
expect(tunnel.calls).toEqual(['/health']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a rejected client token is reported as auth, not unreachable', async () => {
|
|
|
|
|
const tunnel = stubTunnel([
|
|
|
|
|
new Response('{}', { status: 200 }),
|
|
|
|
|
new Response('{}', { status: 401 }),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await withTimerWindow(() => probeRelayDesktopHost(relay, { clientToken: 'stale' }));
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe('auth');
|
|
|
|
|
expect(tunnel.calls).toEqual(['/health', '/auth/session']);
|
|
|
|
|
});
|
|
|
|
|
});
|