0cae89cba2
Tests · Go / test (push) Successful in 1m59s
Stage 1 of the dev-as-prod-mirror rework. The auto-provisioned "Dev Sandbox" game and dummy users are removed so the dev contour starts empty like prod; the separate legacy-report loader stays as the test-data path. - delete backend/internal/devsandbox (package + tests) - drop the bootstrap call + DevSandboxConfig (struct, Config field, BACKEND_DEV_SANDBOX_* env, defaults, loader, validation) - strip BACKEND_DEV_SANDBOX_* from dev-deploy + local-dev compose and .env.example; the generic engine-recycle / prune-broken-engines logic stays (it serves real games) - update tooling docs (dev-deploy README + KNOWN-ISSUES, local-dev README + Makefile) and stale comments; DeleteGame and InsertMembershipDirect remain (exercised by lobby integration tests) No app behaviour change beyond not auto-creating the sandbox game.
96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
package lobby
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// InsertMembershipDirectInput is the parameter struct for
|
|
// Service.InsertMembershipDirect.
|
|
type InsertMembershipDirectInput struct {
|
|
GameID uuid.UUID
|
|
UserID uuid.UUID
|
|
RaceName string
|
|
}
|
|
|
|
// InsertMembershipDirect grants a membership to userID inside gameID
|
|
// bypassing the application/approval flow. It performs the same DB
|
|
// writes as ApproveApplication: the per-game race-name reservation
|
|
// row plus the membership row, and refreshes the in-memory caches.
|
|
//
|
|
// The method is intended for trusted boot-time provisioning and
|
|
// integration tests; it is not exposed through any HTTP handler. The
|
|
// caller must guarantee
|
|
// game.Status == GameStatusEnrollmentOpen — the function returns
|
|
// ErrConflict otherwise — and that the race-name policy and
|
|
// canonical-key invariants are honoured (the implementation reuses
|
|
// the lobby's own Policy and assertRaceNameAvailable so a duplicate
|
|
// or unsuitable name still fails).
|
|
//
|
|
// Idempotency: if a membership for (GameID, UserID) already exists
|
|
// the function returns the existing row without modifying state, so
|
|
// the helper is safe to call repeatedly.
|
|
func (s *Service) InsertMembershipDirect(ctx context.Context, in InsertMembershipDirectInput) (Membership, error) {
|
|
displayName, err := ValidateDisplayName(in.RaceName)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
game, err := s.GetGame(ctx, in.GameID)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
if game.Status != GameStatusEnrollmentOpen {
|
|
return Membership{}, fmt.Errorf("%w: game status is %q, want enrollment_open", ErrConflict, game.Status)
|
|
}
|
|
canonical, err := s.deps.Policy.Canonical(displayName)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
existing, err := s.deps.Store.ListMembershipsForGame(ctx, in.GameID)
|
|
if err != nil {
|
|
return Membership{}, err
|
|
}
|
|
for _, m := range existing {
|
|
if m.UserID == in.UserID && m.Status == MembershipStatusActive {
|
|
return m, nil
|
|
}
|
|
}
|
|
if err := s.assertRaceNameAvailable(ctx, canonical, in.UserID, in.GameID); err != nil {
|
|
return Membership{}, err
|
|
}
|
|
now := s.deps.Now().UTC()
|
|
if _, err := s.deps.Store.InsertRaceName(ctx, raceNameInsert{
|
|
Name: displayName,
|
|
Canonical: canonical,
|
|
Status: RaceNameStatusReservation,
|
|
OwnerUserID: in.UserID,
|
|
GameID: in.GameID,
|
|
ReservedAt: &now,
|
|
}); err != nil {
|
|
return Membership{}, err
|
|
}
|
|
membership, err := s.deps.Store.InsertMembership(ctx, membershipInsert{
|
|
MembershipID: uuid.New(),
|
|
GameID: in.GameID,
|
|
UserID: in.UserID,
|
|
RaceName: displayName,
|
|
CanonicalKey: canonical,
|
|
})
|
|
if err != nil {
|
|
_ = s.deps.Store.DeleteRaceName(ctx, canonical, in.GameID)
|
|
return Membership{}, err
|
|
}
|
|
s.deps.Cache.PutMembership(membership)
|
|
s.deps.Cache.PutRaceName(RaceNameEntry{
|
|
Name: displayName,
|
|
Canonical: canonical,
|
|
Status: RaceNameStatusReservation,
|
|
OwnerUserID: in.UserID,
|
|
GameID: in.GameID,
|
|
ReservedAt: &now,
|
|
})
|
|
return membership, nil
|
|
}
|