520 lines
21 KiB
Go
520 lines
21 KiB
Go
package companion
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testProof = "fixture-component-proof-material"
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
return fn(request)
|
|
}
|
|
|
|
func TestLoadConfigKeepsProofOutOfConfig(t *testing.T) {
|
|
file, err := os.Open("config.yaml.example")
|
|
if err != nil {
|
|
t.Fatalf("open config fixture: %v", err)
|
|
}
|
|
defer file.Close()
|
|
config, err := LoadConfig(file)
|
|
if err != nil {
|
|
t.Fatalf("load config fixture: %v", err)
|
|
}
|
|
encoded, err := json.Marshal(config)
|
|
if err != nil {
|
|
t.Fatalf("marshal config fixture: %v", err)
|
|
}
|
|
serialized := string(encoded)
|
|
for _, forbidden := range []string{testProof, "scum_client_credential", "sessionToken", "/api/v1/scum-clients/", "InsecureSkipVerify"} {
|
|
if strings.Contains(serialized, forbidden) {
|
|
t.Fatalf("config exposed forbidden content %q: %s", forbidden, serialized)
|
|
}
|
|
}
|
|
|
|
t.Setenv(ProofEnvironment, "")
|
|
if _, err := NewClient(config, Options{}); err == nil {
|
|
t.Fatal("expected missing proof environment to be rejected")
|
|
}
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
if _, err := NewClient(config, Options{}); err != nil {
|
|
t.Fatalf("create client from proof environment: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigRejectsLegacySharedCredentialFields(t *testing.T) {
|
|
fixture, err := os.ReadFile("config.yaml.example")
|
|
if err != nil {
|
|
t.Fatalf("read config fixture: %v", err)
|
|
}
|
|
for name, legacy := range map[string]string{
|
|
"legacy endpoint": "server_url: https://legacy.example.test\n",
|
|
"shared credential": "scum_client_credential: legacy-shared-value\n",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := LoadConfig(strings.NewReader(string(fixture) + legacy)); err == nil {
|
|
t.Fatalf("expected legacy config field to be rejected: %s", legacy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewClientRejectsHTTPPlatformBaseURL(t *testing.T) {
|
|
config := loadTestConfig(t)
|
|
config.Platform.BaseURL = "http://platform.example.test"
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
|
|
if _, err := NewClient(config, Options{}); err == nil || !strings.Contains(err.Error(), "HTTPS") {
|
|
t.Fatalf("expected HTTP platform origin to be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewClientCanonicalizesPlatformOriginAndRejectsInvalidPorts(t *testing.T) {
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
config := loadTestConfig(t)
|
|
config.Platform.BaseURL = " https://platform.example.test:8443/ "
|
|
client, err := NewClient(config, Options{})
|
|
if err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
if client.config.Platform.BaseURL != "https://platform.example.test:8443" {
|
|
t.Fatalf("platform origin was not canonicalized: %q", client.config.Platform.BaseURL)
|
|
}
|
|
|
|
for _, baseURL := range []string{"https://:443", "https://platform.example.test:", "https://platform.example.test:65536"} {
|
|
config.Platform.BaseURL = baseURL
|
|
if _, err := NewClient(config, Options{}); err == nil {
|
|
t.Fatalf("expected invalid platform origin to be rejected: %q", baseURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewClientUsesBoundedConfiguredRequestTimeout(t *testing.T) {
|
|
config := loadTestConfig(t)
|
|
config.Timing.RequestTimeoutSeconds = 7
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
|
|
client, err := NewClient(config, Options{})
|
|
if err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
if client.httpClient.Timeout != 7*time.Second {
|
|
t.Fatalf("unexpected request timeout: %s", client.httpClient.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestNewClientDefaultTransportVerifiesSystemRootsWithTLS12Minimum(t *testing.T) {
|
|
config := loadTestConfig(t)
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
|
|
client, err := NewClient(config, Options{})
|
|
if err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
transport, ok := client.httpClient.Transport.(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("unexpected transport type: %T", client.httpClient.Transport)
|
|
}
|
|
if transport == http.DefaultTransport {
|
|
t.Fatal("client must own its transport")
|
|
}
|
|
if transport.TLSClientConfig == nil {
|
|
t.Fatal("client transport must declare an explicit TLS policy")
|
|
}
|
|
if transport.TLSClientConfig.InsecureSkipVerify {
|
|
t.Fatal("client transport must verify server certificates")
|
|
}
|
|
if transport.TLSClientConfig.MinVersion < tls.VersionTLS12 {
|
|
t.Fatalf("client transport allows TLS below 1.2: %x", transport.TLSClientConfig.MinVersion)
|
|
}
|
|
if transport.TLSClientConfig.RootCAs != nil {
|
|
t.Fatal("nil RootCAs must select the host system root pool")
|
|
}
|
|
if client.httpClient.CheckRedirect == nil {
|
|
t.Fatal("client must explicitly reject redirects")
|
|
}
|
|
}
|
|
|
|
func TestNewClientRejectsUntrustedTLSServerByDefault(t *testing.T) {
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
config := loadTestConfig(t)
|
|
config.Platform.BaseURL = server.URL
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
client, err := NewClient(config, Options{
|
|
Now: func() time.Time { return time.Date(2026, 7, 20, 9, 0, 0, 0, time.UTC) },
|
|
Nonce: func() (string, error) { return "nonce-fixture-untrusted-tls", nil },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
|
|
_, err = client.Register(context.Background())
|
|
var verificationError *tls.CertificateVerificationError
|
|
if !errors.As(err, &verificationError) {
|
|
t.Fatalf("expected untrusted TLS certificate verification failure, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClientRejectsRedirectWithoutReplayingComponentSession(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 9, 30, 0, 0, time.UTC)
|
|
config := loadTestConfig(t)
|
|
requests := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
requests++
|
|
switch requests {
|
|
case 1:
|
|
assertPlatformRequest(t, request, registerPath)
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{
|
|
Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-one",
|
|
ExpiresAt: stamp.Add(15 * time.Minute), HeartbeatEverySeconds: 30, ServerTime: stamp,
|
|
}), nil
|
|
case 2:
|
|
assertPlatformRequest(t, request, claimPath)
|
|
var body claimRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-one" {
|
|
t.Fatalf("claim did not carry the component session: %+v", body)
|
|
}
|
|
response := jsonHTTPResponse(http.StatusTemporaryRedirect, nil)
|
|
response.Header.Set("Location", "https://redirect.example.test/capture")
|
|
return response, nil
|
|
default:
|
|
t.Fatalf("redirect replayed component material to %s", request.URL.String())
|
|
return nil, fmt.Errorf("unexpected redirected request")
|
|
}
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
if _, err := client.Register(context.Background()); err != nil {
|
|
t.Fatalf("register client: %v", err)
|
|
}
|
|
_, err := client.ClaimCommands(context.Background(), 1)
|
|
var statusError HTTPError
|
|
if !errors.As(err, &statusError) || statusError.StatusCode != http.StatusTemporaryRedirect {
|
|
t.Fatalf("expected redirect response to be rejected, got %v", err)
|
|
}
|
|
if requests != 2 {
|
|
t.Fatalf("redirect unexpectedly triggered %d requests", requests)
|
|
}
|
|
}
|
|
|
|
func TestRegisterUsesExactCanonicalHMAC(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 5, 4, 3, 123456789, time.UTC)
|
|
config := loadTestConfig(t)
|
|
for left, right := 0, len(config.Capabilities)-1; left < right; left, right = left+1, right-1 {
|
|
config.Capabilities[left], config.Capabilities[right] = config.Capabilities[right], config.Capabilities[left]
|
|
}
|
|
var captured registerRequest
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
assertPlatformRequest(t, request, registerPath)
|
|
decodeRequest(t, request, &captured)
|
|
if strings.Contains(captured.Signature, testProof) {
|
|
t.Fatal("registration signature contains raw proof")
|
|
}
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{
|
|
Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-one",
|
|
ExpiresAt: stamp.Add(15 * time.Minute), HeartbeatEverySeconds: 30, ServerTime: stamp,
|
|
}), nil
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
registration, err := client.Register(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("register client: %v", err)
|
|
}
|
|
if registration.HeartbeatEverySeconds != 30 || !registration.ExpiresAt.Equal(stamp.Add(15*time.Minute)) {
|
|
t.Fatalf("unexpected registration projection: %+v", registration)
|
|
}
|
|
if captured.Timestamp.Format(time.RFC3339Nano) != "2026-07-20T05:04:03.123456789Z" {
|
|
t.Fatalf("registration timestamp was not RFC3339Nano UTC: %s", captured.Timestamp.Format(time.RFC3339Nano))
|
|
}
|
|
expected := independentRegistrationSignature(testProof, captured)
|
|
if captured.Signature != expected {
|
|
t.Fatalf("unexpected registration signature: got %s want %s", captured.Signature, expected)
|
|
}
|
|
if captured.Nonce != "nonce-fixture-registration-0001" {
|
|
t.Fatalf("unexpected registration nonce: %s", captured.Nonce)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotSequenceContinuesAcrossComponentSessions(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 7, 0, 0, 0, time.UTC)
|
|
config := loadTestConfig(t)
|
|
step := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
switch step {
|
|
case 0, 2:
|
|
assertPlatformRequest(t, request, registerPath)
|
|
step++
|
|
session := "component-session-one"
|
|
if step == 3 {
|
|
session = "component-session-two"
|
|
}
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: session, ExpiresAt: stamp.Add(15 * time.Minute), HeartbeatEverySeconds: 30, ServerTime: stamp}), nil
|
|
case 1, 3:
|
|
assertPlatformRequest(t, request, snapshotPath)
|
|
var body snapshotRequest
|
|
decodeRequest(t, request, &body)
|
|
expectedSession := "component-session-one"
|
|
expectedSequence := uint64(10)
|
|
if step == 3 {
|
|
expectedSession = "component-session-two"
|
|
expectedSequence = 11
|
|
}
|
|
if body.SessionToken != expectedSession || body.Sequence != expectedSequence {
|
|
t.Fatalf("snapshot did not continue across component sessions: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusAccepted, AcceptedSnapshot{SnapshotID: fmt.Sprintf("snapshot-%d", body.Sequence), 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 request %s", request.URL.Path)
|
|
}
|
|
})
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
nonce := 0
|
|
client, err := NewClient(config, Options{
|
|
Now: func() time.Time { return stamp },
|
|
Nonce: func() (string, error) {
|
|
nonce++
|
|
return fmt.Sprintf("nonce-fixture-session-%04d", nonce), nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create client: %v", err)
|
|
}
|
|
client.httpClient.Transport = transport
|
|
upload := func(sequence uint64) error {
|
|
_, err := client.UploadSnapshot(context.Background(), Snapshot{Type: "companion.health", SchemaVersion: "1", StreamKey: "current", Sequence: sequence, ObservedAt: stamp, Payload: map[string]any{"status": "online", "observedAt": stamp.Format(time.RFC3339)}, KeepForSeconds: 604800, MaxRecords: 1000})
|
|
return err
|
|
}
|
|
if _, err := client.Register(context.Background()); err != nil {
|
|
t.Fatalf("register first session: %v", err)
|
|
}
|
|
if err := upload(10); err != nil {
|
|
t.Fatalf("upload first session snapshot: %v", err)
|
|
}
|
|
if _, err := client.Register(context.Background()); err != nil {
|
|
t.Fatalf("register replacement session: %v", err)
|
|
}
|
|
if err := upload(10); err == nil || !strings.Contains(err.Error(), "sequence") {
|
|
t.Fatalf("expected stale sequence to be rejected before transport, got %v", err)
|
|
}
|
|
if err := upload(11); err != nil {
|
|
t.Fatalf("upload replacement session snapshot: %v", err)
|
|
}
|
|
if step != 4 {
|
|
t.Fatalf("expected four transport requests, got %d", step)
|
|
}
|
|
}
|
|
|
|
func TestSnapshotRequiresHTTPAccepted(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 8, 0, 0, 0, time.UTC)
|
|
config := loadTestConfig(t)
|
|
step := 0
|
|
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
|
step++
|
|
if step == 1 {
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-one", ExpiresAt: stamp.Add(15 * time.Minute), HeartbeatEverySeconds: 30, ServerTime: stamp}), nil
|
|
}
|
|
return jsonHTTPResponse(http.StatusOK, AcceptedSnapshot{}), nil
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
if _, err := client.Register(context.Background()); err != nil {
|
|
t.Fatalf("register client: %v", err)
|
|
}
|
|
_, err := client.UploadSnapshot(context.Background(), Snapshot{Type: "companion.health", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: stamp, Payload: map[string]any{"status": "online", "observedAt": stamp.Format(time.RFC3339)}, KeepForSeconds: 604800, MaxRecords: 1000})
|
|
var statusError HTTPError
|
|
if !errors.As(err, &statusError) || statusError.StatusCode != http.StatusOK || statusError.ExpectedStatus != http.StatusAccepted {
|
|
t.Fatalf("expected exact 202 enforcement, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestComponentSessionHeartbeatAndBridgeTransport(t *testing.T) {
|
|
stamp := time.Date(2026, 7, 20, 6, 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) {
|
|
if strings.Contains(request.URL.Path, "/api/v1/scum-clients/") {
|
|
t.Fatalf("legacy endpoint used: %s", request.URL.Path)
|
|
}
|
|
switch step {
|
|
case 0:
|
|
assertPlatformRequest(t, request, registerPath)
|
|
var body registerRequest
|
|
decodeRequest(t, request, &body)
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, registerResponse{Accepted: true, InstallationID: config.Component.InstallationID, SessionToken: "component-session-one", 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-one" || body.Sequence != 1 || body.Health != "healthy" || body.HealthReason != "bridge ready" {
|
|
t.Fatalf("unexpected heartbeat body: %+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-one" || body.Limit != 5 {
|
|
t.Fatalf("unexpected claim body: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, claimResponse{Items: []ClaimedCommand{{ID: "command-1", ProfileKey: ProfileKey, CommandType: "companion.diagnostics", Payload: map[string]any{"includeWindowState": true}, FencingToken: 9, 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/command-1/ack")
|
|
var body ackRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-one" || body.FencingToken != 9 {
|
|
t.Fatalf("unexpected ack body: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, CommandAck{CommandID: "command-1", State: "claimed", FencingToken: 9, AcknowledgedAt: stamp}), nil
|
|
case 4:
|
|
assertPlatformRequest(t, request, "/api/v1/game-client-bridge/companion/commands/command-1/result")
|
|
var body resultRequest
|
|
decodeRequest(t, request, &body)
|
|
if body.SessionToken != "component-session-one" || body.FencingToken != 9 || body.Status != "succeeded" || body.Summary != "diagnostics collected" || body.Payload["entries"] != float64(3) {
|
|
t.Fatalf("unexpected result body: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusOK, map[string]any{"commandId": "command-1", "state": "succeeded", "result": map[string]any{"status": "succeeded", "summary": "diagnostics collected", "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-one" || body.Type != "companion.health" || body.SchemaVersion != "1" || body.StreamKey != "current" || body.Sequence != 1 || body.KeepForSeconds != 604800 || body.MaxRecords != 1000 {
|
|
t.Fatalf("unexpected snapshot body: %+v", body)
|
|
}
|
|
step++
|
|
return jsonHTTPResponse(http.StatusAccepted, AcceptedSnapshot{SnapshotID: "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 request %s", request.URL.Path)
|
|
}
|
|
})
|
|
client := newTestClient(t, config, transport, stamp)
|
|
if _, err := client.Register(context.Background()); err != nil {
|
|
t.Fatalf("register client: %v", err)
|
|
}
|
|
if _, err := client.Heartbeat(context.Background(), HealthReport{Status: "healthy", Reason: "bridge ready"}); err != nil {
|
|
t.Fatalf("heartbeat: %v", err)
|
|
}
|
|
commands, err := client.ClaimCommands(context.Background(), 5)
|
|
if err != nil || len(commands) != 1 {
|
|
t.Fatalf("claim commands: commands=%+v err=%v", commands, err)
|
|
}
|
|
if _, err := client.AckCommand(context.Background(), commands[0].ID, commands[0].FencingToken); err != nil {
|
|
t.Fatalf("ack command: %v", err)
|
|
}
|
|
if _, err := client.CompleteCommand(context.Background(), commands[0].ID, commands[0].FencingToken, CommandResult{Status: "succeeded", Summary: "diagnostics collected", Payload: map[string]any{"entries": 3}}); err != nil {
|
|
t.Fatalf("complete command: %v", err)
|
|
}
|
|
snapshot := Snapshot{Type: "companion.health", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: stamp, Payload: map[string]any{"status": "online", "observedAt": stamp.Format(time.RFC3339)}, KeepForSeconds: 604800, MaxRecords: 1000}
|
|
accepted, err := client.UploadSnapshot(context.Background(), snapshot)
|
|
if err != nil || accepted.SnapshotID != "snapshot-1" {
|
|
t.Fatalf("upload snapshot: accepted=%+v err=%v", accepted, err)
|
|
}
|
|
if step != 6 {
|
|
t.Fatalf("expected six fixed API requests, got %d", step)
|
|
}
|
|
}
|
|
|
|
func loadTestConfig(t *testing.T) Config {
|
|
t.Helper()
|
|
file, err := os.Open("config.yaml.example")
|
|
if err != nil {
|
|
t.Fatalf("open config fixture: %v", err)
|
|
}
|
|
defer file.Close()
|
|
config, err := LoadConfig(file)
|
|
if err != nil {
|
|
t.Fatalf("load config fixture: %v", err)
|
|
}
|
|
return config
|
|
}
|
|
|
|
func newTestClient(t *testing.T, config Config, transport http.RoundTripper, stamp time.Time) *Client {
|
|
t.Helper()
|
|
t.Setenv(ProofEnvironment, testProof)
|
|
client, err := NewClient(config, Options{
|
|
Now: func() time.Time { return stamp },
|
|
Nonce: func() (string, error) { return "nonce-fixture-registration-0001", nil },
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create test client: %v", err)
|
|
}
|
|
client.httpClient.Transport = transport
|
|
return client
|
|
}
|
|
|
|
func assertPlatformRequest(t *testing.T, request *http.Request, expectedPath string) {
|
|
t.Helper()
|
|
if request.Method != http.MethodPost || request.URL.Scheme != "https" || request.URL.Host != "platform.example.test" || request.URL.Path != expectedPath {
|
|
t.Fatalf("unexpected platform request: %s %s", request.Method, request.URL.String())
|
|
}
|
|
if request.Header.Get("Authorization") != "" {
|
|
t.Fatalf("component session must be carried in the typed JSON body, got Authorization header")
|
|
}
|
|
if request.Header.Get("Content-Type") != "application/json" || request.Header.Get("Accept") != "application/json" {
|
|
t.Fatalf("unexpected request headers: %+v", request.Header)
|
|
}
|
|
}
|
|
|
|
func decodeRequest(t *testing.T, request *http.Request, target any) {
|
|
t.Helper()
|
|
decoder := json.NewDecoder(request.Body)
|
|
if err := decoder.Decode(target); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
}
|
|
|
|
func jsonHTTPResponse(status int, payload any) *http.Response {
|
|
encoded, _ := json.Marshal(payload)
|
|
return &http.Response{StatusCode: status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(string(encoded)))}
|
|
}
|
|
|
|
func independentRegistrationSignature(proof string, request registerRequest) string {
|
|
capabilities := append([]string(nil), request.Capabilities...)
|
|
sort.Strings(capabilities)
|
|
canonical := strings.Join([]string{
|
|
request.InstallationID,
|
|
request.ServerInstanceID,
|
|
request.ProfileKey,
|
|
request.ArtifactID,
|
|
request.Version,
|
|
request.SourceRevision,
|
|
request.TargetOS,
|
|
request.TargetArch,
|
|
strconv.Itoa(request.KeyGeneration),
|
|
strconv.Itoa(request.DeploymentGeneration),
|
|
request.Timestamp.UTC().Format(time.RFC3339Nano),
|
|
request.Nonce,
|
|
strings.Join(capabilities, ","),
|
|
}, "\n")
|
|
mac := hmac.New(sha256.New, []byte(proof))
|
|
_, _ = mac.Write([]byte(canonical))
|
|
return "sha256:" + hex.EncodeToString(mac.Sum(nil))
|
|
}
|