feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+193
View File
@@ -0,0 +1,193 @@
package user_test
import (
"context"
"errors"
"testing"
"time"
"galaxy/backend/internal/user"
"github.com/google/uuid"
)
// TestSoftDeleteCascadeRunsAuthLobbyNotificationGeoInOrder verifies the
// documented cascade order. The test uses recording stubs for every
// hook and asserts that each one received the soft-delete signal
// exactly once for the right user_id.
func TestSoftDeleteCascadeRunsAuthLobbyNotificationGeoInOrder(t *testing.T) {
db := startPostgres(t)
revoker := &orderTracker{name: "auth"}
lobby := &orderingLobbyCascade{name: "lobby"}
notif := &orderingNotificationCascade{name: "notification"}
geo := &orderingGeoCascade{name: "geo"}
var order []string
revoker.appendTo = func(s string) { order = append(order, s) }
lobby.appendTo = func(s string) { order = append(order, s) }
notif.appendTo = func(s string) { order = append(order, s) }
geo.appendTo = func(s string) { order = append(order, s) }
svc := user.NewService(user.Deps{
Store: user.NewStore(db),
Cache: user.NewCache(),
Lobby: lobby,
Notification: notif,
Geo: geo,
SessionRevoker: revoker,
UserNameMaxRetries: 10,
Now: time.Now,
})
uid, err := svc.EnsureByEmail(context.Background(), "leo@example.test", "en", "UTC", "")
if err != nil {
t.Fatalf("EnsureByEmail: %v", err)
}
if err := svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()}); err != nil {
t.Fatalf("SoftDelete: %v", err)
}
want := []string{"auth", "lobby", "notification", "geo"}
if !equalStrings(order, want) {
t.Fatalf("cascade order = %v, want %v", order, want)
}
// Second call is a no-op — cascade must not fire again.
if err := svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()}); err != nil {
t.Fatalf("idempotent SoftDelete: %v", err)
}
if !equalStrings(order, want) {
t.Fatalf("idempotent SoftDelete fired cascade again: %v", order)
}
}
// TestSoftDeleteCascadeErrorDoesNotRollback covers the contract that
// cascade failures are surfaced to the caller but do not undo the
// `accounts.deleted_at` write.
func TestSoftDeleteCascadeErrorDoesNotRollback(t *testing.T) {
db := startPostgres(t)
failingNotif := &failingNotificationCascade{err: errors.New("notification down")}
svc := user.NewService(user.Deps{
Store: user.NewStore(db),
Cache: user.NewCache(),
Lobby: &orderingLobbyCascade{},
Notification: failingNotif,
Geo: &orderingGeoCascade{},
SessionRevoker: &orderTracker{},
UserNameMaxRetries: 10,
Now: time.Now,
})
uid, err := svc.EnsureByEmail(context.Background(), "mia@example.test", "en", "UTC", "")
if err != nil {
t.Fatalf("EnsureByEmail: %v", err)
}
err = svc.SoftDelete(context.Background(), uid, user.ActorRef{Type: "user", ID: uid.String()})
if err == nil {
t.Fatalf("SoftDelete returned nil despite failing cascade")
}
if !errors.Is(err, failingNotif.err) {
t.Fatalf("SoftDelete error = %v, want join containing %v", err, failingNotif.err)
}
var deletedAt *time.Time
if scanErr := db.QueryRowContext(context.Background(),
`SELECT deleted_at FROM backend.accounts WHERE user_id = $1`, uid,
).Scan(&deletedAt); scanErr != nil {
t.Fatalf("SELECT deleted_at: %v", scanErr)
}
if deletedAt == nil {
t.Fatalf("deleted_at = NULL despite SoftDelete commit")
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// orderTracker spies on a single call kind and pushes its name into
// the ordered slice when invoked. It satisfies user.SessionRevoker.
type orderTracker struct {
name string
calls int
lastUser uuid.UUID
appendTo func(string)
}
func (r *orderTracker) RevokeAllForUser(_ context.Context, userID uuid.UUID) error {
r.calls++
r.lastUser = userID
if r.appendTo != nil && r.name != "" {
r.appendTo(r.name)
}
return nil
}
type orderingLobbyCascade struct {
name string
appendTo func(string)
deleted int
blocked int
}
func (c *orderingLobbyCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
c.deleted++
if c.appendTo != nil && c.name != "" {
c.appendTo(c.name)
}
return nil
}
func (c *orderingLobbyCascade) OnUserBlocked(_ context.Context, _ uuid.UUID) error {
c.blocked++
return nil
}
type orderingNotificationCascade struct {
name string
appendTo func(string)
calls int
}
func (c *orderingNotificationCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
c.calls++
if c.appendTo != nil && c.name != "" {
c.appendTo(c.name)
}
return nil
}
type orderingGeoCascade struct {
name string
appendTo func(string)
calls int
}
func (c *orderingGeoCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
c.calls++
if c.appendTo != nil && c.name != "" {
c.appendTo(c.name)
}
return nil
}
type failingNotificationCascade struct {
err error
calls int
}
func (c *failingNotificationCascade) OnUserDeleted(_ context.Context, _ uuid.UUID) error {
c.calls++
return c.err
}