功能修改
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func seedGameClientBridgeComponentSession(t *testing.T, svc *CoreService, now time.Time, token string) (domain.ClientManagerInstallation, domain.ClientManagerSession) {
|
||||
t.Helper()
|
||||
installation := domain.ClientManagerInstallation{ID: "installation-1", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", RunEndpointID: "run-1", Status: domain.ClientManagerLifecycleOnline, ActiveArtifactID: "artifact-1", KeyGeneration: 2, DeploymentGeneration: 3}
|
||||
session := domain.ClientManagerSession{ID: "component-session-1", InstallationID: installation.ID, ServerInstanceID: installation.ServerInstanceID, ProfileKey: installation.ProfileKey, RunEndpointID: installation.RunEndpointID, ArtifactID: installation.ActiveArtifactID, KeyGeneration: installation.KeyGeneration, DeploymentGeneration: installation.DeploymentGeneration, TokenHash: tokenHash(token), Capabilities: []string{"component.heartbeat", gameClientBridgeCapability}, Status: domain.ClientManagerSessionActive, ExpiresAt: now.Add(time.Hour)}
|
||||
key := domain.EncryptedComponentKey{ID: "key-1", ServerInstanceID: installation.ServerInstanceID, ComponentKind: domain.DistributionComponentClientManager, ComponentKey: installation.ProfileKey, Generation: installation.KeyGeneration, Status: domain.ComponentKeyStatusActive}
|
||||
if err := svc.store.ClientManagerInstallations().Create(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.store.ClientManagerSessions().Create(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.store.EncryptedComponentKeys().Create(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return installation, session
|
||||
}
|
||||
|
||||
func TestGameClientBridgeComponentSessionAuthorizesCommandsAndSnapshots(t *testing.T) {
|
||||
svc, clock := newGameClientBridgeService(t)
|
||||
const token = "component-session-token"
|
||||
_, session := seedGameClientBridgeComponentSession(t, svc, *clock, token)
|
||||
command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "authorized-1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token, Limit: 5})
|
||||
if err != nil || len(claimed) != 1 || claimed[0].ID != command.ID || claimed[0].Claim.SessionID != session.ID {
|
||||
t.Fatalf("authorized claim: %#v err=%v", claimed, err)
|
||||
}
|
||||
if _, err := svc.AckGameClientBridgeCommand(domain.GameClientBridgeAckRequest{SessionToken: token, CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken}); err != nil {
|
||||
t.Fatalf("authorized ack: %v", err)
|
||||
}
|
||||
if _, err := svc.CompleteGameClientBridgeCommand(domain.GameClientBridgeResultRequest{SessionToken: token, CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded, Summary: "delivered"}); err != nil {
|
||||
t.Fatalf("authorized result: %v", err)
|
||||
}
|
||||
|
||||
snapshotRequest := domain.GameClientBridgeSnapshotIngestRequest{SessionToken: token, Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: *clock, Payload: map[string]any{"players": []any{}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}
|
||||
snapshot, err := svc.UploadGameClientBridgeSnapshot(snapshotRequest)
|
||||
if err != nil || snapshot.SourceSessionID != session.ID || snapshot.Sequence != 1 {
|
||||
t.Fatalf("authorized snapshot: %#v err=%v", snapshot, err)
|
||||
}
|
||||
if snapshot.SourceSessionID == token {
|
||||
t.Fatal("raw component token persisted in snapshot")
|
||||
}
|
||||
higherSnapshotRequest := snapshotRequest
|
||||
higherSnapshotRequest.Sequence = 2
|
||||
higherSnapshot, err := svc.UploadGameClientBridgeSnapshot(higherSnapshotRequest)
|
||||
if err != nil || higherSnapshot.Sequence != 2 {
|
||||
t.Fatalf("higher snapshot sequence was not accepted: %#v err=%v", higherSnapshot, err)
|
||||
}
|
||||
equalSnapshotRequest := higherSnapshotRequest
|
||||
if _, err := svc.UploadGameClientBridgeSnapshot(equalSnapshotRequest); err == nil {
|
||||
t.Fatal("expected latest snapshot sequence to be rejected")
|
||||
}
|
||||
if _, err := svc.UploadGameClientBridgeSnapshot(snapshotRequest); err == nil {
|
||||
t.Fatal("expected lower snapshot sequence rejection")
|
||||
}
|
||||
currentSnapshots, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current"})
|
||||
if err != nil || len(currentSnapshots) != 2 {
|
||||
t.Fatalf("stale snapshot attempts changed current stream records: %#v err=%v", currentSnapshots, err)
|
||||
}
|
||||
isolatedStreamRequest := snapshotRequest
|
||||
isolatedStreamRequest.StreamKey = "secondary"
|
||||
if snapshot, err := svc.UploadGameClientBridgeSnapshot(isolatedStreamRequest); err != nil || snapshot.Sequence != 1 {
|
||||
t.Fatalf("independent snapshot stream did not start at sequence one: %#v err=%v", snapshot, err)
|
||||
}
|
||||
stream, err := svc.store.GameClientBridgeSnapshotStreams().Get(gameClientBridgeStreamID("server-1", "game.scum", "scum-client", "players", "current"))
|
||||
if err != nil || stream.LatestSequence != 2 {
|
||||
t.Fatalf("snapshot stream projection: %#v err=%v", stream, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameClientBridgeClaimCannotBeCompletedByAnotherCurrentSession(t *testing.T) {
|
||||
svc, clock := newGameClientBridgeService(t)
|
||||
const firstToken = "component-session-token-one"
|
||||
_, firstSession := seedGameClientBridgeComponentSession(t, svc, *clock, firstToken)
|
||||
command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "session-owner-1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: firstToken, Limit: 1})
|
||||
if err != nil || len(claimed) != 1 {
|
||||
t.Fatalf("first session claim: %#v err=%v", claimed, err)
|
||||
}
|
||||
|
||||
const secondToken = "component-session-token-two"
|
||||
secondSession := firstSession
|
||||
secondSession.ID = "component-session-2"
|
||||
secondSession.TokenHash = tokenHash(secondToken)
|
||||
if err := svc.store.ClientManagerSessions().Create(secondSession); err != nil {
|
||||
t.Fatalf("create second current session: %v", err)
|
||||
}
|
||||
request := domain.GameClientBridgeAckRequest{SessionToken: secondToken, CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken}
|
||||
if _, err := svc.AckGameClientBridgeCommand(request); err == nil {
|
||||
t.Fatal("expected second session ack to be rejected")
|
||||
}
|
||||
if _, err := svc.CompleteGameClientBridgeCommand(domain.GameClientBridgeResultRequest{SessionToken: secondToken, CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded}); err == nil {
|
||||
t.Fatal("expected second session result to be rejected")
|
||||
}
|
||||
if _, err := svc.AckGameClientBridgeCommand(domain.GameClientBridgeAckRequest{SessionToken: firstToken, CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken}); err != nil {
|
||||
t.Fatalf("claim owner could not ack after rejected second session: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameClientBridgeComponentSessionRejectsMissingCapabilityAndStaleFences(t *testing.T) {
|
||||
svc, clock := newGameClientBridgeService(t)
|
||||
const token = "component-session-token"
|
||||
installation, session := seedGameClientBridgeComponentSession(t, svc, *clock, token)
|
||||
if _, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "authz-1")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
session.Capabilities = []string{"component.heartbeat"}
|
||||
if err := svc.store.ClientManagerSessions().Update(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token}); !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("expected missing capability rejection, got %v", err)
|
||||
}
|
||||
session.Capabilities = []string{"component.heartbeat", gameClientBridgeCapability}
|
||||
if err := svc.store.ClientManagerSessions().Update(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
installation.DeploymentGeneration++
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token}); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("expected deployment fence rejection, got %v", err)
|
||||
}
|
||||
installation.DeploymentGeneration = session.DeploymentGeneration
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
installation.ActiveArtifactID = "artifact-2"
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token}); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("expected active artifact fence rejection, got %v", err)
|
||||
}
|
||||
installation.ActiveArtifactID = session.ArtifactID
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
installation.KeyGeneration++
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token}); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("expected installation key-generation fence rejection, got %v", err)
|
||||
}
|
||||
installation.KeyGeneration = session.KeyGeneration
|
||||
if err := svc.store.ClientManagerInstallations().Update(installation); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, err := svc.store.EncryptedComponentKeys().Get("key-1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key.Generation++
|
||||
if err := svc.store.EncryptedComponentKeys().Update(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.ClaimGameClientBridgeCommands(domain.GameClientBridgeClaimRequest{SessionToken: token}); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("expected active component key-generation rejection, got %v", err)
|
||||
}
|
||||
key.Generation = session.KeyGeneration
|
||||
if err := svc.store.EncryptedComponentKeys().Update(key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
session.ExpiresAt = *clock
|
||||
if err := svc.store.ClientManagerSessions().Update(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.UploadGameClientBridgeSnapshot(domain.GameClientBridgeSnapshotIngestRequest{SessionToken: token, Type: "health", SchemaVersion: "1", StreamKey: "current", Sequence: 1, ObservedAt: *clock, Payload: map[string]any{"healthy": true}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 60}}); !errors.Is(err, ErrUnauthorized) {
|
||||
t.Fatalf("expected expired session rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user