feat: backend service
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
backendpg "galaxy/backend/internal/postgres"
|
||||
"galaxy/backend/internal/user"
|
||||
pgshared "galaxy/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
testcontainers "github.com/testcontainers/testcontainers-go"
|
||||
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
const (
|
||||
testImage = "postgres:16-alpine"
|
||||
testUser = "galaxy"
|
||||
testPassword = "galaxy"
|
||||
testDatabase = "galaxy_backend"
|
||||
testSchema = "backend"
|
||||
testStartup = 90 * time.Second
|
||||
testOpTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// startPostgres spins up a Postgres testcontainer with the backend schema
|
||||
// migrated up. The returned db is closed and the container terminated by
|
||||
// t.Cleanup hooks; tests should not close them explicitly.
|
||||
func startPostgres(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
pgContainer, err := tcpostgres.Run(ctx, testImage,
|
||||
tcpostgres.WithDatabase(testDatabase),
|
||||
tcpostgres.WithUsername(testUser),
|
||||
tcpostgres.WithPassword(testPassword),
|
||||
testcontainers.WithWaitStrategy(
|
||||
wait.ForLog("database system is ready to accept connections").
|
||||
WithOccurrence(2).
|
||||
WithStartupTimeout(testStartup),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
t.Skipf("postgres testcontainer unavailable, skipping: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if termErr := testcontainers.TerminateContainer(pgContainer); termErr != nil {
|
||||
t.Errorf("terminate postgres container: %v", termErr)
|
||||
}
|
||||
})
|
||||
|
||||
baseDSN, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
t.Fatalf("connection string: %v", err)
|
||||
}
|
||||
scopedDSN, err := dsnWithSearchPath(baseDSN, testSchema)
|
||||
if err != nil {
|
||||
t.Fatalf("scope dsn: %v", err)
|
||||
}
|
||||
|
||||
cfg := pgshared.DefaultConfig()
|
||||
cfg.PrimaryDSN = scopedDSN
|
||||
cfg.OperationTimeout = testOpTimeout
|
||||
|
||||
db, err := pgshared.OpenPrimary(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("open primary: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Errorf("close db: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if err := pgshared.Ping(ctx, db, cfg.OperationTimeout); err != nil {
|
||||
t.Fatalf("ping: %v", err)
|
||||
}
|
||||
if err := backendpg.ApplyMigrations(ctx, db); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func dsnWithSearchPath(baseDSN, schema string) (string, error) {
|
||||
parsed, err := url.Parse(baseDSN)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
values := parsed.Query()
|
||||
values.Set("search_path", schema)
|
||||
if values.Get("sslmode") == "" {
|
||||
values.Set("sslmode", "disable")
|
||||
}
|
||||
parsed.RawQuery = values.Encode()
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
// newServiceForTest builds a *user.Service with a real Postgres pool
|
||||
// and an empty cache. The cascade dependencies are left nil — the
|
||||
// tests in this file exercise EnsureByEmail and the lookup paths that
|
||||
// do not need them. Tests that drive sanctions/limits/soft-delete
|
||||
// build their own Deps inline.
|
||||
func newServiceForTest(db *sql.DB, now func() time.Time) *user.Service {
|
||||
return user.NewService(user.Deps{
|
||||
|
||||
Store: user.NewStore(db),
|
||||
Cache: user.NewCache(),
|
||||
UserNameMaxRetries: 10,
|
||||
Now: now,
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnsureByEmailCreatesNewAccount(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
ctx := context.Background()
|
||||
uid, err := svc.EnsureByEmail(ctx, "Pilot@Example.Test", "ru", "Europe/Kaliningrad", "RU")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
if uid == uuid.Nil {
|
||||
t.Fatalf("EnsureByEmail returned uuid.Nil")
|
||||
}
|
||||
|
||||
var (
|
||||
gotEmail string
|
||||
gotName string
|
||||
gotLang string
|
||||
gotTZ string
|
||||
gotCountry *string
|
||||
gotDeleted *time.Time
|
||||
permanent bool
|
||||
)
|
||||
err = db.QueryRowContext(ctx, `
|
||||
SELECT email, user_name, preferred_language, time_zone, declared_country, deleted_at, permanent_block
|
||||
FROM backend.accounts WHERE user_id = $1
|
||||
`, uid).Scan(&gotEmail, &gotName, &gotLang, &gotTZ, &gotCountry, &gotDeleted, &permanent)
|
||||
if err != nil {
|
||||
t.Fatalf("post-insert SELECT: %v", err)
|
||||
}
|
||||
if gotEmail != "pilot@example.test" {
|
||||
t.Fatalf("email = %q, want lower-cased %q", gotEmail, "pilot@example.test")
|
||||
}
|
||||
if !strings.HasPrefix(gotName, "Player-") || len(gotName) != len("Player-")+8 {
|
||||
t.Fatalf("user_name = %q, want Player-XXXXXXXX (8 chars)", gotName)
|
||||
}
|
||||
if gotLang != "ru" {
|
||||
t.Fatalf("preferred_language = %q, want %q", gotLang, "ru")
|
||||
}
|
||||
if gotTZ != "Europe/Kaliningrad" {
|
||||
t.Fatalf("time_zone = %q, want %q", gotTZ, "Europe/Kaliningrad")
|
||||
}
|
||||
if gotCountry == nil || *gotCountry != "RU" {
|
||||
t.Fatalf("declared_country = %v, want %q", gotCountry, "RU")
|
||||
}
|
||||
if gotDeleted != nil {
|
||||
t.Fatalf("deleted_at = %v, want NULL", gotDeleted)
|
||||
}
|
||||
if permanent {
|
||||
t.Fatalf("permanent_block = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureByEmailIdempotentOnSecondCall(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
ctx := context.Background()
|
||||
first, err := svc.EnsureByEmail(ctx, "alice@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("first EnsureByEmail: %v", err)
|
||||
}
|
||||
|
||||
second, err := svc.EnsureByEmail(ctx, "alice@example.test", "ru", "Asia/Tokyo", "JP")
|
||||
if err != nil {
|
||||
t.Fatalf("second EnsureByEmail: %v", err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatalf("user_id changed on second call: first=%s second=%s", first, second)
|
||||
}
|
||||
|
||||
// The second call's "would-be" values must be ignored — the row keeps
|
||||
// the values from the first call.
|
||||
var lang, tz string
|
||||
var country *string
|
||||
err = db.QueryRowContext(ctx, `
|
||||
SELECT preferred_language, time_zone, declared_country
|
||||
FROM backend.accounts WHERE user_id = $1
|
||||
`, first).Scan(&lang, &tz, &country)
|
||||
if err != nil {
|
||||
t.Fatalf("post-second SELECT: %v", err)
|
||||
}
|
||||
if lang != "en" || tz != "UTC" || country != nil {
|
||||
t.Fatalf("existing account mutated: lang=%q tz=%q country=%v",
|
||||
lang, tz, country)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureByEmailEmptyDeclaredCountryWritesNull(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(),
|
||||
"bob@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
|
||||
var country *string
|
||||
err = db.QueryRowContext(context.Background(),
|
||||
`SELECT declared_country FROM backend.accounts WHERE user_id = $1`,
|
||||
uid).Scan(&country)
|
||||
if err != nil {
|
||||
t.Fatalf("SELECT: %v", err)
|
||||
}
|
||||
if country != nil {
|
||||
t.Fatalf("declared_country = %q, want NULL", *country)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureByEmailRejectsEmpty(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
if _, err := svc.EnsureByEmail(context.Background(),
|
||||
" ", "en", "UTC", ""); err == nil {
|
||||
t.Fatalf("expected error for blank email")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureByEmailInstallsDefaultEntitlement(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "kira@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
account, err := svc.GetAccount(context.Background(), uid)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccount: %v", err)
|
||||
}
|
||||
if account.Entitlement.Tier != user.TierFree {
|
||||
t.Fatalf("default tier = %q, want %q", account.Entitlement.Tier, user.TierFree)
|
||||
}
|
||||
if account.Entitlement.IsPaid {
|
||||
t.Fatalf("default is_paid = true, want false")
|
||||
}
|
||||
if account.Entitlement.MaxRegisteredRaceNames != 1 {
|
||||
t.Fatalf("default max_registered_race_names = %d, want 1", account.Entitlement.MaxRegisteredRaceNames)
|
||||
}
|
||||
if account.Entitlement.Source != "system" {
|
||||
t.Fatalf("default source = %q, want \"system\"", account.Entitlement.Source)
|
||||
}
|
||||
if account.Entitlement.Actor.Type != "system" {
|
||||
t.Fatalf("default actor.type = %q, want \"system\"", account.Entitlement.Actor.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccountReturnsErrAccountNotFoundForMissing(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
if _, err := svc.GetAccount(context.Background(), uuid.New()); err == nil {
|
||||
t.Fatalf("expected error for missing user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveByEmailFindsLiveAccount(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "carol@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
resolved, err := svc.ResolveByEmail(context.Background(), "Carol@Example.Test")
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveByEmail: %v", err)
|
||||
}
|
||||
if resolved != uid {
|
||||
t.Fatalf("ResolveByEmail = %s, want %s", resolved, uid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveByEmailReturnsErrAccountNotFoundForMissing(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
if _, err := svc.ResolveByEmail(context.Background(), "ghost@example.test"); err == nil {
|
||||
t.Fatalf("expected error for missing email")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProfileWritesDisplayName(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "dan@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
displayName := "Daniel"
|
||||
account, err := svc.UpdateProfile(context.Background(), uid, user.UpdateProfileInput{DisplayName: &displayName})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateProfile: %v", err)
|
||||
}
|
||||
if account.DisplayName != "Daniel" {
|
||||
t.Fatalf("display_name = %q, want %q", account.DisplayName, "Daniel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSettingsRejectsInvalidTimezone(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "eve@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
bogus := "Mars/Olympus"
|
||||
_, err = svc.UpdateSettings(context.Background(), uid, user.UpdateSettingsInput{TimeZone: &bogus})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid time_zone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEntitlementMonthlyComputesEndsAt(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
frozen := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC)
|
||||
svc := newServiceForTest(db, func() time.Time { return frozen })
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "fox@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
account, err := svc.ApplyEntitlement(context.Background(), user.ApplyEntitlementInput{
|
||||
UserID: uid,
|
||||
Tier: user.TierMonthly,
|
||||
Source: "admin",
|
||||
Actor: user.ActorRef{Type: "admin", ID: "operator"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyEntitlement: %v", err)
|
||||
}
|
||||
if account.Entitlement.Tier != user.TierMonthly {
|
||||
t.Fatalf("tier = %q, want %q", account.Entitlement.Tier, user.TierMonthly)
|
||||
}
|
||||
if !account.Entitlement.IsPaid {
|
||||
t.Fatalf("monthly tier returned is_paid=false")
|
||||
}
|
||||
if account.Entitlement.EndsAt == nil {
|
||||
t.Fatalf("monthly tier returned ends_at = nil")
|
||||
}
|
||||
got := account.Entitlement.EndsAt.UTC()
|
||||
want := frozen.Add(30 * 24 * time.Hour)
|
||||
if !got.Equal(want) {
|
||||
t.Fatalf("ends_at = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyEntitlementRejectsUnknownTier(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "gail@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
_, err = svc.ApplyEntitlement(context.Background(), user.ApplyEntitlementInput{
|
||||
UserID: uid,
|
||||
Tier: "platinum",
|
||||
Source: "admin",
|
||||
Actor: user.ActorRef{Type: "admin", ID: "operator"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected ErrInvalidTier")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplySanctionPermanentBlockFlipsFlagAndCallsRevoker(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
revoker := &recordingRevoker{}
|
||||
lobby := &recordingLobbyCascade{}
|
||||
svc := user.NewService(user.Deps{
|
||||
|
||||
Store: user.NewStore(db),
|
||||
Cache: user.NewCache(),
|
||||
Lobby: lobby,
|
||||
SessionRevoker: revoker,
|
||||
UserNameMaxRetries: 10,
|
||||
Now: time.Now,
|
||||
})
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "han@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
if _, err := svc.ApplySanction(context.Background(), user.ApplySanctionInput{
|
||||
UserID: uid,
|
||||
SanctionCode: user.SanctionCodePermanentBlock,
|
||||
Scope: "platform",
|
||||
ReasonCode: "tos_violation",
|
||||
Actor: user.ActorRef{Type: "admin", ID: "operator"},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplySanction: %v", err)
|
||||
}
|
||||
if revoker.calls != 1 || revoker.lastUser != uid {
|
||||
t.Fatalf("revoker calls=%d lastUser=%s, want 1 / %s", revoker.calls, revoker.lastUser, uid)
|
||||
}
|
||||
if lobby.blockedCalls != 1 || lobby.lastBlockedUser != uid {
|
||||
t.Fatalf("lobby blocked calls=%d lastUser=%s, want 1 / %s", lobby.blockedCalls, lobby.lastBlockedUser, uid)
|
||||
}
|
||||
var permanent bool
|
||||
if err := db.QueryRowContext(context.Background(),
|
||||
`SELECT permanent_block FROM backend.accounts WHERE user_id = $1`, uid).Scan(&permanent); err != nil {
|
||||
t.Fatalf("SELECT permanent_block: %v", err)
|
||||
}
|
||||
if !permanent {
|
||||
t.Fatalf("permanent_block = false after permanent_block sanction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLimitWritesActiveRow(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
svc := newServiceForTest(db, time.Now)
|
||||
|
||||
uid, err := svc.EnsureByEmail(context.Background(), "iris@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail: %v", err)
|
||||
}
|
||||
if _, err := svc.ApplyLimit(context.Background(), user.ApplyLimitInput{
|
||||
UserID: uid,
|
||||
LimitCode: "max_active_games",
|
||||
Value: 3,
|
||||
ReasonCode: "manual_review",
|
||||
Actor: user.ActorRef{Type: "admin", ID: "operator"},
|
||||
}); err != nil {
|
||||
t.Fatalf("ApplyLimit: %v", err)
|
||||
}
|
||||
var value int32
|
||||
if err := db.QueryRowContext(context.Background(),
|
||||
`SELECT value FROM backend.limit_active WHERE user_id = $1 AND limit_code = 'max_active_games'`, uid,
|
||||
).Scan(&value); err != nil {
|
||||
t.Fatalf("SELECT limit_active.value: %v", err)
|
||||
}
|
||||
if value != 3 {
|
||||
t.Fatalf("limit_active.value = %d, want 3", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAccountsExcludesSoftDeleted(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
revoker := &recordingRevoker{}
|
||||
svc := user.NewService(user.Deps{
|
||||
|
||||
Store: user.NewStore(db),
|
||||
Cache: user.NewCache(),
|
||||
Lobby: &recordingLobbyCascade{},
|
||||
Notification: &recordingNotificationCascade{},
|
||||
SessionRevoker: revoker,
|
||||
UserNameMaxRetries: 10,
|
||||
Now: time.Now,
|
||||
})
|
||||
|
||||
live, err := svc.EnsureByEmail(context.Background(), "live@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail live: %v", err)
|
||||
}
|
||||
gone, err := svc.EnsureByEmail(context.Background(), "gone@example.test", "en", "UTC", "")
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureByEmail gone: %v", err)
|
||||
}
|
||||
if err := svc.SoftDelete(context.Background(), gone, user.ActorRef{Type: "user", ID: gone.String()}); err != nil {
|
||||
t.Fatalf("SoftDelete: %v", err)
|
||||
}
|
||||
|
||||
page, err := svc.ListAccounts(context.Background(), 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListAccounts: %v", err)
|
||||
}
|
||||
for _, item := range page.Items {
|
||||
if item.UserID == gone {
|
||||
t.Fatalf("ListAccounts returned a soft-deleted account: %+v", item)
|
||||
}
|
||||
if item.DeletedAt != nil {
|
||||
t.Fatalf("ListAccounts returned an account with non-nil DeletedAt: %+v", item)
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, item := range page.Items {
|
||||
if item.UserID == live {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("ListAccounts did not include the live account")
|
||||
}
|
||||
}
|
||||
|
||||
// recordingRevoker is a SessionRevoker spy that captures every call
|
||||
// for assertion. It is shared across tests in this package.
|
||||
type recordingRevoker struct {
|
||||
calls int
|
||||
lastUser uuid.UUID
|
||||
}
|
||||
|
||||
func (r *recordingRevoker) RevokeAllForUser(_ context.Context, userID uuid.UUID) error {
|
||||
r.calls++
|
||||
r.lastUser = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordingLobbyCascade captures the OnUserDeleted / OnUserBlocked
|
||||
// calls so soft-delete and permanent-block tests can assert ordering
|
||||
// and frequency.
|
||||
type recordingLobbyCascade struct {
|
||||
deletedCalls int
|
||||
blockedCalls int
|
||||
lastDeletedUser uuid.UUID
|
||||
lastBlockedUser uuid.UUID
|
||||
}
|
||||
|
||||
func (c *recordingLobbyCascade) OnUserDeleted(_ context.Context, userID uuid.UUID) error {
|
||||
c.deletedCalls++
|
||||
c.lastDeletedUser = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *recordingLobbyCascade) OnUserBlocked(_ context.Context, userID uuid.UUID) error {
|
||||
c.blockedCalls++
|
||||
c.lastBlockedUser = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordingNotificationCascade captures OnUserDeleted invocations.
|
||||
type recordingNotificationCascade struct {
|
||||
calls int
|
||||
lastUser uuid.UUID
|
||||
}
|
||||
|
||||
func (c *recordingNotificationCascade) OnUserDeleted(_ context.Context, userID uuid.UUID) error {
|
||||
c.calls++
|
||||
c.lastUser = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordingGeoCascade captures OnUserDeleted invocations.
|
||||
type recordingGeoCascade struct {
|
||||
calls int
|
||||
lastUser uuid.UUID
|
||||
}
|
||||
|
||||
func (c *recordingGeoCascade) OnUserDeleted(_ context.Context, userID uuid.UUID) error {
|
||||
c.calls++
|
||||
c.lastUser = userID
|
||||
return nil
|
||||
}
|
||||
|
||||
// silence unused-import warnings on database/sql when tests only need
|
||||
// it through fixture helpers.
|
||||
var _ = sql.LevelDefault
|
||||
Reference in New Issue
Block a user