WebSockets and Testing
Junction includes native WebSocket support with upgrade handshake, message framing, ping/pong keepalive, and structured close codes. The testing module provides mock request builders, response inspectors, and assertion helpers for integration testing without a running server.
WebSocket Support
WebSockets are handled via an upgrade mechanism. Register a WebSocket handler for a path, and the framework performs the HTTP upgrade handshake (Sec-WebSocket-Accept, status 101) automatically.
Registering a WebSocket Handler
import lib.frameworks.junction.websocket;
junction.register_websocket(app, "/ws/chat", fn(ws_msg: object) {
let msg_type = ws_msg["type"];
let conn = ws_msg["conn"];
if (msg_type == "message") {
let data = ws_msg["data"];
websocket.send_message(conn, "Echo: " + data);
} else if (msg_type == "ping") {
websocket.send_pong(conn);
} else if (msg_type == "close") {
print("Connection closed: " + conn["id"]);
}
});Manual Upgrade
For full control, use websocket.upgrade(handler, req) or websocket.upgrade_with_config(handler, req, config) inside a route handler:
junction.route(app, "GET", "/ws", fn(req: Request) -> Response {
if (!websocket.is_websocket_request(req)) {
return junction.error("WS_REQUIRED", "WebSocket upgrade required", 426);
}
return websocket.upgrade(fn(ws_msg: object) { /* handler */ }, req);
});WebSocket Configuration
let ws_config = {
"max_message_size": 2097152, // 2 MB
"ping_interval_ms": 15000, // 15s between pings
"close_timeout_ms": 3000 // 3s close handshake timeout
};
let res = websocket.upgrade_with_config(handler, req, ws_config);WebSocket API Reference
| Function | Description |
|---|---|
websocket.upgrade(handler, req) | Perform upgrade handshake, returns 101 response |
websocket.upgrade_with_config(handler, req, config) | Upgrade with custom config |
websocket.send_message(conn, data) | Send a text/binary message |
websocket.send_json(conn, data) | Serialize and send JSON message |
websocket.send_ping(conn) | Send a ping frame |
websocket.send_pong(conn) | Send a pong frame |
websocket.close(conn, code, reason) | Initiate close handshake |
websocket.close_normal(conn) | Close with code 1000 |
websocket.close_going_away(conn) | Close with code 1001 |
websocket.close_protocol_error(conn) | Close with code 1002 |
websocket.close_unsupported(conn) | Close with code 1003 |
websocket.close_policy_violation(conn) | Close with code 1008 |
websocket.close_too_large(conn) | Close with code 1009 |
websocket.close_internal_error(conn) | Close with code 1011 |
websocket.is_websocket_request(req) | Check for valid upgrade headers |
websocket.mask_data(data, key) | XOR mask frame payload |
websocket.build_frame(opcode, data) | Construct a raw WebSocket frame |
Chat Example
let clients = [];
junction.register_websocket(app, "/chat", fn(msg: object) {
let conn = msg["conn"];
if (msg["type"] == "message") {
let data = msg["data"];
let i = 0;
while (i < std.collections.length(clients)) {
if (clients[i]["id"] != conn["id"]) {
websocket.send_message(clients[i], data);
}
i = i + 1;
}
} else if (msg["type"] == "close") {
let i = 0;
while (i < std.collections.length(clients)) {
if (clients[i]["id"] == conn["id"]) {
clients.splice(i, 1);
break;
}
i = i + 1;
}
}
});Testing Framework
The testing module provides mock request builders, assertion helpers, and a test runner for integration-testing Junction apps without starting an HTTP server. Use create_test_app() and test_request() to exercise the full middleware pipeline and router.
Test Lifecycle
import lib.frameworks.junction.testing;
// 1. Create a test app
let app = junction.create_test_app();
// 2. Register routes and middleware
junction.route(app, "GET", "/ping", fn(req: Request) -> Response {
return junction.json({"pong": true}, 200);
});
// 3. Build a mock request and dispatch it
let req = testing.mock_get("/ping");
let res = junction.test_request(app, req);
// 4. Assert on the response
testing.assert_equal(result, res.status, 200, "status should be 200");Mock Request Builders
// GET request
let req = testing.mock_get("/users");
// POST with JSON body
let req = testing.mock_json_post("/users", {"name": "Alice"});
// POST with raw body and content type
let req = testing.mock_post("/upload", "raw data", "text/plain");
// PUT with JSON body
let req = testing.mock_put("/users/1", json_body, "application/json");
// PATCH with JSON body
let req = testing.mock_patch("/users/1", json_body, "application/json");
// DELETE request
let req = testing.mock_delete("/users/1");Mock Request Modifiers
testing.mock_with_auth(req, "api-key-123");
testing.mock_with_bearer(req, "bearer-token-xyz");
testing.mock_with_header(req, "X-Custom", "value");
testing.mock_with_query(req, "page", "2");
testing.mock_with_cookie(req, "session_id", "sess_abc");Assertions
let result = testing.new_test_result("my test");
testing.assert_equal(result, actual, expected, "description");
testing.assert_not_equal(result, actual, expected, "description");
testing.assert_true(result, value, "description");
testing.assert_false(result, value, "description");
testing.assert_null(result, value, "description");
testing.assert_not_null(result, value, "description");
// Response-specific assertions
testing.assert_status(result, response, 200, "status ok");
testing.assert_json_contains(result, response, "user_id", "has user_id");
testing.assert_json_value(result, response, "role", "admin", "role is admin");
testing.assert_header(result, response, "Content-Type", "application/json", "content type");
testing.assert_header_exists(result, response, "X-Request-Id", "has request id");
testing.assert_content_type(result, response, "application/json", "content type");
testing.assert_body_contains(result, response, "success", "body contains success");
testing.assert_redirect(result, response, "/login", "redirects to login");Running Tests
fn test_health(t: object) {
let app = junction.create_test_app();
my_routes.setup(app);
let req = testing.mock_get("/health");
let res = junction.test_request(app, req);
testing.assert_status(t, res, 200, "health endpoint ok");
}
fn test_create_user(t: object) {
let app = junction.create_test_app();
my_routes.setup(app);
let req = testing.mock_json_post("/users", {"name": "Alice"});
let res = junction.test_request(app, req);
testing.assert_status(t, res, 201, "user created");
testing.assert_json_value(t, res, "name", "Alice", "name matches");
}
fn main() {
let results = testing.run_tests({
"health": test_health,
"create_user": test_create_user
});
// Output: 2 tests, 2 passed, 0 failed
}Test Results Structure
Each TestResult contains the test name, an array of assertions, pass/fail status, failed/total counts, and duration in milliseconds. The run_tests() function prints a summary to stdout and returns a summary object with total, passed, and failed counts.
CLI Test Runner
Run tests from the command line:
braidc junction test