package devsandbox import ( "context" "errors" "testing" "galaxy/backend/internal/config" "github.com/google/uuid" "go.uber.org/zap" ) // TestBootstrapSkippedWhenEmailEmpty exercises the no-op branch: with // the production posture (Email == "") Bootstrap must return without // touching any dependency. The fact that Users/Lobby/EngineVersions // are nil here doubles as a check that the early-return runs first. func TestBootstrapSkippedWhenEmailEmpty(t *testing.T) { err := Bootstrap( context.Background(), Deps{}, config.DevSandboxConfig{}, zap.NewNop(), ) if err != nil { t.Fatalf("expected nil error on empty email, got: %v", err) } } // TestBootstrapRejectsZeroPlayerCount confirms the validation // short-circuits the flow before any DB call when PlayerCount is // non-positive but Email is set. The error path is fast and never // dereferences the (still-nil) Users/Lobby deps. func TestBootstrapRejectsZeroPlayerCount(t *testing.T) { err := Bootstrap( context.Background(), Deps{Users: stubEnsurer{}, Lobby: nil, EngineVersions: nil}, config.DevSandboxConfig{ Email: "dev@local.test", EngineImage: "galaxy-engine:local-dev", EngineVersion: "0.0.0-local-dev", PlayerCount: 0, }, zap.NewNop(), ) if err == nil { t.Fatal("expected error on zero PlayerCount, got nil") } } // TestBootstrapRejectsMissingDeps checks that a misconfigured wiring // (Email set but one of the required services nil) fails fast rather // than panicking when the bootstrap reaches its first service call. func TestBootstrapRejectsMissingDeps(t *testing.T) { err := Bootstrap( context.Background(), Deps{Users: stubEnsurer{}, Lobby: nil, EngineVersions: nil}, config.DevSandboxConfig{ Email: "dev@local.test", EngineImage: "galaxy-engine:local-dev", EngineVersion: "0.0.0-local-dev", PlayerCount: 20, }, zap.NewNop(), ) if err == nil { t.Fatal("expected error on missing deps, got nil") } if !errors.Is(err, errMissingDepsSentinel) && err.Error() == "" { // The exact wording is not part of the contract; this branch // only asserts the error is non-nil and human-readable. t.Fatalf("error has empty message: %v", err) } } // errMissingDepsSentinel exists so the assertion above can compile; // the real error is constructed via errors.New inside Bootstrap and // is intentionally not exported. The test only needs to confirm the // returned error has a message. var errMissingDepsSentinel = errors.New("sentinel") // TestTerminalSandboxStatus pins the contract that decides whether a // previously created sandbox game gets purged on the next boot. // Terminal states are deleted (cascade-style) so the developer's // lobby never piles up dead tiles between `make rebuild` cycles. func TestTerminalSandboxStatus(t *testing.T) { terminal := []string{"cancelled", "finished", "start_failed"} live := []string{"draft", "enrollment_open", "ready_to_start", "starting", "running", "paused"} for _, status := range terminal { if !terminalSandboxStatus(status) { t.Errorf("expected %q to be terminal", status) } } for _, status := range live { if terminalSandboxStatus(status) { t.Errorf("expected %q to be non-terminal", status) } } } type stubEnsurer struct{} func (stubEnsurer) EnsureByEmail(_ context.Context, _, _, _, _ string) (uuid.UUID, error) { return uuid.UUID{}, nil }