// Package userservicestub provides an in-process // ports.UserService implementation for service-level tests. The stub // stores per-user Eligibility values and lets tests inject errors for // specific user ids to exercise the unavailable / decode-failure paths. package userservicestub import ( "context" "errors" "fmt" "strings" "sync" "galaxy/lobby/internal/ports" ) // Service is a concurrency-safe in-memory implementation of // ports.UserService. The zero value is not usable; call NewService to // construct. type Service struct { mu sync.Mutex eligibilities map[string]ports.Eligibility failures map[string]error defaultMissing bool } // NewService constructs an empty Service with no preloaded // eligibilities. By default an unknown user maps to // Eligibility{Exists:false}, mirroring the production HTTP client's // 404 handling. Use WithDefaultUnavailable to flip the unknown-user // behaviour to a transport failure. func NewService(opts ...Option) *Service { service := &Service{ eligibilities: make(map[string]ports.Eligibility), failures: make(map[string]error), } for _, opt := range opts { opt(service) } return service } // Option tunes Service construction. type Option func(*Service) // WithDefaultUnavailable makes the stub return ErrUserServiceUnavailable // for any user id without a preloaded eligibility or failure entry. // Useful for tests that exercise the "User Service down" path without // having to enumerate every caller. func WithDefaultUnavailable() Option { return func(service *Service) { service.defaultMissing = true } } // SetEligibility preloads eligibility for userID. Subsequent calls // overwrite the prior value. func (service *Service) SetEligibility(userID string, eligibility ports.Eligibility) { if service == nil { return } service.mu.Lock() defer service.mu.Unlock() service.eligibilities[strings.TrimSpace(userID)] = eligibility } // SetFailure preloads err to be returned for userID. err takes // precedence over any preloaded eligibility. func (service *Service) SetFailure(userID string, err error) { if service == nil { return } service.mu.Lock() defer service.mu.Unlock() service.failures[strings.TrimSpace(userID)] = err } // GetEligibility returns the preloaded eligibility for userID. func (service *Service) GetEligibility(ctx context.Context, userID string) (ports.Eligibility, error) { if service == nil { return ports.Eligibility{}, errors.New("get eligibility: nil service") } if ctx == nil { return ports.Eligibility{}, errors.New("get eligibility: nil context") } trimmed := strings.TrimSpace(userID) if trimmed == "" { return ports.Eligibility{}, errors.New("get eligibility: user id must not be empty") } service.mu.Lock() defer service.mu.Unlock() if err, ok := service.failures[trimmed]; ok { return ports.Eligibility{}, err } if eligibility, ok := service.eligibilities[trimmed]; ok { return eligibility, nil } if service.defaultMissing { return ports.Eligibility{}, fmt.Errorf("get eligibility: %w", ports.ErrUserServiceUnavailable) } return ports.Eligibility{Exists: false}, nil } // Compile-time interface assertion. var _ ports.UserService = (*Service)(nil)