Files
tglock/worker/tglock-worker.test.js
T

197 lines
7.2 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
const source = await readFile(new URL("./tglock-worker.js", import.meta.url), "utf8");
const tick = () => new Promise((resolve) => setImmediate(resolve));
function deferred() {
let resolve, reject;
const promise = new Promise((a, b) => { resolve = a; reject = b; });
return { promise, resolve, reject };
}
// Execute the actual standalone deployment file, replacing only Cloudflare's
// platform primitives. No test-only implementation of the relay is used.
async function fixture({ opened = Promise.resolve(), write, timeout = false } = {}) {
let readController;
const readable = new ReadableStream({ start(controller) { readController = controller; } });
const reads = { reject(error) { readController.error(error); } };
const closed = deferred();
const writes = [];
const events = new Map();
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
const server = {
accept() { state.accepted = true; },
addEventListener(name, callback) { events.set(name, callback); },
close(code, reason) { state.closes.push({ code, reason }); },
send(chunk) { state.sent.push(Array.from(chunk)); },
};
const socket = {
opened, closed: closed.promise,
writable: { getWriter: () => ({
ready: Promise.resolve(),
async write(chunk) { writes.push(Array.from(chunk)); if (write) await write(chunk); },
}) },
readable,
async close() {
state.socketCloses++;
try { readController.close(); } catch { /* already closed or errored */ }
closed.resolve();
},
};
const context = vm.createContext({
URL, Uint8Array, ArrayBuffer, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
Response: class {
constructor(body, options) { this.body = body; Object.assign(this, options); }
},
WebSocketPair: class { constructor() { this[0] = {}; this[1] = server; } },
});
const sockets = new vm.SyntheticModule(["connect"], function () {
this.setExport("connect", (address) => {
state.connects++;
state.address = address;
return socket;
});
}, { context });
const module = new vm.SourceTextModule(source, { context });
await module.link((name) => {
assert.equal(name, "cloudflare:sockets");
return sockets;
});
await module.evaluate();
return {
...state,
state, writes, reads, closed, socket,
download(chunk) { readController.enqueue(chunk); },
eof() { readController.close(); },
message(data) { events.get("message")({ data }); },
event(name) { events.get(name)({}); },
fetch(path = "/apiws?dst=149.154.167.51&dc=2", headers = {}, env = {}) {
return module.namespace.default.fetch(new Request(`https://example.workers.dev${path}`, {
headers: { Upgrade: "websocket", "Sec-WebSocket-Protocol": "binary", ...headers },
}), env);
},
};
}
test("HTTP deployment check does not claim Telegram connectivity", async () => {
const f = await fixture();
assert.equal((await f.fetch("/apiws", { Upgrade: "" })).status, 426);
assert.equal(f.state.connects, 0);
});
test("reject unauthorized destinations, token and subprotocol before opening TCP", async () => {
const f = await fixture();
assert.equal((await f.fetch("/wrong")).status, 404);
assert.equal((await f.fetch("/apiws?dst=127.0.0.1")).status, 403);
assert.equal((await f.fetch(undefined, {}, { TGLOCK_TOKEN: "test-token" })).status, 403);
assert.equal((await f.fetch(undefined, { "Sec-WebSocket-Protocol": "chat" })).status, 400);
assert.equal(f.state.connects, 0);
});
test("wait for upstream TCP before accepting WebSocket", async () => {
const opened = deferred();
const f = await fixture({ opened: opened.promise });
const response = f.fetch();
await tick();
assert.equal(f.state.accepted, false);
opened.resolve();
const result = await response;
assert.equal(result.status, 101);
assert.equal(result.headers["Sec-WebSocket-Protocol"], "binary");
assert.equal(f.state.accepted, true);
f.event("close");
});
test("failed upstream returns HTTP 502, never a successful tunnel", async () => {
const f = await fixture({ opened: Promise.reject(new Error("unreachable")) });
assert.equal((await f.fetch()).status, 502);
assert.equal(f.state.accepted, false);
assert.equal(f.state.socketCloses, 1);
});
test("upstream timeout returns 504 and cancels socket", async () => {
const f = await fixture({ opened: new Promise(() => {}), timeout: true });
assert.equal((await f.fetch()).status, 504);
assert.equal(f.state.accepted, false);
assert.equal(f.state.socketCloses, 1);
});
test("serialize binary writes, preserve bytes and view offsets", async () => {
const firstWrite = deferred();
let calls = 0;
const f = await fixture({ write: async () => { if (++calls === 1) await firstWrite.promise; } });
await f.fetch();
f.message(new Uint8Array([1, 2]).buffer);
f.message(new Uint8Array([99, 3, 4, 88]).subarray(1, 3));
await tick();
assert.deepEqual(f.writes, [[1, 2]]);
firstWrite.resolve();
await tick();
assert.deepEqual(f.writes, [[1, 2], [3, 4]]);
f.event("close");
});
test("overflow closes socket and discards queued writes", async () => {
const f = await fixture();
await f.fetch();
f.message(new Uint8Array(1024 * 1024).buffer);
f.message(new Uint8Array([1]).buffer);
await tick();
assert.equal(f.state.closes[0].code, 1009);
assert.equal(f.state.socketCloses, 1);
assert.equal(f.writes.length, 0);
});
test("empty-message floods have a bounded queue too", async () => {
const f = await fixture();
await f.fetch();
for (let i = 0; i < 257; i++) f.message(new ArrayBuffer(0));
await tick();
assert.equal(f.state.closes[0].code, 1009);
assert.equal(f.state.socketCloses, 1);
});
test("text and write failures get diagnostic close codes", async () => {
const f = await fixture();
await f.fetch();
f.message("not binary");
assert.equal(f.state.closes[0].code, 1003);
const failing = await fixture({ write: async () => { throw new Error("reset"); } });
await failing.fetch();
failing.message(new Uint8Array([1]).buffer);
await tick();
assert.equal(failing.state.closes[0].code, 1011);
assert.equal(failing.state.socketCloses, 1);
});
test("read errors propagate and close both socket directions", async () => {
const f = await fixture();
await f.fetch();
f.reads.reject(new Error("reset"));
await tick();
assert.equal(f.state.closes[0].code, 1011);
assert.equal(f.state.socketCloses, 1);
});
test("Telegram bytes reach WebSocket unchanged and EOF closes both directions", async () => {
const f = await fixture();
await f.fetch();
f.download(new Uint8Array([0, 255, 128]));
f.download(new Uint8Array([1, 2]));
f.eof();
await tick();
assert.deepEqual(f.state.sent, [[0, 255, 128], [1, 2]]);
assert.equal(f.state.closes[0].code, 1000);
assert.equal(f.state.socketCloses, 1);
});
test("CDN worker keeps the requested DC203 destination", async () => {
const f = await fixture();
await f.fetch("/apiws?dst=91.105.192.100&dc=203");
assert.equal(f.state.address.hostname, "91.105.192.100");
assert.equal(f.state.address.port, 443);
f.event("close");
});