功能修改

This commit is contained in:
npc0-hue
2026-07-20 16:42:33 +08:00
parent 48b8ad8d6c
commit a0e69417db
224 changed files with 22015 additions and 884 deletions
@@ -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)
}
}