Files
Rendezvous/docs/integration/test-client.md

11 KiB

Start-to-finish TestClient guide

Tracking: #20, #25

FinalFactory.Rendezvous.TestClient is the supported executable proof that a consumer can publish, browse, authorize, punch, connect, exchange direct traffic, and diagnose a failure using only the public Client and Contracts packages. It is intentionally thin: a polished server browser and player-facing connection UI belong in each game repository.

Traversal boundary: Rendezvous v1 is not a relay and cannot guarantee a connection through symmetric NAT, carrier-grade NAT, restrictive firewalls, VPNs, or platform policy. It provides no accounts, social system, skill-based matchmaking, gameplay server, gameplay authority, or gameplay transport.

The automated scenario matrix, privileged Linux namespace run, and simulation limits are in the deterministic topology harness. The SDK seam guide covers the few integration details that this executable cannot show.

First local connection from a clean checkout

Prerequisites are the pinned .NET SDK, Docker with Compose, OpenSSL, Python 3, curl, and jq. Run these commands from the repository root. The generated key and credential are disposable local fixtures, not production provisioning.

install -d -m 0700 deploy/compose/secrets
umask 077
openssl rand -out deploy/compose/secrets/signing-key 32
export RENDEZVOUS_UID="$(id -u)"
export RENDEZVOUS_GID="$(id -g)"
test "$RENDEZVOUS_UID" -ne 0
docker compose -f deploy/compose/compose.yaml up --build --detach
ready=false
for attempt in {1..45}; do
  if curl --fail --silent http://127.0.0.1:8080/health/ready >/dev/null; then
    ready=true
    break
  fi
  sleep 1
done
test "$ready" = true
curl --fail http://127.0.0.1:8080/health/live
curl --fail http://127.0.0.1:8080/health/ready
dotnet build src/FinalFactory.Rendezvous.TestClient --configuration Release

Only the host terminal needs a publisher credential. Disable shell tracing before capturing it; the helper prints the credential on stdout so command substitution can place it directly in the environment without writing it to disk.

set +x
export RENDEZVOUS_PUBLISHER_CREDENTIAL="$(./scripts/mint-local-publisher-credential.sh)"

The helper accepts no arguments, reads the ignored 0600 local Compose key, and mints only space-game / smoke / local / protocol 1 for ten minutes. It is not a reusable issuer or an example for production. Never put the result in a command argument, URL, shell history, log, screenshot, support ticket, captured fixture, or source file.

In terminal 1, publish a host. It stays alive for at most 60 seconds and exits after a joining peer completes the authenticated echo exchange:

dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  host --service http://127.0.0.1:8080/ --mediator 127.0.0.1:9050 \
  --game space-game --environment smoke --region local --protocol 1 \
  --display-name "Local diagnostic" --timeout-seconds 60 --run-seconds 60 \
  --exit-after-echo

Copy the public listing ID printed by the host, or discover it from terminal 2:

dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  browse --service http://127.0.0.1:8080/ \
  --game space-game --environment smoke --region local --protocol 1

In terminal 3, either omit --listing and select interactively, or provide the copied ID for deterministic selection:

dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  join --service http://127.0.0.1:8080/ --mediator 127.0.0.1:9050 \
  --game space-game --environment smoke --region local --protocol 1 \
  --listing REPLACE_WITH_LISTING_UUID --timeout-seconds 30

Success means the joiner prints join.connected and verified direct traffic, and the host prints verified direct traffic before deregistering. The host and joiner each create one caller-owned LiteNetLib manager. The same UDP socket sends presence and punch traffic, accepts the authenticated peer, and carries the ping/echo/ack/completion payload; direct traffic does not pass through the HTTP service or mediator.

Clean up secrets and the disposable service when finished:

unset RENDEZVOUS_PUBLISHER_CREDENTIAL
docker compose -f deploy/compose/compose.yaml down
rm deploy/compose/secrets/signing-key

Script and JSON automation

--script forbids prompts and selects the first compatible listing unless --listing UUID fixes the choice. --json emits one JSON object per line with version: 1. New optional properties may be added, but event names and exit codes are stable automation contracts. Informational events use stdout and failures use stderr.

Successful direct-connection and direct-traffic events include the coarse addressFamily value ipv4 or ipv6. They never include the peer address.

The deployment smoke performs the full health, publish, join, mediation, direct traffic, outcome-report, and cleanup flow using bounded waits:

dotnet build src/FinalFactory.Rendezvous.TestClient --configuration Release
./scripts/smoke-deployment.sh

For custom automation, capture JSON and preserve the process status separately:

