Spaces:
Running
Running
Download proxy/proxy_test.ts from SilverElixir/Lumen: direct link, hf CLI and curl.
- Browser
- Download file 20.3 kB
-
https://huggingface.co/spaces/SilverElixir/Lumen/resolve/main/proxy/proxy_test.ts
- Command line
-
hf download hf://spaces/SilverElixir/Lumen/proxy/proxy_test.ts
-
curl -L -o proxy_test.ts https://huggingface.co/spaces/SilverElixir/Lumen/resolve/main/proxy/proxy_test.ts
20.3 kB
| // proxy_test.ts — юнит-тесты на proxy.ts. Намеренно БЕЗ внешних зависимостей | |
| // (никаких jsr:/npm: импортов) — только простые ручные assert-функции ниже. | |
| // Так `deno test` работает даже без доступа в интернет и не тянет lock-файл. | |
| // | |
| // Запуск: | |
| // deno test proxy_test.ts | |
| import { | |
| ALLOWED_HOSTS, | |
| MAX_REQUEST_BODY_BYTES, | |
| PROXY_AUTH_HEADER, | |
| buildForwardHeaders, | |
| buildResponseHeaders, | |
| handleRequest, | |
| limitStreamBytes, | |
| resolveTarget, | |
| } from "./proxy.ts"; | |
| const TEST_SECRET = "isolated-test-secret-0123456789abcdef"; | |
| const AUTH_HEADERS = { [PROXY_AUTH_HEADER]: TEST_SECRET }; | |
| function assert(condition: boolean, message: string): void { | |
| if (!condition) throw new Error(message); | |
| } | |
| function assertEquals<T>(actual: T, expected: T, message?: string): void { | |
| const ok = JSON.stringify(actual) === JSON.stringify(expected); | |
| if (!ok) { | |
| throw new Error( | |
| message ?? `Ожидалось ${JSON.stringify(expected)}, получено ${JSON.stringify(actual)}`, | |
| ); | |
| } | |
| } | |
| // ─────────────────────────── resolveTarget ─────────────────────────── | |
| Deno.test("resolveTarget строит корректный URL для разрешённого хоста", () => { | |
| const result = resolveTarget("/fetch/api.telegram.org/bot123/getMe", ""); | |
| assert(result.ok, "ожидался ok=true"); | |
| if (result.ok) { | |
| assertEquals(result.url, "https://api.telegram.org/bot123/getMe"); | |
| } | |
| }); | |
| Deno.test("resolveTarget сохраняет query-строку как есть", () => { | |
| const result = resolveTarget("/fetch/www.tikwm.com/api/", "?url=https://x&hd=1"); | |
| assert(result.ok, "ожидался ok=true"); | |
| if (result.ok) { | |
| assertEquals(result.url, "https://www.tikwm.com/api/?url=https://x&hd=1"); | |
| } | |
| }); | |
| Deno.test("resolveTarget не теряет завершающий слеш в пути (регрессия)", () => { | |
| // РЕГРЕССИЯ: первая версия использовала .filter(Boolean) при разборе пути, | |
| // который съедал пустой хвостовой сегмент после последнего "/" — путь вида | |
| // "/fetch/host/api/" превращался в "https://host/api" (без слеша). Именно | |
| // такой формат URL реально нужен TikWM (см. bot.py: ".../api/?url=..."). | |
| const withSlash = resolveTarget("/fetch/www.tikwm.com/api/", ""); | |
| assert(withSlash.ok, "ожидался ok=true"); | |
| if (withSlash.ok) assertEquals(withSlash.url, "https://www.tikwm.com/api/"); | |
| const withoutSlash = resolveTarget("/fetch/www.tikwm.com/api", ""); | |
| assert(withoutSlash.ok, "ожидался ok=true"); | |
| if (withoutSlash.ok) assertEquals(withoutSlash.url, "https://www.tikwm.com/api"); | |
| }); | |
| Deno.test("resolveTarget корректно строит вложенный путь (Telegram file API)", () => { | |
| // Регрессия на реальный формат вызова из bot.py (_download_telegram_file_bytes): | |
| // {TELEGRAM_API_BASE_URL}/file/bot{TOKEN}/{file_path} | |
| const result = resolveTarget("/fetch/api.telegram.org/file/bot123/photos/file_1.jpg", ""); | |
| assert(result.ok, "ожидался ok=true"); | |
| if (result.ok) { | |
| assertEquals(result.url, "https://api.telegram.org/file/bot123/photos/file_1.jpg"); | |
| } | |
| }); | |
| Deno.test("resolveTarget отклоняет неразрешённый хост с 403 и не строит URL на его основе", () => { | |
| const result = resolveTarget("/fetch/evil.example.com/steal", ""); | |
| assert(!result.ok, "ожидался ok=false"); | |
| if (!result.ok) { | |
| assertEquals(result.status, 403); | |
| } | |
| }); | |
| Deno.test("resolveTarget отклоняет некорректный формат пути с 404", () => { | |
| assertEquals(resolveTarget("/", "").ok, false); | |
| assertEquals(resolveTarget("/fetch", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/", "").ok, false); | |
| assertEquals(resolveTarget("/wrong-prefix/api.telegram.org/getMe", "").ok, false); | |
| }); | |
| Deno.test("resolveTarget требует форму bot-пути для api.telegram.org", () => { | |
| // Без токена в пути — не релей: произвольные пути при наличии секрета закрыты. | |
| assertEquals(resolveTarget("/fetch/api.telegram.org/getMe", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org/evil", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org/bot", "").ok, false); | |
| // Легитимные формы бота проходят. | |
| assertEquals(resolveTarget("/fetch/api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMe", "").ok, true); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org/file/bot123/photos/f.jpg", "").ok, true); | |
| // Чужим хостам форма не нужна. | |
| assertEquals(resolveTarget("/fetch/www.tikwm.com/api/", "").ok, true); | |
| }); | |
| Deno.test("ALLOWED_HOSTS содержит ровно те хосты, что реально нужны боту", () => { | |
| assertEquals(ALLOWED_HOSTS.has("api.telegram.org"), true); | |
| assertEquals(ALLOWED_HOSTS.has("www.tikwm.com"), true); | |
| assertEquals(ALLOWED_HOSTS.has("tikwm.com"), true); | |
| assertEquals(ALLOWED_HOSTS.size, 3); | |
| }); | |
| // ─────────────────────────── buildForwardHeaders / buildResponseHeaders ─────────────────────────── | |
| Deno.test("buildForwardHeaders убирает hop-by-hop заголовки запроса, остальные сохраняет", () => { | |
| const original = new Headers({ | |
| "Host": "proxy.example.com", | |
| "Connection": "keep-alive", | |
| "User-Agent": "test-ua", | |
| }); | |
| const forwarded = buildForwardHeaders(original); | |
| assertEquals(forwarded.has("host"), false); | |
| assertEquals(forwarded.has("connection"), false); | |
| assertEquals(forwarded.get("user-agent"), "test-ua"); | |
| }); | |
| Deno.test("buildResponseHeaders убирает Content-Encoding/Content-Length (см. докстринг про распаковку)", () => { | |
| // РЕГРЕССИЯ: fetch() в Deno сам распаковывает gzip/br до того, как тело | |
| // становится доступно — пробросить исходный Content-Encoding было бы багом | |
| // (клиент попытался бы распаковать уже распакованные данные). | |
| const original = new Headers({ | |
| "Content-Encoding": "gzip", | |
| "Content-Length": "1234", | |
| "Content-Type": "application/json", | |
| }); | |
| const cleaned = buildResponseHeaders(original); | |
| assertEquals(cleaned.has("content-encoding"), false); | |
| assertEquals(cleaned.has("content-length"), false); | |
| assertEquals(cleaned.get("content-type"), "application/json"); | |
| }); | |
| // ─────────────────────────── handleRequest (с подменённым fetch) ─────────────────────────── | |
| Deno.test("handleRequest пробрасывает GET без тела и возвращает статус/тело апстрима", async () => { | |
| const fakeFetch: typeof fetch = (url, init) => { | |
| assertEquals(String(url), "https://www.tikwm.com/api/?url=x"); | |
| assertEquals(init?.method, "GET"); | |
| assertEquals(init?.body, undefined); | |
| return Promise.resolve(new Response(JSON.stringify({ code: 0 }), { | |
| status: 200, | |
| headers: { "Content-Type": "application/json" }, | |
| })); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/www.tikwm.com/api/?url=x", { method: "GET", headers: AUTH_HEADERS }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 200); | |
| const body = await resp.json(); | |
| assertEquals(body, { code: 0 }); | |
| }); | |
| Deno.test("handleRequest возвращает 403 для неразрешённого хоста, не вызывая fetch вообще", async () => { | |
| let fetchCalled = false; | |
| const fakeFetch: typeof fetch = () => { | |
| fetchCalled = true; | |
| return Promise.resolve(new Response("не должно случиться")); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/evil.example.com/x", { method: "GET", headers: AUTH_HEADERS }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 403); | |
| assertEquals(fetchCalled, false, "fetch не должен был вызываться для неразрешённого хоста"); | |
| }); | |
| Deno.test("handleRequest возвращает 404 для пути без /fetch/ префикса", async () => { | |
| const req = new Request("https://proxy.example/something-else", { method: "GET", headers: AUTH_HEADERS }); | |
| const unusedFetch: typeof fetch = () => Promise.resolve(new Response("unused")); | |
| const resp = await handleRequest(req, unusedFetch, TEST_SECRET); | |
| assertEquals(resp.status, 404); | |
| }); | |
| Deno.test("handleRequest пробрасывает метод и тело для POST (Telegram sendMessage)", async () => { | |
| let capturedMethod: string | undefined; | |
| let capturedBodyIsStream = false; | |
| const fakeFetch: typeof fetch = (_url, init) => { | |
| capturedMethod = init?.method; | |
| capturedBodyIsStream = init?.body instanceof ReadableStream; | |
| return Promise.resolve(new Response('{"ok":true}', { status: 200 })); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/sendMessage", { | |
| method: "POST", | |
| body: JSON.stringify({ chat_id: 1, text: "hi" }), | |
| headers: { "Content-Type": "application/json", ...AUTH_HEADERS }, | |
| }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 200); | |
| assertEquals(capturedMethod, "POST"); | |
| assert(capturedBodyIsStream, "тело POST-запроса должно передаваться апстриму как поток (без буферизации целиком)"); | |
| }); | |
| Deno.test("handleRequest возвращает 502, если апстрим-fetch упал с исключением", async () => { | |
| const fakeFetch: typeof fetch = () => { | |
| throw new Error(`network unreachable ${TEST_SECRET}`); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/getMe", { method: "GET", headers: AUTH_HEADERS }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 502); | |
| assertEquals(await resp.text(), "Upstream fetch failed"); | |
| }); | |
| for (const suppliedSecret of [undefined, "", "incorrect-test-secret"]) { | |
| Deno.test(`handleRequest rejects missing or wrong secret: ${String(suppliedSecret)}`, async () => { | |
| let fetchCalled = false; | |
| const fakeFetch: typeof fetch = () => { | |
| fetchCalled = true; | |
| return Promise.resolve(new Response("unexpected")); | |
| }; | |
| const headers = new Headers(); | |
| if (suppliedSecret !== undefined) headers.set(PROXY_AUTH_HEADER, suppliedSecret); | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/getMe", { headers }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 401); | |
| assertEquals(await resp.text(), "Unauthorized"); | |
| assertEquals(fetchCalled, false); | |
| }); | |
| } | |
| for (const configuredSecret of [undefined, "", " ", "invalid secret", "short"]) { | |
| Deno.test(`handleRequest fails closed for invalid configuration: ${String(configuredSecret)}`, async () => { | |
| let fetchCalled = false; | |
| const fakeFetch: typeof fetch = () => { | |
| fetchCalled = true; | |
| return Promise.resolve(new Response("unexpected")); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/www.tikwm.com/api/", { headers: AUTH_HEADERS }); | |
| const resp = await handleRequest(req, fakeFetch, configuredSecret); | |
| assertEquals(resp.status, 503); | |
| assertEquals(await resp.text(), "Proxy authentication unavailable"); | |
| assertEquals(fetchCalled, false); | |
| }); | |
| } | |
| Deno.test("correct secret is stripped without changing Telegram authentication", async () => { | |
| let fetchCalled = false; | |
| const fakeFetch: typeof fetch = (url, init) => { | |
| fetchCalled = true; | |
| assertEquals(String(url), "https://api.telegram.org/bot123/getMe"); | |
| const headers = new Headers(init?.headers); | |
| assertEquals(headers.has(PROXY_AUTH_HEADER), false); | |
| assertEquals(headers.get("authorization"), "Bearer upstream-test-token"); | |
| assertEquals(init?.redirect, "error"); | |
| return Promise.resolve(new Response('{"ok":true}', { status: 200 })); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/getMe", { | |
| headers: { | |
| "x-lumen-proxy-secret": TEST_SECRET, | |
| "Authorization": "Bearer upstream-test-token", | |
| }, | |
| }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 200); | |
| assertEquals(fetchCalled, true); | |
| assertEquals(await resp.json(), { ok: true }); | |
| assertEquals(req.headers.get(PROXY_AUTH_HEADER), TEST_SECRET); | |
| }); | |
| Deno.test("buildForwardHeaders removes mixed-case proxy secret without mutating input", () => { | |
| const original = new Headers({ "X-LuMeN-PrOxY-SeCrEt": TEST_SECRET }); | |
| assertEquals(buildForwardHeaders(original).has(PROXY_AUTH_HEADER), false); | |
| assertEquals(original.get(PROXY_AUTH_HEADER), TEST_SECRET); | |
| }); | |
| Deno.test("handleRequest rejects oversized declared body before fetching", async () => { | |
| let fetchCalled = false; | |
| const fakeFetch: typeof fetch = () => { | |
| fetchCalled = true; | |
| return Promise.resolve(new Response("unexpected")); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/sendDocument", { | |
| method: "POST", | |
| headers: { | |
| ...AUTH_HEADERS, | |
| "content-length": String(MAX_REQUEST_BODY_BYTES + 1), | |
| }, | |
| }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(resp.status, 413); | |
| assertEquals(fetchCalled, false); | |
| }); | |
| Deno.test("handleRequest allows body at the cap and unknown-length streams", async () => { | |
| const seen: number[] = []; | |
| const fakeFetch: typeof fetch = () => { | |
| seen.push(1); | |
| return Promise.resolve(new Response('{"ok":true}', { status: 200 })); | |
| }; | |
| const atCap = new Request("https://proxy.example/fetch/api.telegram.org/bot123/sendDocument", { | |
| method: "POST", | |
| headers: { ...AUTH_HEADERS, "content-length": String(MAX_REQUEST_BODY_BYTES) }, | |
| }); | |
| assertEquals((await handleRequest(atCap, fakeFetch, TEST_SECRET)).status, 200); | |
| const streamed = new Request("https://proxy.example/fetch/api.telegram.org/bot123/sendVideo", { | |
| method: "POST", | |
| headers: { ...AUTH_HEADERS }, | |
| body: new Uint8Array([1, 2, 3]), | |
| }); | |
| assertEquals((await handleRequest(streamed, fakeFetch, TEST_SECRET)).status, 200); | |
| assertEquals(seen.length, 2); | |
| }); | |
| Deno.test("limitStreamBytes пропускает тело в пределах капа без буферизации", async () => { | |
| const capped = limitStreamBytes(new ReadableStream({ | |
| start(controller) { | |
| controller.enqueue(new Uint8Array([1, 2])); | |
| controller.enqueue(new Uint8Array([3])); | |
| controller.close(); | |
| }, | |
| }), 3); | |
| assert(capped !== null, "ожидался поток"); | |
| assertEquals(Array.from(new Uint8Array(await new Response(capped).arrayBuffer())), [1, 2, 3]); | |
| }); | |
| Deno.test("limitStreamBytes рвёт поток при превышении капа", async () => { | |
| const capped = limitStreamBytes(new ReadableStream({ | |
| start(controller) { | |
| controller.enqueue(new Uint8Array([1, 2])); | |
| controller.enqueue(new Uint8Array([3, 4])); | |
| controller.close(); | |
| }, | |
| }), 3); | |
| assert(capped !== null, "ожидался поток"); | |
| let failed = false; | |
| try { | |
| await new Response(capped).arrayBuffer(); | |
| } catch { | |
| failed = true; | |
| } | |
| assert(failed, "поток сверх капа обязан оборваться, а не доставиться целиком"); | |
| }); | |
| Deno.test("handleRequest не пропускает chunked-тело запроса сверх капа", async () => { | |
| const fakeFetch: typeof fetch = async (_url, init) => { | |
| await new Response(init?.body as BodyInit | null | undefined).arrayBuffer(); | |
| return new Response('{"ok":true}', { status: 200 }); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/sendVideo", { | |
| method: "POST", | |
| headers: { ...AUTH_HEADERS }, | |
| body: new Uint8Array([1, 2, 3, 4, 5]), | |
| }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET, 3); | |
| assertEquals(resp.status, 413); | |
| assertEquals(await resp.text(), "Request body too large"); | |
| }); | |
| Deno.test("handleRequest не отдаёт тело ответа сверх капа", async () => { | |
| const fakeFetch: typeof fetch = () => | |
| Promise.resolve(new Response(new Uint8Array([1, 2, 3, 4]), { status: 200 })); | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/file/bot123/x", { | |
| headers: { ...AUTH_HEADERS }, | |
| }); | |
| const resp = await handleRequest(req, fakeFetch, TEST_SECRET, 3); | |
| let failed = false; | |
| try { | |
| await resp.arrayBuffer(); | |
| } catch { | |
| failed = true; | |
| } | |
| assert(failed, "ответ сверх капа обязан оборваться, а не утечь целиком"); | |
| }); | |
| Deno.test("handleRequest проверяет секрет раньше маршрутизации (401 раньше 403/404)", async () => { | |
| const unusedFetch: typeof fetch = () => Promise.resolve(new Response("unused")); | |
| const noSecret404 = new Request("https://proxy.example/something-else"); | |
| assertEquals((await handleRequest(noSecret404, unusedFetch, TEST_SECRET)).status, 401); | |
| const noSecret403 = new Request("https://proxy.example/fetch/evil.example.com/x"); | |
| assertEquals((await handleRequest(noSecret403, unusedFetch, TEST_SECRET)).status, 401); | |
| }); | |
| Deno.test("resolveTarget отклоняет враждебные варианты хостов", () => { | |
| assertEquals(resolveTarget("/fetch/API.TELEGRAM.ORG/x", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org./x", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org:443/x", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/api.telegram.org%2Fevil/x", "").ok, false); | |
| assertEquals(resolveTarget("/fetch/../x", "").ok, false); | |
| }); | |
| Deno.test("handleRequest требует от апстрима не следовать редиректам", async () => { | |
| let capturedRedirect: string | undefined; | |
| const fakeFetch: typeof fetch = (_url, init) => { | |
| capturedRedirect = init?.redirect; | |
| return Promise.resolve(new Response('{"ok":true}', { status: 200 })); | |
| }; | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/getMe", { headers: AUTH_HEADERS }); | |
| await handleRequest(req, fakeFetch, TEST_SECRET); | |
| assertEquals(capturedRedirect, "error"); | |
| }); | |
| Deno.test("handleRequest терпит кривой content-length (NaN/отрицательный)", async () => { | |
| let fetchCalled = 0; | |
| const fakeFetch: typeof fetch = () => { | |
| fetchCalled++; | |
| return Promise.resolve(new Response('{"ok":true}', { status: 200 })); | |
| }; | |
| for (const declared of ["abc", "-5"]) { | |
| const req = new Request("https://proxy.example/fetch/api.telegram.org/bot123/getMe", { | |
| headers: { ...AUTH_HEADERS, "content-length": declared }, | |
| }); | |
| assertEquals((await handleRequest(req, fakeFetch, TEST_SECRET)).status, 200); | |
| } | |
| assertEquals(fetchCalled, 2); | |
| }); | |
| Deno.test("limitStreamBytes пропускает null без изменений", () => { | |
| assertEquals(limitStreamBytes(null, 3), null); | |
| }); | |
| Deno.test("buildForwardHeaders убирает расширенный hop-by-hop список и content-length", () => { | |
| const original = new Headers({ | |
| "Transfer-Encoding": "chunked", | |
| "TE": "trailers", | |
| "Upgrade": "websocket", | |
| "Content-Length": "10", | |
| "X-Custom": "keep", | |
| }); | |
| const forwarded = buildForwardHeaders(original); | |
| for (const name of ["transfer-encoding", "te", "upgrade", "content-length"]) { | |
| assertEquals(forwarded.has(name), false); | |
| } | |
| assertEquals(forwarded.get("x-custom"), "keep"); | |
| }); | |