214 lines
11 KiB
Go
214 lines
11 KiB
Go
package companion
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestRunOneShotSmokeProcessesOnlySafeDiagnosticsAndUploadsIsolatedHealth(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 9, 10, 11, 123456789, time.UTC)
|
|
config := loadTestConfig(t)
|
|
sessionExpiry := stamp.Add(15 * time.Minute)
|
|
step := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
switch step {
|
|
case 0:
|
|
assertPlatformRequest(t, request, registerPath)
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-smoke", ExpiresAt: sessionExpiry, HeartbeatEverySeconds: 30, ServerTime: stamp}), nil
|
|
case 1:
|
|
assertPlatformRequest(t, request, heartbeatPath)
|
|
var body heartbeatRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-smoke" || body.HealthReason != "one-shot smoke ready" {
|
|
t.Fatalf("unexpected smoke heartbeat: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, heartbeatResponse{Accepted: true, InstallationID: config.Component.InstallationID, Status: "online", Health: "healthy", NextHeartbeatSeconds: 30, SessionExpiresAt: sessionExpiry, ServerTime: stamp}), nil
|
|
case 2:
|
|
assertPlatformRequest(t, request, claimPath)
|
|
var body claimRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-smoke" || body.Limit != 1 {
|
|
t.Fatalf("unexpected smoke claim: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, claimResponse{Items: []ClaimedCommand{{ID: "diagnostics-1", ProfileKey: ProfileKey, CommandType: companionDiagnosticsCommandType, Payload: map[string]any{"includeWindowState": true, "maxEntries": 3}, FencingToken: 17, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(5 * time.Minute)}}, Count: 1}), nil
|
|
case 3:
|
|
assertPlatformRequest(t, request, "/api/v1/game-client-bridge/companion/commands/diagnostics-1/ack")
|
|
var body ackRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-smoke" || body.FencingToken != 17 {
|
|
t.Fatalf("unexpected smoke ack: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, CommandAck{CommandID: "diagnostics-1", State: "claimed", FencingToken: 17, AcknowledgedAt: stamp}), nil
|
|
case 4:
|
|
assertPlatformRequest(t, request, "/api/v1/game-client-bridge/companion/commands/diagnostics-1/result")
|
|
var body resultRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-smoke" || body.FencingToken != 17 || body.Status != "succeeded" || body.Summary != "companion diagnostics available" {
|
|
t.Fatalf("unexpected smoke result: %+v", body)
|
|
}
|
|
if body.Payload["status"] != "online" || body.Payload["version"] != config.Component.Version || body.Payload["lastHeartbeatAt"] != stamp.Format(time.RFC3339Nano) {
|
|
t.Fatalf("unexpected bounded diagnostics payload: %+v", body.Payload)
|
|
}
|
|
serialized, _ := json.Marshal(body)
|
|
if strings.Contains(string(serialized), testProof) {
|
|
t.Fatalf("result exposed proof: %s", serialized)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, map[string]any{"commandId": "diagnostics-1", "state": "succeeded", "result": map[string]any{"status": "succeeded", "summary": "companion diagnostics available", "completedAt": stamp}, "updatedAt": stamp, "completedAt": stamp}), nil
|
|
case 5:
|
|
assertPlatformRequest(t, request, snapshotPath)
|
|
var body snapshotRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-smoke" || body.Type != companionHealthSnapshotType || body.SchemaVersion != companionHealthSchemaVersion || body.StreamKey != "smoke-fixture-stream-nonce-0001" || body.Sequence != 1 {
|
|
t.Fatalf("unexpected isolated smoke snapshot identity: %+v", body)
|
|
}
|
|
if !body.ObservedAt.Equal(stamp) || body.KeepForSeconds != 604800 || body.MaxRecords != 1000 || body.Payload["status"] != "online" || body.Payload["observedAt"] != stamp.Format(time.RFC3339Nano) {
|
|
t.Fatalf("unexpected typed health snapshot: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusAccepted, AcceptedSnapshot{SnapshotID: "health-snapshot-1", ProfileKey: ProfileKey, Type: body.Type, SchemaVersion: body.SchemaVersion, StreamKey: body.StreamKey, Sequence: body.Sequence, AcceptedAt: stamp, ExpiresAt: stamp.Add(7 * 24 * time.Hour)}), nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected smoke transport: %s", request.URL.Path)
|
|
}
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
result, err := RunOneShotSmoke(context.Background(), client, SmokeOptions{IsolatedNonProduction: true, StreamSuffix: func() (string, error) { return "fixture-stream-nonce-0001", nil }})
|
|
if err != nil {
|
|
t.Fatalf("run one-shot smoke: %v", err)
|
|
}
|
|
if result.ClaimedCount != 1 || result.CompletedCount != 1 || result.UnsupportedCount != 0 || result.SnapshotID != "health-snapshot-1" {
|
|
t.Fatalf("unexpected safe smoke result: %+v", result)
|
|
}
|
|
if len(result.CompletedCommandIDs) != 1 || result.CompletedCommandIDs[0] != "diagnostics-1" || len(result.UnsupportedCommandIDs) != 0 {
|
|
t.Fatalf("unexpected smoke IDs: %+v", result)
|
|
}
|
|
assertSafeSmokeOutput(t, result)
|
|
if step != 6 {
|
|
t.Fatalf("expected diagnostics ack/result plus snapshot, got %d requests", step)
|
|
}
|
|
}
|
|
|
|
func TestRunOneShotSmokeLeavesUnsupportedCommandUnackedAndUnexecuted(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC)
|
|
config := loadTestConfig(t)
|
|
sessionExpiry := stamp.Add(15 * time.Minute)
|
|
step := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
switch step {
|
|
case 0:
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-unsupported", ExpiresAt: sessionExpiry, HeartbeatEverySeconds: 30, ServerTime: stamp}), nil
|
|
case 1:
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, heartbeatResponse{Accepted: true, InstallationID: config.Component.InstallationID, Status: "online", Health: "healthy", NextHeartbeatSeconds: 30, SessionExpiresAt: sessionExpiry, ServerTime: stamp}), nil
|
|
case 2:
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, claimResponse{Items: []ClaimedCommand{{ID: "unsupported-1", ProfileKey: ProfileKey, CommandType: "announcement.send", Payload: map[string]any{"message": "must not execute"}, FencingToken: 18, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(5 * time.Minute)}}, Count: 1}), nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported command triggered transport: %s", request.URL.Path)
|
|
}
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
result, err := RunOneShotSmoke(context.Background(), client, SmokeOptions{IsolatedNonProduction: true, StreamSuffix: func() (string, error) { return "fixture-stream-nonce-0002", nil }})
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported") {
|
|
t.Fatalf("expected unsupported smoke to stop, result=%+v err=%v", result, err)
|
|
}
|
|
if result.ClaimedCount != 1 || result.CompletedCount != 0 || result.UnsupportedCount != 1 || len(result.CompletedCommandIDs) != 0 || len(result.UnsupportedCommandIDs) != 1 || result.UnsupportedCommandIDs[0] != "unsupported-1" {
|
|
t.Fatalf("unsupported command did not fail safely: %+v", result)
|
|
}
|
|
assertSafeSmokeOutput(t, result)
|
|
if step != 3 {
|
|
t.Fatalf("expected no unsupported ack/result requests, got %d steps", step)
|
|
}
|
|
}
|
|
|
|
func TestRunOneShotSmokeRejectsNonSingletonClaimBeforeExecution(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 10, 30, 0, 0, time.UTC)
|
|
for _, test := range []struct {
|
|
name string
|
|
items []ClaimedCommand
|
|
}{
|
|
{name: "zero"},
|
|
{name: "multiple", items: []ClaimedCommand{{ID: "diagnostics-1"}, {ID: "diagnostics-2"}}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
config := loadTestConfig(t)
|
|
step := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
step++
|
|
switch step {
|
|
case 1:
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-singleton", ExpiresAt: stamp.Add(15 * time.Minute), HeartbeatEverySeconds: 30, ServerTime: stamp}), nil
|
|
case 2:
|
|
return jsonHTTPResponse(http.StatusOK, heartbeatResponse{Accepted: true, InstallationID: config.Component.InstallationID, Status: "online", Health: "healthy", NextHeartbeatSeconds: 30, SessionExpiresAt: stamp.Add(15 * time.Minute), ServerTime: stamp}), nil
|
|
case 3:
|
|
return jsonHTTPResponse(http.StatusOK, claimResponse{Items: test.items, Count: len(test.items)}), nil
|
|
default:
|
|
return nil, fmt.Errorf("non-singleton claim triggered execution: %s", request.URL.Path)
|
|
}
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
result, err := RunOneShotSmoke(context.Background(), client, SmokeOptions{IsolatedNonProduction: true, StreamSuffix: func() (string, error) { return "fixture-singleton-nonce", nil }})
|
|
if err == nil || !strings.Contains(err.Error(), "exactly one") || result.ClaimedCount != len(test.items) || step != 3 {
|
|
t.Fatalf("non-singleton claim was not rejected: result=%+v step=%d err=%v", result, step, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateSmokeDiagnosticsCommandRejectsUnsafeClaims(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 11, 0, 0, 0, time.UTC)
|
|
valid := ClaimedCommand{ID: "diagnostics-1", ProfileKey: ProfileKey, CommandType: companionDiagnosticsCommandType, Payload: map[string]any{"includeWindowState": false, "maxEntries": float64(5)}, FencingToken: 7, LeaseExpiresAt: stamp.Add(time.Minute), ExpiresAt: stamp.Add(2 * time.Minute)}
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*ClaimedCommand)
|
|
}{
|
|
{name: "profile", mutate: func(command *ClaimedCommand) { command.ProfileKey = "other" }},
|
|
{name: "fence", mutate: func(command *ClaimedCommand) { command.FencingToken = 0 }},
|
|
{name: "lease", mutate: func(command *ClaimedCommand) { command.LeaseExpiresAt = stamp }},
|
|
{name: "expiry", mutate: func(command *ClaimedCommand) { command.ExpiresAt = stamp.Add(-time.Second) }},
|
|
{name: "nil payload", mutate: func(command *ClaimedCommand) { command.Payload = nil }},
|
|
{name: "unknown payload", mutate: func(command *ClaimedCommand) { command.Payload["message"] = "not allowed" }},
|
|
{name: "payload type", mutate: func(command *ClaimedCommand) { command.Payload["maxEntries"] = 2.5 }},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
candidate := valid
|
|
candidate.Payload = map[string]any{"includeWindowState": false, "maxEntries": float64(5)}
|
|
test.mutate(&candidate)
|
|
if err := validateSmokeDiagnosticsCommand(candidate, stamp); err == nil {
|
|
t.Fatalf("expected unsafe diagnostics claim to be rejected: %+v", candidate)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunOneShotSmokeRequiresIsolatedNonProductionQueue(t *testing.T) {
|
|
result, err := RunOneShotSmoke(context.Background(), &Client{}, SmokeOptions{})
|
|
if err == nil || !strings.Contains(err.Error(), "isolated non-production") {
|
|
t.Fatalf("expected isolation gate, result=%+v err=%v", result, err)
|
|
}
|
|
}
|
|
|
|
func assertSafeSmokeOutput(t *testing.T, result SmokeResult) {
|
|
t.Helper()
|
|
serialized, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Fatalf("marshal smoke result: %v", err)
|
|
}
|
|
for _, forbidden := range []string{testProof, "component-session", "includeWindowState", "must not execute", "payload"} {
|
|
if strings.Contains(string(serialized), forbidden) {
|
|
t.Fatalf("smoke output exposed %q: %s", forbidden, serialized)
|
|
}
|
|
}
|
|
}
|