set +e
dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  browse --service http://127.0.0.1:8080/ \
  --game space-game --environment smoke --region local --protocol 1 \
  --script --json >browse.jsonl
status=$?
set -e
jq -e 'select(.version == 1 and .event == "browse.completed")' browse.jsonl
test "$status" -eq 0

Never use an unbounded sleep to orchestrate processes. Wait for versioned events such as host.ready and apply a deadline. Useful success events are host.registered, host.ready, host.direct-traffic, host.deregistered, browse.completed, browse.session, join.connected, join.direct-traffic, and join.outcome-report.

Exit Meaning
0 Requested diagnostic flow completed successfully
2 Invalid command or options
3 Missing or invalid local configuration
10 HTTP, registration, browser, lease, or socket failure
11 No compatible session was available or selected
12 Authorization or traversal reached a typed terminal failure
13 Direct connection succeeded but the direct traffic proof failed
130 Caller cancellation or Ctrl+C

Observe a safe failure

Run this after the protocol-1 browse in terminal 2 and before the terminal-3 join (or restart terminal 1 first). The preceding browse proves that one protocol-1 host is present. Now browse for deliberately incompatible protocol 999. The command emits a successful directory response with browse.completed, count: 0, then exits 11 to distinguish compatibility from a service outage:

set +e
dotnet run --project src/FinalFactory.Rendezvous.TestClient \
  --configuration Release --no-build -- \
  browse --service http://127.0.0.1:8080/ \
  --game space-game --environment smoke --region local --protocol 999 \
  --script --json >incompatible.jsonl
status=$?
set -e
jq -e 'select(.event == "browse.completed" and .phase == "directory" and .count == 0)' \
  incompatible.jsonl
test "$status" -eq 11

This is a diagnostic failure drill, not a bypass: unknown tenant scope and protocols still fail closed, and the local helper cannot mint a credential for them.

Diagnose by phase, not by guesswork

Start with the exit code, then the last versioned event and its phase, status, and typed outcome. Endpoint categories may be reported as loopback, private, or public; raw endpoints, credentials, capabilities, metadata, and player identities are never emitted.

Symptom or last event Distinction Check next
host.configuration, exit 3 Local credential variable is missing or malformed before any request Confirm the named environment variable exists, tracing is off, and the credential has not expired
host.registration, exit 10 Publisher authentication, tenant policy, metadata, quota, or HTTP failure Use the typed status; compare credential scope with game/environment/region and the provisioned policy, then correlate protected server telemetry by operation and time
browse.sessions, exit 10 Directory request failed Check HTTP reachability, /health/ready, rate limiting, and contract compatibility
browse.completed count 0, or join.selection empty, exit 11 Healthy directory but no compatible visible listing Match game, environment, region, and exact gameplay protocol; then confirm a host lease is still active
Exact join.selection failure, exit 10 Listing disappeared, is hidden, or scope no longer matches Browse again; do not retry an old listing ID forever
join.authorization, exit 12 Service rejected the attempt before NAT traversal Inspect typed category/outcome for policy, capacity, stale host, or active-attempt limits
join.punch / join.traversal, exit 12 Mediation or NAT traversal did not establish a peer Confirm UDP endpoint/reply path, host presence, clocks, firewall/NAT behavior, and topology; use a game-owned fallback if policy supplies one
join.direct-connect, exit 12 Introduction occurred but authenticated direct admission failed Confirm host is polling the same socket, the one-time ticket is current, and game admission did not reject capacity, identity, or bans
join.connected followed by exit 13 Peer connected but the direct gameplay-like echo did not finish Inspect the peer lifecycle and caller polling; this is not an HTTP/directory failure

Stopping a host without deregistration may leave its listing visible only until the bounded lease expires. During that window, a join can produce a typed stale host or traversal outcome; it must not be interpreted as a healthy host. Restarting the single-active service intentionally loses all ephemeral listings and attempts, so hosts re-register and clients browse again.

If a terminal outcome reports an authoritative dedicated fallback, join.fallback exposes only availability and endpoint type. TestClient never connects to it automatically. The game owns the decision, authentication, and connection policy. If no fallback is present, Rendezvous v1 offers no relay.

Production use

Do not copy a production signing key to a diagnostic host. Supply a short-lived, least-scope publisher credential from the deployment secret boundary and set the external service, mediator, and matching scope variables described in the secure Linux deployment smoke. Run representative external-network tests; loopback success is not NAT coverage. Use the redacting, bounded real-network canary procedure for formal production evidence rather than committing raw TestClient JSON.