Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { resolveNovncTarget, safeProxyHeaders, watchScreenAuthorization } from "./screen-proxy.js";
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("screen proxy", () => {
|
|
it("rejects legacy URLs without contacting the API", async () => {
|
|
const fetch = vi.fn();
|
|
vi.stubGlobal("fetch", fetch);
|
|
expect(
|
|
await resolveNovncTarget("/novnc/old/view/token/embed.html", "secret", "http://api.example"),
|
|
).toBeNull();
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
it("checks each request and fails closed on revocation or API failure", async () => {
|
|
const target = {
|
|
protocol: "http:",
|
|
hostname: "127.0.0.1",
|
|
port: 49152,
|
|
path: "/websockify",
|
|
interactive: false,
|
|
};
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(Response.json(target))
|
|
.mockResolvedValueOnce(new Response(null, { status: 403 }))
|
|
.mockRejectedValueOnce(new Error("offline"));
|
|
vi.stubGlobal("fetch", fetch);
|
|
const resolve = () =>
|
|
resolveNovncTarget("/novnc/session/view/token/websockify", "secret", "http://api.example");
|
|
expect(await resolve()).toEqual(target);
|
|
expect(await resolve()).toBeNull();
|
|
expect(await resolve()).toBeNull();
|
|
expect(fetch).toHaveBeenCalledTimes(3);
|
|
expect(fetch.mock.calls[0]?.[1]).toMatchObject({
|
|
redirect: "error",
|
|
headers: { authorization: "Bearer secret" },
|
|
});
|
|
});
|
|
it("fails closed for a malformed authority response", async () => {
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn().mockResolvedValue(Response.json({ hostname: "screen.example", port: "443" })),
|
|
);
|
|
expect(
|
|
await resolveNovncTarget(
|
|
"/novnc/session/view/token/vnc.html",
|
|
"secret",
|
|
"http://api.example",
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("closes an active stream on revocation and stops checking closed streams", async () => {
|
|
vi.useFakeTimers();
|
|
const check = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
|
const revoke = vi.fn();
|
|
watchScreenAuthorization(check, revoke);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(revoke).not.toHaveBeenCalled();
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(revoke).toHaveBeenCalledTimes(1);
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
expect(check).toHaveBeenCalledTimes(2);
|
|
const stop = watchScreenAuthorization(check, revoke);
|
|
stop();
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(check).toHaveBeenCalledTimes(2);
|
|
});
|
|
it("closes a stream when authorization fails unexpectedly", async () => {
|
|
vi.useFakeTimers();
|
|
const revoke = vi.fn();
|
|
watchScreenAuthorization(async () => {
|
|
throw new Error("unavailable");
|
|
}, revoke);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
expect(revoke).toHaveBeenCalledTimes(1);
|
|
});
|
|
it("does not forward application credentials", () => {
|
|
expect(
|
|
safeProxyHeaders({
|
|
host: "app.example",
|
|
cookie: "session=secret",
|
|
authorization: "Bearer secret",
|
|
"proxy-authorization": "Basic secret",
|
|
upgrade: "websocket",
|
|
"sec-websocket-key": "key",
|
|
}),
|
|
).toEqual({ upgrade: "websocket", "sec-websocket-key": "key" });
|
|
});
|
|
|
|
it("does not forward HTTP/2 pseudo-headers to the HTTP/1 upstream", () => {
|
|
expect(
|
|
safeProxyHeaders({
|
|
":method": "GET",
|
|
":path": "/novnc/embed.html",
|
|
":authority": "localhost:5173",
|
|
":scheme": "https",
|
|
upgrade: "websocket",
|
|
"sec-websocket-key": "key",
|
|
}),
|
|
).toEqual({ upgrade: "websocket", "sec-websocket-key": "key" });
|
|
});
|
|
});
|