feat: game lobby service
This commit is contained in:
@@ -4,38 +4,11 @@ package account
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/user/internal/domain/common"
|
||||
)
|
||||
|
||||
// RaceNameCanonicalKey stores the policy-produced reservation key used to
|
||||
// enforce replaceable race-name uniqueness.
|
||||
type RaceNameCanonicalKey string
|
||||
|
||||
// String returns RaceNameCanonicalKey as its stored canonical string.
|
||||
func (key RaceNameCanonicalKey) String() string {
|
||||
return string(key)
|
||||
}
|
||||
|
||||
// IsZero reports whether RaceNameCanonicalKey does not contain a usable value.
|
||||
func (key RaceNameCanonicalKey) IsZero() bool {
|
||||
return strings.TrimSpace(string(key)) == ""
|
||||
}
|
||||
|
||||
// Validate reports whether RaceNameCanonicalKey is non-empty and trimmed.
|
||||
func (key RaceNameCanonicalKey) Validate() error {
|
||||
switch {
|
||||
case key.IsZero():
|
||||
return fmt.Errorf("race name canonical key must not be empty")
|
||||
case strings.TrimSpace(string(key)) != string(key):
|
||||
return fmt.Errorf("race name canonical key must not contain surrounding whitespace")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UserAccount stores the current editable account state of one regular user.
|
||||
type UserAccount struct {
|
||||
// UserID identifies the durable regular-user account.
|
||||
@@ -44,8 +17,11 @@ type UserAccount struct {
|
||||
// Email stores the normalized login/contact address of the account.
|
||||
Email common.Email
|
||||
|
||||
// RaceName stores the original-casing user-facing race name.
|
||||
RaceName common.RaceName
|
||||
// UserName stores the immutable auto-generated `player-<suffix>` handle.
|
||||
UserName common.UserName
|
||||
|
||||
// DisplayName stores the optional mutable free-text user-facing label.
|
||||
DisplayName common.DisplayName
|
||||
|
||||
// PreferredLanguage stores the current declared language tag.
|
||||
PreferredLanguage common.LanguageTag
|
||||
@@ -62,10 +38,23 @@ type UserAccount struct {
|
||||
|
||||
// UpdatedAt stores the last account mutation timestamp.
|
||||
UpdatedAt time.Time
|
||||
|
||||
// DeletedAt stores the soft-delete timestamp set by the `DeleteUser`
|
||||
// command. A nil value means the account is live. A non-nil value marks
|
||||
// the record as soft-deleted: external auth, self-service, admin-read,
|
||||
// and lobby-eligibility operations must reject subsequent access with
|
||||
// `subject_not_found`.
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether UserAccount satisfies the frozen Stage 02
|
||||
// structural invariants.
|
||||
// IsDeleted reports whether the account has been soft-deleted through the
|
||||
// `DeleteUser` command.
|
||||
func (record UserAccount) IsDeleted() bool {
|
||||
return record.DeletedAt != nil
|
||||
}
|
||||
|
||||
// Validate reports whether UserAccount satisfies the Stage 21 structural
|
||||
// invariants, including the Stage 22 soft-delete rules.
|
||||
func (record UserAccount) Validate() error {
|
||||
if err := record.UserID.Validate(); err != nil {
|
||||
return fmt.Errorf("user account user id: %w", err)
|
||||
@@ -73,8 +62,11 @@ func (record UserAccount) Validate() error {
|
||||
if err := record.Email.Validate(); err != nil {
|
||||
return fmt.Errorf("user account email: %w", err)
|
||||
}
|
||||
if err := record.RaceName.Validate(); err != nil {
|
||||
return fmt.Errorf("user account race name: %w", err)
|
||||
if err := record.UserName.Validate(); err != nil {
|
||||
return fmt.Errorf("user account user name: %w", err)
|
||||
}
|
||||
if err := record.DisplayName.Validate(); err != nil {
|
||||
return fmt.Errorf("user account display name: %w", err)
|
||||
}
|
||||
if err := record.PreferredLanguage.Validate(); err != nil {
|
||||
return fmt.Errorf("user account preferred language: %w", err)
|
||||
@@ -96,40 +88,13 @@ func (record UserAccount) Validate() error {
|
||||
if record.UpdatedAt.Before(record.CreatedAt) {
|
||||
return fmt.Errorf("user account updated at must not be before created at")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RaceNameReservation stores the current uniqueness reservation for one
|
||||
// canonicalized race-name key.
|
||||
type RaceNameReservation struct {
|
||||
// CanonicalKey stores the policy-produced uniqueness key.
|
||||
CanonicalKey RaceNameCanonicalKey
|
||||
|
||||
// UserID identifies the account that owns the reservation.
|
||||
UserID common.UserID
|
||||
|
||||
// RaceName stores the original-casing name linked to the reservation.
|
||||
RaceName common.RaceName
|
||||
|
||||
// ReservedAt stores when the reservation was acquired.
|
||||
ReservedAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether RaceNameReservation satisfies the frozen Stage 02
|
||||
// structural invariants.
|
||||
func (record RaceNameReservation) Validate() error {
|
||||
if err := record.CanonicalKey.Validate(); err != nil {
|
||||
return fmt.Errorf("race name reservation canonical key: %w", err)
|
||||
}
|
||||
if err := record.UserID.Validate(); err != nil {
|
||||
return fmt.Errorf("race name reservation user id: %w", err)
|
||||
}
|
||||
if err := record.RaceName.Validate(); err != nil {
|
||||
return fmt.Errorf("race name reservation race name: %w", err)
|
||||
}
|
||||
if err := common.ValidateTimestamp("race name reservation reserved at", record.ReservedAt); err != nil {
|
||||
return err
|
||||
if record.DeletedAt != nil {
|
||||
if err := common.ValidateTimestamp("user account deleted at", *record.DeletedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
if record.DeletedAt.Before(record.CreatedAt) {
|
||||
return fmt.Errorf("user account deleted at must not be before created at")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,11 +21,11 @@ func TestUserAccountValidate(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid without declared country",
|
||||
name: "valid without declared country or display name",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
RaceName: common.RaceName("Pilot Nova"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
@@ -33,11 +33,12 @@ func TestUserAccountValidate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid with declared country",
|
||||
name: "valid with declared country and display name",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
RaceName: common.RaceName("Pilot Nova"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
DisplayName: common.DisplayName("PilotNova"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
DeclaredCountry: common.CountryCode("DE"),
|
||||
@@ -45,12 +46,38 @@ func TestUserAccountValidate(t *testing.T) {
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing user name",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid display name",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
DisplayName: common.DisplayName("Pilot Nova"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "updated before created",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
RaceName: common.RaceName("Pilot Nova"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
@@ -58,53 +85,50 @@ func TestUserAccountValidate(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.record.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceNameReservationValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
record RaceNameReservation
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
record: RaceNameReservation{
|
||||
CanonicalKey: RaceNameCanonicalKey("pilot-nova"),
|
||||
UserID: common.UserID("user-123"),
|
||||
RaceName: common.RaceName("Pilot Nova"),
|
||||
ReservedAt: time.Unix(1_775_240_100, 0).UTC(),
|
||||
name: "valid soft-deleted after update",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
DeletedAt: timePtr(updatedAt),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty canonical key",
|
||||
record: RaceNameReservation{
|
||||
UserID: common.UserID("user-123"),
|
||||
RaceName: common.RaceName("Pilot Nova"),
|
||||
ReservedAt: time.Unix(1_775_240_100, 0).UTC(),
|
||||
name: "deleted at before created",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
DeletedAt: timePtr(createdAt.Add(-time.Second)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "deleted at zero",
|
||||
record: UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
DeletedAt: timePtr(time.Time{}),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -117,3 +141,28 @@ func TestRaceNameReservationValidate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAccountIsDeleted(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
createdAt := time.Unix(1_775_240_000, 0).UTC()
|
||||
record := UserAccount{
|
||||
UserID: common.UserID("user-123"),
|
||||
Email: common.Email("pilot@example.com"),
|
||||
UserName: common.UserName("player-abcdefgh"),
|
||||
PreferredLanguage: common.LanguageTag("en"),
|
||||
TimeZone: common.TimeZoneName("Europe/Berlin"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
}
|
||||
require.False(t, record.IsDeleted())
|
||||
|
||||
deleted := record
|
||||
deletedAt := createdAt.Add(time.Minute)
|
||||
deleted.DeletedAt = &deletedAt
|
||||
require.True(t, deleted.IsDeleted())
|
||||
}
|
||||
|
||||
func timePtr(value time.Time) *time.Time {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
"net/mail"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/util"
|
||||
)
|
||||
|
||||
const (
|
||||
maxRaceNameLength = 64
|
||||
maxUserNameLength = 64
|
||||
maxLanguageTagLength = 32
|
||||
maxTimeZoneNameLength = 128
|
||||
)
|
||||
@@ -64,29 +66,64 @@ func (email Email) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RaceName stores one original-casing race name selected for the user
|
||||
// account.
|
||||
type RaceName string
|
||||
// UserName stores one immutable auto-generated platform handle in
|
||||
// `player-<suffix>` form. It is unique platform-wide and never changes after
|
||||
// account creation.
|
||||
type UserName string
|
||||
|
||||
// String returns RaceName as its stored value.
|
||||
func (name RaceName) String() string {
|
||||
// String returns UserName as its stored value.
|
||||
func (name UserName) String() string {
|
||||
return string(name)
|
||||
}
|
||||
|
||||
// IsZero reports whether RaceName does not contain a usable value.
|
||||
func (name RaceName) IsZero() bool {
|
||||
// IsZero reports whether UserName does not contain a usable value.
|
||||
func (name UserName) IsZero() bool {
|
||||
return strings.TrimSpace(string(name)) == ""
|
||||
}
|
||||
|
||||
// Validate reports whether RaceName is non-empty, trimmed, and within the
|
||||
// frozen OpenAPI length bound.
|
||||
func (name RaceName) Validate() error {
|
||||
// Validate reports whether UserName is non-empty, trimmed, uses the frozen
|
||||
// `player-` prefix, and stays within the reserved length bound.
|
||||
func (name UserName) Validate() error {
|
||||
raw := string(name)
|
||||
if err := validateToken("race name", raw); err != nil {
|
||||
if err := validatePrefixedToken("user name", raw, "player-"); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(raw) > maxRaceNameLength {
|
||||
return fmt.Errorf("race name must be at most %d bytes", maxRaceNameLength)
|
||||
if len(raw) > maxUserNameLength {
|
||||
return fmt.Errorf("user name must be at most %d bytes", maxUserNameLength)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayName stores one optional free-text user-facing label. It may be
|
||||
// empty and is not required to be unique; validation delegates to
|
||||
// galaxy/util.ValidateTypeName when a value is present.
|
||||
type DisplayName string
|
||||
|
||||
// String returns DisplayName as its stored value.
|
||||
func (name DisplayName) String() string {
|
||||
return string(name)
|
||||
}
|
||||
|
||||
// IsZero reports whether DisplayName is empty after trimming surrounding
|
||||
// whitespace.
|
||||
func (name DisplayName) IsZero() bool {
|
||||
return strings.TrimSpace(string(name)) == ""
|
||||
}
|
||||
|
||||
// Validate reports whether DisplayName is either empty or a valid
|
||||
// util.ValidateTypeName value. Trimming is the caller's responsibility;
|
||||
// Validate rejects values that still contain surrounding whitespace.
|
||||
func (name DisplayName) Validate() error {
|
||||
raw := string(name)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(raw) != raw {
|
||||
return fmt.Errorf("display name must not contain surrounding whitespace")
|
||||
}
|
||||
if _, ok := util.ValidateTypeName(raw); !ok {
|
||||
return fmt.Errorf("display name %q is invalid", raw)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -65,17 +65,51 @@ func TestEmailValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceNameValidate(t *testing.T) {
|
||||
func TestUserNameValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value RaceName
|
||||
value UserName
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid", value: RaceName("Admiral Nova")},
|
||||
{name: "empty", value: RaceName(""), wantErr: true},
|
||||
{name: "too long", value: RaceName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmn"), wantErr: true},
|
||||
{name: "valid", value: UserName("player-abcd1234")},
|
||||
{name: "empty", value: UserName(""), wantErr: true},
|
||||
{name: "wrong prefix", value: UserName("user-abcdefgh"), wantErr: true},
|
||||
{name: "prefix only", value: UserName("player-"), wantErr: true},
|
||||
{name: "surrounding whitespace", value: UserName(" player-abcd1234 "), wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.value.Validate()
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayNameValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
value DisplayName
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "empty accepted", value: DisplayName("")},
|
||||
{name: "valid simple", value: DisplayName("PilotNova")},
|
||||
{name: "valid unicode", value: DisplayName("АдмиралНова")},
|
||||
{name: "internal whitespace", value: DisplayName("Pilot Nova"), wantErr: true},
|
||||
{name: "leading whitespace", value: DisplayName(" PilotNova"), wantErr: true},
|
||||
{name: "trailing whitespace", value: DisplayName("PilotNova "), wantErr: true},
|
||||
{name: "leading special", value: DisplayName("-Pilot"), wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -30,6 +30,14 @@ const (
|
||||
// SanctionCodeProfileUpdateBlock denies self-service profile/settings
|
||||
// mutations.
|
||||
SanctionCodeProfileUpdateBlock SanctionCode = "profile_update_block"
|
||||
|
||||
// SanctionCodePermanentBlock marks the account as permanently disabled.
|
||||
// It is a terminal sanction: every `can_*` eligibility marker collapses to
|
||||
// false while it is active, self-service reads and writes are rejected
|
||||
// with 409 conflict, and Game Lobby performs Race Name Directory cascade
|
||||
// release when it observes the corresponding `user:lifecycle_events`
|
||||
// event.
|
||||
SanctionCodePermanentBlock SanctionCode = "permanent_block"
|
||||
)
|
||||
|
||||
// IsKnown reports whether SanctionCode belongs to the frozen v1 catalog.
|
||||
@@ -39,7 +47,8 @@ func (code SanctionCode) IsKnown() bool {
|
||||
SanctionCodePrivateGameCreateBlock,
|
||||
SanctionCodePrivateGameManageBlock,
|
||||
SanctionCodeGameJoinBlock,
|
||||
SanctionCodeProfileUpdateBlock:
|
||||
SanctionCodeProfileUpdateBlock,
|
||||
SanctionCodePermanentBlock:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -63,6 +72,12 @@ const (
|
||||
// LimitCodeMaxActiveGameMemberships limits how many active public-game
|
||||
// memberships the user may hold at once.
|
||||
LimitCodeMaxActiveGameMemberships LimitCode = "max_active_game_memberships"
|
||||
|
||||
// LimitCodeMaxRegisteredRaceNames overrides the tariff default quota for
|
||||
// permanent race-name registrations in the Game Lobby Race Name Directory.
|
||||
// The value `0` denotes an unlimited quota and is the canonical marker used
|
||||
// by the `paid_lifetime` tariff default.
|
||||
LimitCodeMaxRegisteredRaceNames LimitCode = "max_registered_race_names"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,7 +106,8 @@ func (code LimitCode) IsSupported() bool {
|
||||
switch code {
|
||||
case LimitCodeMaxOwnedPrivateGames,
|
||||
LimitCodeMaxPendingPublicApplications,
|
||||
LimitCodeMaxActiveGameMemberships:
|
||||
LimitCodeMaxActiveGameMemberships,
|
||||
LimitCodeMaxRegisteredRaceNames:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -127,6 +127,50 @@ func TestActiveSanctionsAt(t *testing.T) {
|
||||
require.Equal(t, SanctionCodeProfileUpdateBlock, active[0].SanctionCode)
|
||||
}
|
||||
|
||||
func TestSanctionCodeCatalog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.True(t, SanctionCodeLoginBlock.IsKnown())
|
||||
require.True(t, SanctionCodePrivateGameCreateBlock.IsKnown())
|
||||
require.True(t, SanctionCodePrivateGameManageBlock.IsKnown())
|
||||
require.True(t, SanctionCodeGameJoinBlock.IsKnown())
|
||||
require.True(t, SanctionCodeProfileUpdateBlock.IsKnown())
|
||||
require.True(t, SanctionCodePermanentBlock.IsKnown())
|
||||
require.False(t, SanctionCode("unknown_code").IsKnown())
|
||||
}
|
||||
|
||||
func TestActiveSanctionsAtPermanentBlockCoexistsWithOtherCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Unix(1_775_240_000, 0).UTC()
|
||||
records := []SanctionRecord{
|
||||
{
|
||||
RecordID: SanctionRecordID("sanction-1"),
|
||||
UserID: common.UserID("user-123"),
|
||||
SanctionCode: SanctionCodePermanentBlock,
|
||||
Scope: common.Scope("platform"),
|
||||
ReasonCode: common.ReasonCode("terminal_policy_violation"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin"), ID: common.ActorID("admin-1")},
|
||||
AppliedAt: now.Add(-time.Hour),
|
||||
},
|
||||
{
|
||||
RecordID: SanctionRecordID("sanction-2"),
|
||||
UserID: common.UserID("user-123"),
|
||||
SanctionCode: SanctionCodeLoginBlock,
|
||||
Scope: common.Scope("auth"),
|
||||
ReasonCode: common.ReasonCode("policy"),
|
||||
Actor: common.ActorRef{Type: common.ActorType("admin")},
|
||||
AppliedAt: now.Add(-2 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
active, err := ActiveSanctionsAt(records, now)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, active, 2)
|
||||
require.Equal(t, SanctionCodeLoginBlock, active[0].SanctionCode)
|
||||
require.Equal(t, SanctionCodePermanentBlock, active[1].SanctionCode)
|
||||
}
|
||||
|
||||
func TestActiveSanctionsAtDuplicateActiveCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user