feat: sync account freeze lifecycle
This commit is contained in:
parent
76bfc5100f
commit
47fcf0ea41
40 changed files with 1363 additions and 196 deletions
|
|
@ -4,13 +4,14 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestSetSendFrozenDryRunExecuteAndIdempotency(t *testing.T) {
|
||||
func TestSetAccountFrozenDryRunExecuteAndIdempotency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := newMemoryCommandRepo()
|
||||
restrictions := &fakeRestrictionStore{}
|
||||
|
|
@ -20,10 +21,12 @@ func TestSetSendFrozenDryRunExecuteAndIdempotency(t *testing.T) {
|
|||
Now: fixedNow,
|
||||
})
|
||||
|
||||
dry, err := svc.SetSendFrozen(ctx, SetSendFrozenRequest{
|
||||
dry, err := svc.SetAccountFrozen(ctx, SetAccountFrozenRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "dry-freeze", Actor: "ops", Reason: "test", DryRun: true},
|
||||
UserID: 1001,
|
||||
Frozen: true,
|
||||
Until: fixedNow().Add(7 * 24 * time.Hour),
|
||||
AppealURL: "https://appeals.example.test/account/1001",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run freeze: %v", err)
|
||||
|
|
@ -32,23 +35,29 @@ func TestSetSendFrozenDryRunExecuteAndIdempotency(t *testing.T) {
|
|||
t.Fatalf("dry-run result=%+v setCalls=%d, want completed dry-run without mutation", dry, restrictions.setCalls)
|
||||
}
|
||||
|
||||
execReq := SetSendFrozenRequest{
|
||||
execReq := SetAccountFrozenRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "exec-freeze", Actor: "ops", Reason: "incident", DryRun: false},
|
||||
UserID: 1001,
|
||||
Frozen: true,
|
||||
Until: fixedNow().Add(7 * 24 * time.Hour),
|
||||
AppealURL: "https://appeals.example.test/account/1001",
|
||||
}
|
||||
exec, err := svc.SetSendFrozen(ctx, execReq)
|
||||
exec, err := svc.SetAccountFrozen(ctx, execReq)
|
||||
if err != nil {
|
||||
t.Fatalf("execute freeze: %v", err)
|
||||
}
|
||||
if exec.Status != string(domain.AdminCommandCompleted) || restrictions.setCalls != 1 {
|
||||
t.Fatalf("execute result=%+v setCalls=%d", exec, restrictions.setCalls)
|
||||
}
|
||||
if err := svc.CanSendMessages(ctx, 1001); !errors.Is(err, domain.ErrUserSendRestricted) {
|
||||
t.Fatalf("CanSendMessages err=%v, want ErrUserSendRestricted", err)
|
||||
if err := svc.CanSendMessages(ctx, 1001); !errors.Is(err, domain.ErrUserFrozen) {
|
||||
t.Fatalf("CanSendMessages err=%v, want ErrUserFrozen", err)
|
||||
}
|
||||
freeze, found, err := svc.AccountFreeze(ctx, 1001)
|
||||
if err != nil || !found || !freeze.Frozen || !freeze.Since.Equal(fixedNow()) || freeze.AppealURL != execReq.AppealURL {
|
||||
t.Fatalf("AccountFreeze = %+v found=%v err=%v", freeze, found, err)
|
||||
}
|
||||
|
||||
again, err := svc.SetSendFrozen(ctx, execReq)
|
||||
again, err := svc.SetAccountFrozen(ctx, execReq)
|
||||
if err != nil {
|
||||
t.Fatalf("duplicate freeze: %v", err)
|
||||
}
|
||||
|
|
@ -57,6 +66,101 @@ func TestSetSendFrozenDryRunExecuteAndIdempotency(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSetAccountFrozenRejectsIncompleteStateAndUnfreezeClearsOverlay(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
restrictions := &fakeRestrictionStore{}
|
||||
svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Restrictions: restrictions, Now: fixedNow})
|
||||
for _, req := range []SetAccountFrozenRequest{
|
||||
{CommandMeta: CommandMeta{CommandID: "bad-until", Actor: "ops", Reason: "test"}, UserID: 1001, Frozen: true, Until: fixedNow(), AppealURL: "https://appeals.example.test"},
|
||||
{CommandMeta: CommandMeta{CommandID: "too-far", Actor: "ops", Reason: "test"}, UserID: 1001, Frozen: true, Until: time.Unix(1<<31, 0), AppealURL: "https://appeals.example.test"},
|
||||
{CommandMeta: CommandMeta{CommandID: "bad-url", Actor: "ops", Reason: "test"}, UserID: 1001, Frozen: true, Until: fixedNow().Add(time.Hour), AppealURL: "javascript:bad"},
|
||||
{CommandMeta: CommandMeta{CommandID: "long-url", Actor: "ops", Reason: "test"}, UserID: 1001, Frozen: true, Until: fixedNow().Add(time.Hour), AppealURL: "https://appeals.example.test/" + strings.Repeat("x", maxFreezeAppealURLLength)},
|
||||
} {
|
||||
if _, err := svc.SetAccountFrozen(ctx, req); err == nil {
|
||||
t.Fatalf("SetAccountFrozen(%+v) succeeded", req)
|
||||
}
|
||||
}
|
||||
freezeReq := SetAccountFrozenRequest{CommandMeta: CommandMeta{CommandID: "freeze", Actor: "ops", Reason: "test"}, UserID: 1001, Frozen: true, Until: fixedNow().Add(24 * time.Hour), AppealURL: "https://appeals.example.test"}
|
||||
if _, err := svc.SetAccountFrozen(ctx, freezeReq); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.SetAccountFrozen(ctx, SetAccountFrozenRequest{CommandMeta: CommandMeta{CommandID: "unfreeze", Actor: "ops", Reason: "accepted"}, UserID: 1001}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
freeze, found, err := svc.AccountFreeze(ctx, 1001)
|
||||
if err != nil || !found || freeze.Frozen || !freeze.Since.IsZero() || !freeze.Until.IsZero() || freeze.AppealURL != "" {
|
||||
t.Fatalf("unfrozen state = %+v found=%v err=%v", freeze, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAccountFrozenUpdatePreservesOriginalSince(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := fixedNow()
|
||||
restrictions := &fakeRestrictionStore{}
|
||||
svc := NewService(Dependencies{
|
||||
Commands: newMemoryCommandRepo(),
|
||||
Restrictions: restrictions,
|
||||
Now: func() time.Time { return now },
|
||||
})
|
||||
if _, err := svc.SetAccountFrozen(ctx, SetAccountFrozenRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "freeze-initial", Actor: "ops", Reason: "review"},
|
||||
UserID: 1001,
|
||||
Frozen: true,
|
||||
Until: now.Add(24 * time.Hour),
|
||||
AppealURL: "https://appeals.example.test/initial",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
originalSince := now
|
||||
now = now.Add(2 * time.Hour)
|
||||
updatedUntil := now.Add(72 * time.Hour)
|
||||
if _, err := svc.SetAccountFrozen(ctx, SetAccountFrozenRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "freeze-update", Actor: "ops", Reason: "extend review"},
|
||||
UserID: 1001,
|
||||
Frozen: true,
|
||||
Until: updatedUntil,
|
||||
AppealURL: "https://appeals.example.test/updated",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
freeze, found, err := svc.AccountFreeze(ctx, 1001)
|
||||
if err != nil || !found || !freeze.Since.Equal(originalSince) || !freeze.Until.Equal(updatedUntil) ||
|
||||
freeze.AppealURL != "https://appeals.example.test/updated" {
|
||||
t.Fatalf("updated freeze = %+v found=%v err=%v", freeze, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAccountFrozenReplayRemainsIdempotentAfterDeadline(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := fixedNow()
|
||||
restrictions := &fakeRestrictionStore{}
|
||||
svc := NewService(Dependencies{
|
||||
Commands: newMemoryCommandRepo(),
|
||||
Restrictions: restrictions,
|
||||
Now: func() time.Time { return now },
|
||||
})
|
||||
req := SetAccountFrozenRequest{
|
||||
CommandMeta: CommandMeta{CommandID: "freeze-expiring", Actor: "ops", Reason: "review"},
|
||||
UserID: 1001,
|
||||
Frozen: true,
|
||||
Until: now.Add(time.Hour),
|
||||
AppealURL: "https://appeals.example.test/expiring",
|
||||
}
|
||||
if _, err := svc.SetAccountFrozen(ctx, req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now = now.Add(2 * time.Hour)
|
||||
replayed, err := svc.SetAccountFrozen(ctx, req)
|
||||
if err != nil || !replayed.AlreadyExecuted || restrictions.setCalls != 1 {
|
||||
t.Fatalf("expired replay = %+v err=%v setCalls=%d", replayed, err, restrictions.setCalls)
|
||||
}
|
||||
stale := req
|
||||
stale.CommandID = "new-stale-freeze"
|
||||
if _, err := svc.SetAccountFrozen(ctx, stale); err == nil || restrictions.setCalls != 1 {
|
||||
t.Fatalf("new stale request err=%v setCalls=%d, want rejection without state mutation", err, restrictions.setCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantPremiumDryRunExecuteAndIdempotency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := &fakeUsersService{users: map[int64]domain.User{
|
||||
|
|
@ -365,21 +469,21 @@ func (m *memoryCommandRepo) FinishCommand(_ context.Context, commandID string, s
|
|||
}
|
||||
|
||||
type fakeRestrictionStore struct {
|
||||
items map[int64]domain.AccountSendRestriction
|
||||
items map[int64]domain.AccountFreeze
|
||||
setCalls int
|
||||
}
|
||||
|
||||
func (f *fakeRestrictionStore) GetSendRestriction(_ context.Context, userID int64) (domain.AccountSendRestriction, bool, error) {
|
||||
func (f *fakeRestrictionStore) GetAccountFreeze(_ context.Context, userID int64) (domain.AccountFreeze, bool, error) {
|
||||
if f.items == nil {
|
||||
return domain.AccountSendRestriction{}, false, nil
|
||||
return domain.AccountFreeze{}, false, nil
|
||||
}
|
||||
r, ok := f.items[userID]
|
||||
return r, ok, nil
|
||||
}
|
||||
|
||||
func (f *fakeRestrictionStore) SetSendRestriction(_ context.Context, r domain.AccountSendRestriction) (domain.AccountSendRestriction, error) {
|
||||
func (f *fakeRestrictionStore) SetAccountFreeze(_ context.Context, r domain.AccountFreeze) (domain.AccountFreeze, error) {
|
||||
if f.items == nil {
|
||||
f.items = map[int64]domain.AccountSendRestriction{}
|
||||
f.items = map[int64]domain.AccountFreeze{}
|
||||
}
|
||||
f.setCalls++
|
||||
r.UpdatedAt = fixedNow()
|
||||
|
|
@ -387,13 +491,6 @@ func (f *fakeRestrictionStore) SetSendRestriction(_ context.Context, r domain.Ac
|
|||
return r, nil
|
||||
}
|
||||
|
||||
func (f *fakeRestrictionStore) IsSendFrozen(_ context.Context, userID int64) (bool, error) {
|
||||
if f.items == nil {
|
||||
return false, nil
|
||||
}
|
||||
return f.items[userID].Frozen, nil
|
||||
}
|
||||
|
||||
type fakeMessagesService struct {
|
||||
byID []domain.Message
|
||||
deleteCalls int
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue