docs: reorder & testing

This commit is contained in:
Ilia Denisov
2026-05-07 00:58:53 +03:00
committed by GitHub
parent f446c6a2ac
commit 604fe40bcf
148 changed files with 9150 additions and 2757 deletions
+22 -6
View File
@@ -12,19 +12,35 @@ import (
// ActorRef identifies the principal that produced an audit-bearing
// mutation. The wire shape mirrors the OpenAPI ActorRef schema. Type is
// a free-form string ("user", "admin", "system" in MVP); ID is opaque
// (a user UUID, an admin username, or empty for system).
// one of "user", "admin", "system" in MVP. ID carries a user UUID for
// Type=="user", an admin username for Type=="admin", and is empty for
// Type=="system".
type ActorRef struct {
Type string
ID string
ID string
}
// Validate rejects empty actor types. Admin handlers always populate
// Type; user-side mutations supply Type internally.
// Validate rejects empty actor types and enforces the per-type shape
// of ID: a user actor requires a UUID id, a system actor must have an
// empty id. Other types pass through with no further check.
func (a ActorRef) Validate() error {
if strings.TrimSpace(a.Type) == "" {
t := strings.TrimSpace(a.Type)
if t == "" {
return ErrInvalidActor
}
switch t {
case "user":
if strings.TrimSpace(a.ID) == "" {
return fmt.Errorf("%w: user actor requires id", ErrInvalidActor)
}
if _, err := uuid.Parse(a.ID); err != nil {
return fmt.Errorf("%w: user actor id must be a uuid: %v", ErrInvalidActor, err)
}
case "system":
if strings.TrimSpace(a.ID) != "" {
return fmt.Errorf("%w: system actor must have an empty id", ErrInvalidActor)
}
}
return nil
}
+25 -1
View File
@@ -34,10 +34,34 @@ type GeoCascade interface {
// canonical implementation wraps `*auth.Service.RevokeAllForUser`. The
// adapter lives in `cmd/backend/main.go` so `auth` does not export an
// extra method shape.
//
// The actor argument carries audit context: who initiated the revoke
// and why. The auth side persists it into `session_revocations`; user
// callers populate it with a fixed kind matching the trigger.
type SessionRevoker interface {
RevokeAllForUser(ctx context.Context, userID uuid.UUID) error
RevokeAllForUser(ctx context.Context, userID uuid.UUID, actor SessionRevokeActor) error
}
// SessionRevokeActor describes the principal behind a session revoke.
// Kind is a closed vocabulary mirrored by `auth.ActorKind`; ID is the
// stable identifier of the principal (a user UUID for self-driven
// flows, an admin username for admin-driven flows). Reason is a
// free-form note recorded in the audit row.
type SessionRevokeActor struct {
Kind string
ID string
Reason string
}
// Closed Kind vocabulary. Mirror constants live in
// `auth.ActorKind*`; the values must stay in sync because the auth
// adapter forwards them verbatim.
const (
SessionRevokeActorSoftDeleteUser = "soft_delete_user"
SessionRevokeActorSoftDeleteAdmin = "soft_delete_admin"
SessionRevokeActorAdminSanction = "admin_sanction"
)
// NewNoopLobbyCascade returns a LobbyCascade that logs every invocation
// at info level and returns nil. The canonical lobby is wired in `cmd/backend/main.go`.
// implementation; until then the no-op keeps the cascade orchestration
+6 -7
View File
@@ -59,14 +59,13 @@ func (s *Service) ApplyLimit(ctx context.Context, input ApplyLimitInput) (Accoun
}
if err := s.deps.Store.ApplyLimitTx(ctx, limitInsert{
UserID: input.UserID,
LimitCode: input.LimitCode,
Value: input.Value,
UserID: input.UserID,
LimitCode: input.LimitCode,
Value: input.Value,
ReasonCode: input.ReasonCode,
ActorType: input.Actor.Type,
ActorID: input.Actor.ID,
AppliedAt: now,
ExpiresAt: expiresAt,
Actor: input.Actor,
AppliedAt: now,
ExpiresAt: expiresAt,
}); err != nil {
if errors.Is(err, ErrAccountNotFound) {
return Account{}, err
+15 -11
View File
@@ -77,14 +77,13 @@ func (s *Service) ApplySanction(ctx context.Context, input ApplySanctionInput) (
flipPermanent := input.SanctionCode == SanctionCodePermanentBlock
if err := s.deps.Store.ApplySanctionTx(ctx, sanctionInsert{
UserID: input.UserID,
SanctionCode: input.SanctionCode,
Scope: input.Scope,
ReasonCode: input.ReasonCode,
ActorType: input.Actor.Type,
ActorID: input.Actor.ID,
AppliedAt: now,
ExpiresAt: expiresAt,
UserID: input.UserID,
SanctionCode: input.SanctionCode,
Scope: input.Scope,
ReasonCode: input.ReasonCode,
Actor: input.Actor,
AppliedAt: now,
ExpiresAt: expiresAt,
FlipPermanent: flipPermanent,
}); err != nil {
if errors.Is(err, ErrAccountNotFound) {
@@ -94,7 +93,7 @@ func (s *Service) ApplySanction(ctx context.Context, input ApplySanctionInput) (
}
if flipPermanent {
if err := s.cascadePermanentBlock(ctx, input.UserID); err != nil {
if err := s.cascadePermanentBlock(ctx, input.UserID, input.Actor, input.ReasonCode); err != nil {
s.deps.Logger.Warn("permanent-block cascade returned error",
zap.String("user_id", input.UserID.String()),
zap.Error(err),
@@ -117,10 +116,15 @@ func validateSanctionCode(code string) error {
// lobby on-user-blocked hook. Both calls are best-effort — they run
// after the database commit and only join errors for the caller to
// log.
func (s *Service) cascadePermanentBlock(ctx context.Context, userID uuid.UUID) error {
func (s *Service) cascadePermanentBlock(ctx context.Context, userID uuid.UUID, actor ActorRef, reasonCode string) error {
var joined error
if s.deps.SessionRevoker != nil {
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID); err != nil {
revokeActor := SessionRevokeActor{
Kind: SessionRevokeActorAdminSanction,
ID: actor.ID,
Reason: SanctionCodePermanentBlock + ":" + reasonCode,
}
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID, revokeActor); err != nil {
joined = errors.Join(joined, fmt.Errorf("session revoke: %w", err))
}
}
+12 -3
View File
@@ -45,17 +45,26 @@ func (s *Service) SoftDelete(ctx context.Context, userID uuid.UUID, actor ActorR
zap.String("user_id", userID.String()),
zap.String("actor_type", actor.Type),
)
return s.runSoftDeleteCascade(ctx, userID)
return s.runSoftDeleteCascade(ctx, userID, actor)
}
// runSoftDeleteCascade fans the soft-delete signal out to dependent
// modules in the documented order: auth → lobby → notification → geo.
// Each call's error is joined; the loop continues even after a
// failure so the remaining modules still get notified.
func (s *Service) runSoftDeleteCascade(ctx context.Context, userID uuid.UUID) error {
func (s *Service) runSoftDeleteCascade(ctx context.Context, userID uuid.UUID, actor ActorRef) error {
var joined error
if s.deps.SessionRevoker != nil {
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID); err != nil {
kind := SessionRevokeActorSoftDeleteAdmin
if actor.Type == "user" {
kind = SessionRevokeActorSoftDeleteUser
}
revokeActor := SessionRevokeActor{
Kind: kind,
ID: actor.ID,
Reason: "soft delete",
}
if err := s.deps.SessionRevoker.RevokeAllForUser(ctx, userID, revokeActor); err != nil {
joined = errors.Join(joined, fmt.Errorf("session revoke: %w", err))
}
}
+7 -5
View File
@@ -119,15 +119,17 @@ func equalStrings(a, b []string) bool {
// 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)
name string
calls int
lastUser uuid.UUID
lastActor user.SessionRevokeActor
appendTo func(string)
}
func (r *orderTracker) RevokeAllForUser(_ context.Context, userID uuid.UUID) error {
func (r *orderTracker) RevokeAllForUser(_ context.Context, userID uuid.UUID, actor user.SessionRevokeActor) error {
r.calls++
r.lastUser = userID
r.lastActor = actor
if r.appendTo != nil && r.name != "" {
r.appendTo(r.name)
}
+108 -49
View File
@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"galaxy/backend/internal/postgres/jet/backend/model"
@@ -72,8 +73,7 @@ type sanctionInsert struct {
SanctionCode string
Scope string
ReasonCode string
ActorType string
ActorID string
Actor ActorRef
AppliedAt time.Time
ExpiresAt *time.Time
FlipPermanent bool
@@ -85,8 +85,7 @@ type limitInsert struct {
LimitCode string
Value int32
ReasonCode string
ActorType string
ActorID string
Actor ActorRef
AppliedAt time.Time
ExpiresAt *time.Time
}
@@ -113,7 +112,8 @@ func accountColumns() postgres.ColumnList {
func snapshotColumns() postgres.ColumnList {
s := table.EntitlementSnapshots
return postgres.ColumnList{
s.UserID, s.Tier, s.IsPaid, s.Source, s.ActorType, s.ActorID,
s.UserID, s.Tier, s.IsPaid, s.Source,
s.ActorType, s.ActorUserID, s.ActorUsername,
s.ReasonCode, s.StartsAt, s.EndsAt, s.MaxRegisteredRaceNames, s.UpdatedAt,
}
}
@@ -275,7 +275,7 @@ func (s *Store) ListActiveSanctions(ctx context.Context, userID uuid.UUID) ([]Ac
r := table.SanctionRecords
stmt := postgres.SELECT(
r.SanctionCode, r.Scope, r.ReasonCode,
r.ActorType, r.ActorID,
r.ActorType, r.ActorUserID, r.ActorUsername,
r.AppliedAt, r.ExpiresAt,
).
FROM(a.INNER_JOIN(r, r.RecordID.EQ(a.RecordID))).
@@ -292,7 +292,7 @@ func (s *Store) ListActiveSanctions(ctx context.Context, userID uuid.UUID) ([]Ac
SanctionCode: row.SanctionCode,
Scope: row.Scope,
ReasonCode: row.ReasonCode,
Actor: ActorRef{Type: row.ActorType, ID: derefString(row.ActorID)},
Actor: actorFromColumns(row.ActorType, row.ActorUserID, row.ActorUsername),
AppliedAt: row.AppliedAt,
}
if row.ExpiresAt != nil {
@@ -311,7 +311,7 @@ func (s *Store) ListActiveLimits(ctx context.Context, userID uuid.UUID) ([]Activ
r := table.LimitRecords
stmt := postgres.SELECT(
r.LimitCode, a.Value, r.ReasonCode,
r.ActorType, r.ActorID,
r.ActorType, r.ActorUserID, r.ActorUsername,
r.AppliedAt, r.ExpiresAt,
).
FROM(a.INNER_JOIN(r, r.RecordID.EQ(a.RecordID))).
@@ -331,7 +331,7 @@ func (s *Store) ListActiveLimits(ctx context.Context, userID uuid.UUID) ([]Activ
LimitCode: row.LimitRecords.LimitCode,
Value: row.LimitActive.Value,
ReasonCode: row.LimitRecords.ReasonCode,
Actor: ActorRef{Type: row.LimitRecords.ActorType, ID: derefString(row.LimitRecords.ActorID)},
Actor: actorFromColumns(row.LimitRecords.ActorType, row.LimitRecords.ActorUserID, row.LimitRecords.ActorUsername),
AppliedAt: row.LimitRecords.AppliedAt,
}
if row.LimitRecords.ExpiresAt != nil {
@@ -395,9 +395,12 @@ func (s *Store) ApplyEntitlementTx(ctx context.Context, snap EntitlementSnapshot
if err := s.assertAccountLive(ctx, snap.UserID); err != nil {
return EntitlementSnapshot{}, err
}
err := withTx(ctx, s.db, func(tx *sql.Tx) error {
actorUserID, actorUsername, err := actorToColumnArgs(snap.Actor)
if err != nil {
return EntitlementSnapshot{}, err
}
err = withTx(ctx, s.db, func(tx *sql.Tx) error {
recordID := uuid.New()
actorID := nullableString(snap.Actor.ID)
var endsAt any
if snap.EndsAt != nil {
endsAt = *snap.EndsAt
@@ -409,20 +412,21 @@ func (s *Store) ApplyEntitlementTx(ctx context.Context, snap EntitlementSnapshot
table.EntitlementRecords.IsPaid,
table.EntitlementRecords.Source,
table.EntitlementRecords.ActorType,
table.EntitlementRecords.ActorID,
table.EntitlementRecords.ActorUserID,
table.EntitlementRecords.ActorUsername,
table.EntitlementRecords.ReasonCode,
table.EntitlementRecords.StartsAt,
table.EntitlementRecords.EndsAt,
table.EntitlementRecords.CreatedAt,
).VALUES(
recordID, snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
snap.Actor.Type, actorID, snap.ReasonCode,
snap.Actor.Type, actorUserID, actorUsername, snap.ReasonCode,
snap.StartsAt, endsAt, snap.UpdatedAt,
)
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("insert entitlement record: %w", err)
}
return upsertSnapshotTx(ctx, tx, snap)
return upsertSnapshotTx(ctx, tx, snap, actorUserID, actorUsername)
})
if err != nil {
return EntitlementSnapshot{}, err
@@ -437,9 +441,12 @@ func (s *Store) ApplySanctionTx(ctx context.Context, input sanctionInsert) error
if err := s.assertAccountLive(ctx, input.UserID); err != nil {
return err
}
actorUserID, actorUsername, err := actorToColumnArgs(input.Actor)
if err != nil {
return err
}
return withTx(ctx, s.db, func(tx *sql.Tx) error {
recordID := uuid.New()
actorID := nullableString(input.ActorID)
var expiresAt any
if input.ExpiresAt != nil {
expiresAt = *input.ExpiresAt
@@ -451,12 +458,13 @@ func (s *Store) ApplySanctionTx(ctx context.Context, input sanctionInsert) error
table.SanctionRecords.Scope,
table.SanctionRecords.ReasonCode,
table.SanctionRecords.ActorType,
table.SanctionRecords.ActorID,
table.SanctionRecords.ActorUserID,
table.SanctionRecords.ActorUsername,
table.SanctionRecords.AppliedAt,
table.SanctionRecords.ExpiresAt,
).VALUES(
recordID, input.UserID, input.SanctionCode, input.Scope, input.ReasonCode,
input.ActorType, actorID, input.AppliedAt, expiresAt,
input.Actor.Type, actorUserID, actorUsername, input.AppliedAt, expiresAt,
)
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("insert sanction record: %w", err)
@@ -498,9 +506,12 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
if err := s.assertAccountLive(ctx, input.UserID); err != nil {
return err
}
actorUserID, actorUsername, err := actorToColumnArgs(input.Actor)
if err != nil {
return err
}
return withTx(ctx, s.db, func(tx *sql.Tx) error {
recordID := uuid.New()
actorID := nullableString(input.ActorID)
var expiresAt any
if input.ExpiresAt != nil {
expiresAt = *input.ExpiresAt
@@ -512,12 +523,13 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
table.LimitRecords.Value,
table.LimitRecords.ReasonCode,
table.LimitRecords.ActorType,
table.LimitRecords.ActorID,
table.LimitRecords.ActorUserID,
table.LimitRecords.ActorUsername,
table.LimitRecords.AppliedAt,
table.LimitRecords.ExpiresAt,
).VALUES(
recordID, input.UserID, input.LimitCode, input.Value, input.ReasonCode,
input.ActorType, actorID, input.AppliedAt, expiresAt,
input.Actor.Type, actorUserID, actorUsername, input.AppliedAt, expiresAt,
)
if _, err := recordStmt.ExecContext(ctx, tx); err != nil {
return fmt.Errorf("insert limit record: %w", err)
@@ -547,12 +559,16 @@ func (s *Store) ApplyLimitTx(ctx context.Context, input limitInsert) error {
// successful idempotent operation.
func (s *Store) SoftDeleteAccount(ctx context.Context, userID uuid.UUID, actor ActorRef, now time.Time) (bool, error) {
a := table.Accounts
actorIDExpr := nullableStringExpr(actor.ID)
actorUserIDExpr, actorUsernameExpr, err := actorToColumnExprs(actor)
if err != nil {
return false, err
}
stmt := a.UPDATE().
SET(
a.DeletedAt.SET(postgres.TimestampzT(now)),
a.DeletedActorType.SET(postgres.String(actor.Type)),
a.DeletedActorID.SET(actorIDExpr),
a.DeletedActorUserID.SET(actorUserIDExpr),
a.DeletedActorUsername.SET(actorUsernameExpr),
a.UpdatedAt.SET(postgres.TimestampzT(now)),
).
WHERE(
@@ -593,18 +609,23 @@ func (s *Store) assertAccountLive(ctx context.Context, userID uuid.UUID) error {
}
func insertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot) error {
actorUserID, actorUsername, err := actorToColumnArgs(snap.Actor)
if err != nil {
return err
}
es := table.EntitlementSnapshots
actorID := nullableString(snap.Actor.ID)
var endsAt any
if snap.EndsAt != nil {
endsAt = *snap.EndsAt
}
stmt := es.INSERT(
es.UserID, es.Tier, es.IsPaid, es.Source, es.ActorType, es.ActorID,
es.UserID, es.Tier, es.IsPaid, es.Source,
es.ActorType, es.ActorUserID, es.ActorUsername,
es.ReasonCode, es.StartsAt, es.EndsAt,
es.MaxRegisteredRaceNames, es.UpdatedAt,
).VALUES(
snap.UserID, snap.Tier, snap.IsPaid, snap.Source, snap.Actor.Type, actorID,
snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
snap.Actor.Type, actorUserID, actorUsername,
snap.ReasonCode, snap.StartsAt, endsAt, snap.MaxRegisteredRaceNames, snap.UpdatedAt,
)
if _, err := stmt.ExecContext(ctx, tx); err != nil {
@@ -613,19 +634,20 @@ func insertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot)
return nil
}
func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot) error {
func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot, actorUserID, actorUsername any) error {
es := table.EntitlementSnapshots
actorID := nullableString(snap.Actor.ID)
var endsAt any
if snap.EndsAt != nil {
endsAt = *snap.EndsAt
}
stmt := es.INSERT(
es.UserID, es.Tier, es.IsPaid, es.Source, es.ActorType, es.ActorID,
es.UserID, es.Tier, es.IsPaid, es.Source,
es.ActorType, es.ActorUserID, es.ActorUsername,
es.ReasonCode, es.StartsAt, es.EndsAt,
es.MaxRegisteredRaceNames, es.UpdatedAt,
).VALUES(
snap.UserID, snap.Tier, snap.IsPaid, snap.Source, snap.Actor.Type, actorID,
snap.UserID, snap.Tier, snap.IsPaid, snap.Source,
snap.Actor.Type, actorUserID, actorUsername,
snap.ReasonCode, snap.StartsAt, endsAt, snap.MaxRegisteredRaceNames, snap.UpdatedAt,
).
ON_CONFLICT(es.UserID).
@@ -634,7 +656,8 @@ func upsertSnapshotTx(ctx context.Context, tx *sql.Tx, snap EntitlementSnapshot)
es.IsPaid.SET(es.EXCLUDED.IsPaid),
es.Source.SET(es.EXCLUDED.Source),
es.ActorType.SET(es.EXCLUDED.ActorType),
es.ActorID.SET(es.EXCLUDED.ActorID),
es.ActorUserID.SET(es.EXCLUDED.ActorUserID),
es.ActorUsername.SET(es.EXCLUDED.ActorUsername),
es.ReasonCode.SET(es.EXCLUDED.ReasonCode),
es.StartsAt.SET(es.EXCLUDED.StartsAt),
es.EndsAt.SET(es.EXCLUDED.EndsAt),
@@ -680,7 +703,7 @@ func modelToSnapshot(row model.EntitlementSnapshots) EntitlementSnapshot {
Tier: row.Tier,
IsPaid: row.IsPaid,
Source: row.Source,
Actor: ActorRef{Type: row.ActorType, ID: derefString(row.ActorID)},
Actor: actorFromColumns(row.ActorType, row.ActorUserID, row.ActorUsername),
ReasonCode: row.ReasonCode,
StartsAt: row.StartsAt,
MaxRegisteredRaceNames: row.MaxRegisteredRaceNames,
@@ -693,31 +716,67 @@ func modelToSnapshot(row model.EntitlementSnapshots) EntitlementSnapshot {
return out
}
// nullableString converts a Go string to the `any` form expected by jet
// VALUES: an empty string becomes nil so the column receives NULL.
func nullableString(v string) any {
if v == "" {
return nil
// actorToColumnArgs converts an ActorRef into the (actor_user_id,
// actor_username) values for jet INSERT VALUES. A nil-typed `any` lands
// as SQL NULL through the database/sql driver. Type=="user" parses ID
// as a UUID; Type=="admin" stores ID verbatim as the username;
// everything else (system, unknown) writes both columns as NULL. An
// empty ID is allowed for "user" so synthetic system events that label
// themselves as "user" do not fail.
func actorToColumnArgs(actor ActorRef) (any, any, error) {
switch strings.TrimSpace(actor.Type) {
case "user":
id := strings.TrimSpace(actor.ID)
if id == "" {
return nil, nil, nil
}
uid, err := uuid.Parse(id)
if err != nil {
return nil, nil, fmt.Errorf("user store: actor id %q is not a uuid: %w", actor.ID, err)
}
return uid, nil, nil
case "admin":
if strings.TrimSpace(actor.ID) == "" {
return nil, nil, nil
}
return nil, actor.ID, nil
default:
return nil, nil, nil
}
return v
}
// nullableStringExpr returns a typed jet expression: the empty string
// produces NULL, otherwise a String literal. Used by UPDATE SET paths
// where jet's SET wants a typed Expression rather than `any`.
func nullableStringExpr(v string) postgres.StringExpression {
if v == "" {
return postgres.StringExp(postgres.NULL)
// actorToColumnExprs is the typed-expression analogue of
// actorToColumnArgs for the UPDATE SET sites. jet's generated bindings
// type uuid columns as ColumnString (the dialect emits an explicit
// CAST), so both returned expressions are StringExpression.
func actorToColumnExprs(actor ActorRef) (postgres.StringExpression, postgres.StringExpression, error) {
uidArg, nameArg, err := actorToColumnArgs(actor)
if err != nil {
return nil, nil, err
}
return postgres.String(v)
uidExpr := postgres.StringExp(postgres.NULL)
if uid, ok := uidArg.(uuid.UUID); ok {
uidExpr = postgres.UUID(uid)
}
nameExpr := postgres.StringExp(postgres.NULL)
if name, ok := nameArg.(string); ok {
nameExpr = postgres.String(name)
}
return uidExpr, nameExpr, nil
}
// derefString returns the empty string when p is nil, otherwise *p.
func derefString(p *string) string {
if p == nil {
return ""
// actorFromColumns reconstructs an ActorRef from the (actor_type,
// actor_user_id, actor_username) triple read from an audit row. The
// non-nil column wins; both nil yields an empty ID.
func actorFromColumns(actorType string, userID *uuid.UUID, username *string) ActorRef {
out := ActorRef{Type: actorType}
switch {
case userID != nil:
out.ID = userID.String()
case username != nil:
out.ID = *username
}
return *p
return out
}
// rowsAffectedOrNotFound returns ErrAccountNotFound when the UPDATE
+6 -4
View File
@@ -68,7 +68,7 @@ func startPostgres(t *testing.T) *sql.DB {
cfg.PrimaryDSN = scopedDSN
cfg.OperationTimeout = testOpTimeout
db, err := pgshared.OpenPrimary(ctx, cfg)
db, err := pgshared.OpenPrimary(ctx, cfg, backendpg.NoObservabilityOptions()...)
if err != nil {
t.Fatalf("open primary: %v", err)
}
@@ -508,13 +508,15 @@ func TestListAccountsExcludesSoftDeleted(t *testing.T) {
// 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
calls int
lastUser uuid.UUID
lastActor user.SessionRevokeActor
}
func (r *recordingRevoker) RevokeAllForUser(_ context.Context, userID uuid.UUID) error {
func (r *recordingRevoker) RevokeAllForUser(_ context.Context, userID uuid.UUID, actor user.SessionRevokeActor) error {
r.calls++
r.lastUser = userID
r.lastActor = actor
return nil
}