Skip to content

WebSocket Test Harness

Eric Fitzgerald edited this page Apr 8, 2026 · 2 revisions

WebSocket Test Harness

A standalone Go application for testing the TMI WebSocket interface for collaborative threat modeling.

Source Location: wstest/ directory in the TMI repository

Features

  • OAuth authentication with TMI test provider using login hints (idp=test)
  • Ticket-based WebSocket authentication (obtains a short-lived ticket from /ws/ticket before connecting)
  • Host mode: Creates threat models, adds participants with random permissions, creates diagrams, and starts collaboration sessions
  • Participant mode: Polls /me/sessions every 5 seconds for available collaboration sessions and joins them; automatically reconnects after disconnection
  • Handles all AsyncAPI message types: participant updates, diagram operations, sync protocol, presenter controls, error/rejection messages, and authorization events
  • Comprehensive structured logging (slog) of all network interactions and WebSocket messages
  • Graceful shutdown on SIGINT/SIGTERM with proper WebSocket close handshake
  • Supports multiple concurrent instances
  • Uses the Gorilla WebSocket library

Building

Use the make target (preferred):

make build-wstest

Or build directly:

cd wstest
go mod tidy
go build -o wstest

Usage

Host Mode

Start as a host to create a new collaboration session:

# Basic host mode
./wstest --user alice --host

# Host mode with participants
./wstest --user alice --host --participants "bob,charlie,dave"

# Custom server
./wstest --server http://localhost:8080 --user alice --host

Participant Mode

Start as a participant to join existing collaboration sessions:

# Join any available session
./wstest --user bob

# Multiple participants
./wstest --user charlie &
./wstest --user dave &

Command Line Options

Option Description Default
--server <url> Server URL localhost:8080
--user <hint> User login hint Required
--host Run in host mode false
--participants <list> Comma-separated list of participant hints (host mode only) Empty

Test Scenarios

Basic Two-User Test

Terminal 1 (Host):

./wstest --user alice --host --participants "bob"

Terminal 2 (Participant):

./wstest --user bob

Multi-User Collaboration

Terminal 1 (Host):

./wstest --user alice --host --participants "bob,charlie,dave"

Terminals 2-4 (Participants):

./wstest --user bob
./wstest --user charlie
./wstest --user dave

Automated Multi-Terminal Test

Use the make target to automatically launch three terminal windows:

make wstest

This launches:

Each instance has a 30-second timeout to prevent runaway processes.

WebSocket Authentication

The harness uses ticket-based authentication for WebSocket connections:

  1. After OAuth login, the harness requests a short-lived ticket from GET /ws/ticket?session_id=<session_id> using the Bearer token.
  2. The WebSocket connection URL includes the ticket as a query parameter: /threat_models/<id>/diagrams/<id>/ws?session_id=<id>&ticket=<ticket>.

This approach avoids sending long-lived access tokens over the WebSocket upgrade request.

Expected WebSocket Messages

All WebSocket messages are logged with timestamps and pretty-printed JSON. The harness handles the following message types:

Server to Client Messages

Message Type Description
participants_update Full list of current participants, host, and current presenter
diagram_operation_event A diagram operation applied by another user (includes operation_id, sequence_number, update_vector, operation)
diagram_state Full diagram state with all cells (sent on sync)
sync_status_response Current update_vector for the diagram
error Server error with error, message, code, and timestamp fields
operation_rejected A submitted operation was rejected (includes reason, affected_cells, requires_resync)
presenter_cursor Presenter's cursor position (x, y coordinates)
presenter_selection Cells currently selected by the presenter
presenter_request_event Notification to host that a user is requesting presenter role
presenter_denied_event Notification that a presenter request was denied
authorization_denied An operation was denied due to insufficient permissions

Client to Server Messages

Message Type Description
diagram_operation_request Submit a diagram operation (includes operation_id, base_vector, operation)
sync_status_request Request current sync status
sync_request Request diagram state sync (optionally from a specific update_vector)
undo_request Request undo of the last operation
redo_request Request redo of the last undone operation
presenter_request Request to become the presenter
change_presenter_request Request to change the presenter to another user
presenter_denied_request Host denies a presenter request (includes denied_user)
remove_participant_request Remove a participant from the session (includes removed_user)

participants_update

Full list of current participants (includes presenter info if any).

Structure:

{
  "message_type": "participants_update",
  "initiating_user": { /* user who triggered the update, or null for system events */ },
  "participants": [
    {
      "user": {
        "principal_type": "user",
        "provider": "tmi",
        "provider_id": "alice@tmi.local",
        "email": "alice@tmi.local",
        "display_name": "Alice (TMI User)"
      },
      "permissions": "owner",
      "last_activity": "2025-01-24T12:00:00Z"
    }
  ],
  "host": { /* host user object */ },
  "current_presenter": { /* presenter user object, or null if no presenter */ }
}

OAuth Flow

The harness implements the OAuth authorization code flow:

  1. Starts a local HTTP server on a random port for the callback.
  2. Makes a GET request to /oauth2/authorize?idp=test&login_hint=<user>&client_callback=<url>&scope=openid+email+profile.
  3. Receives a redirect response from the server pointing to the local callback.
  4. Follows the redirect by making a GET request to the callback URL (headless -- no browser involved).
  5. The callback handler extracts the authorization code and exchanges it for tokens via POST to /oauth2/token (JSON body with grant_type, code, redirect_uri).
  6. Calls GET /oauth2/userinfo with the Bearer token to ensure the user is created in the database.
  7. Uses the access token for all subsequent API calls and WebSocket ticket requests.

Note: The harness uses idp=test, the TMI development OAuth provider that creates test users based on login hints. The callback handler also supports implicit flow (tokens returned directly as query parameters) as a fallback.

Logging

The harness uses Go's slog structured logging (via the TMI slogging package). All network interactions are logged to the console:

  • HTTP requests show method, URL, and request bodies (at DEBUG level)
  • HTTP responses show status codes and response bodies (at DEBUG level)
  • WebSocket messages are parsed by type and logged with relevant fields at INFO level; full JSON is logged at DEBUG level
  • OAuth callback parameters are logged in detail at INFO level
  • Errors and warnings use appropriate log levels (ERROR, WARN)

Exit

Use Ctrl+C to gracefully shutdown the application. The WebSocket connection will be properly closed.

Make Targets

Target Description
make build-wstest Build the WebSocket test harness binary
make wstest Launch 3-terminal test (alice as host, bob & charlie as participants)
make monitor-wstest Run WebSocket harness with user 'monitor' in foreground
make clean-wstest Stop all running WebSocket test harness instances

Troubleshooting

Connection Fails Immediately

Check:

  1. Verify the server is running (make start-dev).
  2. Verify the server URL is correct (default is localhost:8080).
  3. Confirm the OAuth flow completes successfully (check console output).

No Sessions Found (Participant Mode)

Check:

  1. Verify the host has started a session first.
  2. Verify the participant user has been added to the threat model authorization.
  3. Confirm the session is still active (not timed out).

WebSocket Disconnects

Check:

  1. Review server logs for errors.
  2. Confirm the session has not been terminated by the host.
  3. Verify network connectivity.

Related Pages

Clone this wiki locally