Skip to main content

WebSocket testing

:::warning No automated tests exist apps/web-socket has no test suite — no test files, no test script. All 261 tests live in apps/web. Everything below is either manual, or a suggestion for tests worth adding. :::

Manual testing with wscat

npm install -g wscat

Health

curl http://localhost:8080/health

Guards

Each of these should fail, and failing is the pass condition:

# No token → closes immediately
npx wscat -c "ws://localhost:8080/ws"

# Bad origin → rejected at handshake
npx wscat -c "ws://localhost:8080/ws" -o "http://evil.example.com"

# Expired token → closes (wait >300s after minting)
npx wscat -c "ws://localhost:8080/ws?token=<stale>"

Check the server log for 🚫 Rejected WS connection from unauthorized origin.

A real session

Get a token from the browser — sign in, then in devtools:

await fetch("/api/ws-token").then((r) => r.json());
npx wscat -c "ws://localhost:8080/ws?token=<token>" -o http://localhost:3000

> {"type":"join_group","groupId":"<groupId>"}
< {"type":"joined_group","groupId":"<groupId>"}

> {"type":"message","groupId":"<groupId>","content":"hello"}

Open the group in a browser at the same time — the message should appear there.

Rate limit

# 20 rapid frames; the connection should close partway
for i in $(seq 1 20); do
echo "{\"type\":\"message\",\"groupId\":\"g1\",\"content\":\"$i\"}"
done | npx wscat -c "ws://localhost:8080/ws?token=<token>" -o http://localhost:3000

Size limit

# ~9 KB — over the 8 KB cap
node -e '
const big = "x".repeat(9000);
console.log(JSON.stringify({type:"message",groupId:"g1",content:big}));
' | npx wscat -c "ws://localhost:8080/ws?token=<token>" -o http://localhost:3000

Connections per IP

Open six wscat sessions from one machine. The sixth should be refused — the cap is 5.

This one matters in practice: a developer with several tabs plus a wscat session can hit it accidentally and misread it as a bug.

Git endpoints

SECRET=<GIT_SERVICE_SECRET>

curl -X POST http://localhost:8080/git/ensure \
-H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"groupId":"g1","repoUrl":"https://github.com/owner/repo","branch":"main"}'

curl -X POST http://localhost:8080/git/content \
-H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{"groupId":"g1","branch":"main","path":""}'

Path traversal — test this

safeResolve (gitWorkspace.ts:26) is the guard. Confirm it holds:

curl -G http://localhost:8080/git/file \
-H "Authorization: Bearer $SECRET" \
--data-urlencode "groupId=g1" \
--data-urlencode "branch=main" \
--data-urlencode "path=../../../../etc/passwd"

Anything other than an error is a serious vulnerability. Try the encoded variants too — %2e%2e%2f, and a leading /.

Realtime behaviour

Needs two browser profiles signed in as different users in one group.

CheckExpected
A sends a messageAppears for B in ~1s
A moves a board nodeB sees it move
A moves the mouseB sees A's cursor with their name
Kill the WS serverBoth show an offline state
Restart itBoth reconnect and resync
A edits while offline, returnsBoard converges
Both drag the same nodeLast write wins, no crash

Suggested automated tests

The service has no suite; these would be the first ones worth writing.

Token verification — pure and easy to test:

import { describe, expect, it } from "vitest";
import { verifyWsToken } from "../src/wsToken";
import { signWsToken } from "../../web/app/lib/wsToken";

describe("verifyWsToken", () => {
it("accepts a freshly signed token", () => {
const token = signWsToken("u1", ["g1"], 300);
expect(verifyWsToken(token)?.userId).toBe("u1");
});

it("rejects a tampered payload", () => {
const token = signWsToken("u1", ["g1"], 300);
const [payload, sig] = token.split(".");
expect(verifyWsToken(`${payload}x.${sig}`)).toBeNull();
});

it("rejects an expired token", () => {
const token = signWsToken("u1", ["g1"], -1);
expect(verifyWsToken(token)).toBeNull();
});
});

safeResolve — the traversal guard deserves a test per attack shape:

it("rejects traversal outside the workspace", () => {
expect(() => safeResolve("/ws/g1/main", "../../etc/passwd")).toThrow();
expect(() => safeResolve("/ws/g1/main", "/etc/passwd")).toThrow();
});

it("allows a normal path", () => {
expect(safeResolve("/ws/g1/main", "src/index.ts")).toBe("/ws/g1/main/src/index.ts");
});

withGroupLock — assert that two concurrent calls serialise rather than interleave.

Rate limiting and origin rejection can be tested with a real ws client against a server started on an ephemeral port.