import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { applyGatewayPreference, buildGatewayPresence, createGatewayAutoState, nextGatewayAutoState, probeGatewayPresence, readHostNetworkState, verifyGatewayPresence, } from '../../src/server/gatewayPresence.js'; import { createStateSnapshot } from '../../src/shared/contracts/state.js'; const subscriptionUrl = 'https://subscription.example/0123456789abcdef0123456789abcdef'; const nonce = '0123456789abcdef0123456789abcdef'; test('Gateway presence is authenticated by the shared subscription secret', async () => { const payload = buildGatewayPresence({ appMode: 'gateway', subscriptionUrl, gatewayId: 'gateway-1', nonce, }); assert.equal(verifyGatewayPresence(payload, { subscriptionUrl, nonce }), true); assert.equal(verifyGatewayPresence(payload, { subscriptionUrl: 'https://subscription.example/fedcba9876543210fedcba9876543210', nonce, }), false); const result = await probeGatewayPresence({ gateway: '192.168.50.111', subscriptionUrl, port: 4567, nonce, fetchImpl: async (url) => { assert.equal( url, `http://192.168.50.111:4567/api/gateway-presence?nonce=${nonce}`, ); return { ok: true, json: async () => payload }; }, }); assert.equal(result.gatewayId, 'gateway-1'); assert.equal(result.uiOrigin, 'http://192.168.50.111:4567'); assert.equal(Number.isFinite(Date.parse(result.verifiedAt)), true); assert.equal(buildGatewayPresence({ appMode: 'gateway', subscriptionUrl: 'https://subscription.example/public-feed', gatewayId: 'gateway-1', nonce, }).available, false); assert.equal(buildGatewayPresence({ appMode: 'gateway', subscriptionUrl: 'https://subscription.example/0123456789abcdef', gatewayId: 'gateway-1', nonce, }).available, true); const sharedPublicValue = 'https://public.example/sing-box-configuration-v1'; const firstUrl = `${subscriptionUrl}?redirect=${encodeURIComponent(sharedPublicValue)}`; const secondUrl = `https://subscription.example/fedcba9876543210fedcba9876543210?redirect=${encodeURIComponent(sharedPublicValue)}`; const firstPayload = buildGatewayPresence({ appMode: 'gateway', subscriptionUrl: firstUrl, gatewayId: 'gateway-1', nonce, }); assert.equal(verifyGatewayPresence(firstPayload, { subscriptionUrl: secondUrl, nonce, }), false); }); test('verified Gateway stays active through transient discovery failures', () => { const now = Date.now(); const statePath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-route-')), 'network.json'); fs.writeFileSync(statePath, JSON.stringify({ gateway: '192.168.50.111', interface: 'en0', mac: 'aa:bb:cc:dd:ee:ff', observedAt: new Date(now).toISOString(), })); const network = readHostNetworkState(statePath, { now }); assert.equal(network.gateway, '192.168.50.111'); let state = nextGatewayAutoState(createGatewayAutoState(), { network, verifiedGateway: { gatewayId: 'gateway-1', uiOrigin: 'http://192.168.50.111:4567', verifiedAt: new Date(now).toISOString(), }, }); assert.equal(state.mode, 'gateway-direct'); assert.equal(state.uiOrigin, 'http://192.168.50.111:4567'); assert.equal(state.lastVerifiedAt, new Date(now).toISOString()); state = nextGatewayAutoState(state, { network }); state = nextGatewayAutoState(state, { network }); assert.equal(state.mode, 'gateway-direct'); state = nextGatewayAutoState(state, { network }); assert.equal(state.mode, 'gateway-direct'); assert.equal(state.gatewayId, 'gateway-1'); assert.equal(state.failures, 3); state = nextGatewayAutoState(state, { network: null, error: 'host snapshot stale', }); assert.equal(state.mode, 'gateway-direct'); assert.equal(state.gatewayId, 'gateway-1'); assert.equal(state.lastError, 'host snapshot stale'); assert.equal(applyGatewayPreference(state, false).mode, 'local-vpn'); const newNetwork = { ...network, mac: '11:22:33:44:55:66' }; state = nextGatewayAutoState(state, { network: newNetwork }); assert.equal(state.mode, 'local-vpn'); assert.equal(state.gatewayId, ''); assert.equal(readHostNetworkState(statePath, { now: now + 16_000 }), null); fs.writeFileSync(statePath, JSON.stringify({ gateway: '192.168.50.111', interface: 'en0', mac: '', observedAt: new Date(now).toISOString(), })); assert.equal(readHostNetworkState(statePath, { now }), null); }); test('canonical route distinguishes fresh, stale, lost, disabled and local states', () => { const storedState = { revision: 1, subscriptionUrl, gatewayAutoEnabled: true, }; const snapshot = (gatewayAuto, stored = storedState) => createStateSnapshot({ storedState: stored, runtime: { running: true }, gatewayAuto, appMode: 'client', configExists: true, subscriptionHost: 'subscription.example/…', now: new Date('2026-07-13T12:00:00.000Z'), }).route; const fresh = { ...createGatewayAutoState(), mode: 'gateway-direct', gateway: { gateway: '192.168.50.111' }, gatewayId: 'gateway-1', uiOrigin: 'http://192.168.50.111:4567', lastVerifiedAt: '2026-07-13T11:59:59.000Z', }; const found = snapshot(fresh); assert.equal(found.mode, 'gateway-direct'); assert.equal(found.reason, 'gateway-found'); assert.equal(found.gatewayAddress, '192.168.50.111'); assert.equal(found.gatewayUiOrigin, 'http://192.168.50.111:4567'); assert.equal(found.lastVerifiedAt, '2026-07-13T11:59:59.000Z'); assert.equal(found.autoEnabled, true); assert.equal(found.fallbackPreference, 'local-vpn'); assert.equal(snapshot({ ...fresh, failures: 1 }).reason, 'gateway-stale'); assert.equal(snapshot({ ...fresh, mode: 'local-vpn', gatewayId: '', lastError: 'lost' }).reason, 'gateway-lost'); assert.equal(snapshot(fresh, { ...storedState, gatewayAutoEnabled: false }).reason, 'disabled'); assert.equal(snapshot(createGatewayAutoState()).reason, 'local'); }); test('client can ignore and restore a verified Gateway without losing discovery', () => { const detected = { ...createGatewayAutoState(), mode: 'gateway-direct', gatewayId: 'gateway-1', }; const ignored = applyGatewayPreference(detected, false); assert.equal(ignored.mode, 'local-vpn'); assert.equal(ignored.gatewayId, 'gateway-1'); assert.equal(applyGatewayPreference(ignored, true).mode, 'gateway-direct'); assert.equal(applyGatewayPreference(createGatewayAutoState(), true).mode, 'local-vpn'); });