Files
galaxy-game/authsession/internal/service/sendemailcode/anti_abuse_test.go
T
2026-04-08 16:23:07 +02:00

168 lines
5.4 KiB
Go

package sendemailcode
import (
"context"
"testing"
"time"
"galaxy/authsession/internal/domain/challenge"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/domain/userresolution"
"galaxy/authsession/internal/ports"
"galaxy/authsession/internal/testkit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExecuteCreatesThrottledChallengeWithoutUserDirectoryOrMail(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
abuseProtector := &testkit.InMemorySendEmailCodeAbuseProtector{}
now := time.Unix(10, 0).UTC()
require.NoError(t, reserveSendCooldown(abuseProtector, common.Email("pilot@example.com"), now))
userDirectory := &countingUserDirectory{}
mailSender := &testkit.RecordingMailSender{}
service, err := NewWithRuntime(
challengeStore,
userDirectory,
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
abuseProtector,
testkit.FixedClock{Time: now},
nil,
)
require.NoError(t, err)
result, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
require.NoError(t, err)
assert.Equal(t, "challenge-1", result.ChallengeID)
assert.Zero(t, userDirectory.resolveCalls)
assert.Empty(t, mailSender.RecordedInputs())
record, getErr := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
require.NoError(t, getErr)
assert.Equal(t, challenge.StatusDeliveryThrottled, record.Status)
assert.Equal(t, challenge.DeliveryThrottled, record.DeliveryState)
assert.Equal(t, 1, record.Attempts.Send)
}
func TestExecuteBlockedEmailOutsideThrottleStillSuppressesDelivery(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
userDirectory := &testkit.InMemoryUserDirectory{}
require.NoError(t, userDirectory.SeedBlockedEmail(common.Email("pilot@example.com"), userresolution.BlockReasonCode("policy_block")))
mailSender := &testkit.RecordingMailSender{}
service, err := NewWithRuntime(
challengeStore,
userDirectory,
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
&testkit.InMemorySendEmailCodeAbuseProtector{},
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
nil,
)
require.NoError(t, err)
result, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
require.NoError(t, err)
assert.Equal(t, "challenge-1", result.ChallengeID)
assert.Empty(t, mailSender.RecordedInputs())
record, getErr := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
require.NoError(t, getErr)
assert.Equal(t, challenge.StatusDeliverySuppressed, record.Status)
assert.Equal(t, challenge.DeliverySuppressed, record.DeliveryState)
}
func TestExecuteAllowsAgainAfterCooldown(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
userDirectory := &testkit.InMemoryUserDirectory{}
mailSender := &testkit.RecordingMailSender{}
abuseProtector := &testkit.InMemorySendEmailCodeAbuseProtector{}
clock := &mutableClock{time: time.Unix(10, 0).UTC()}
idGenerator := &testkit.SequenceIDGenerator{
ChallengeIDs: []common.ChallengeID{"challenge-1", "challenge-2"},
}
service, err := NewWithRuntime(
challengeStore,
userDirectory,
idGenerator,
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
abuseProtector,
clock,
nil,
)
require.NoError(t, err)
first, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
require.NoError(t, err)
assert.Equal(t, "challenge-1", first.ChallengeID)
clock.time = clock.time.Add(challenge.ResendThrottleCooldown)
second, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
require.NoError(t, err)
assert.Equal(t, "challenge-2", second.ChallengeID)
require.Len(t, mailSender.RecordedInputs(), 2)
secondRecord, getErr := challengeStore.Get(context.Background(), common.ChallengeID("challenge-2"))
require.NoError(t, getErr)
assert.Equal(t, challenge.StatusSent, secondRecord.Status)
assert.Equal(t, challenge.DeliverySent, secondRecord.DeliveryState)
}
func reserveSendCooldown(protector ports.SendEmailCodeAbuseProtector, email common.Email, now time.Time) error {
_, err := protector.CheckAndReserve(context.Background(), ports.SendEmailCodeAbuseInput{
Email: email,
Now: now,
})
return err
}
type mutableClock struct {
time time.Time
}
func (c *mutableClock) Now() time.Time {
return c.time
}
type countingUserDirectory struct {
resolveCalls int
}
func (d *countingUserDirectory) ResolveByEmail(_ context.Context, _ common.Email) (userresolution.Result, error) {
d.resolveCalls++
return userresolution.Result{Kind: userresolution.KindCreatable}, nil
}
func (d *countingUserDirectory) ExistsByUserID(context.Context, common.UserID) (bool, error) {
return false, nil
}
func (d *countingUserDirectory) EnsureUserByEmail(context.Context, common.Email) (ports.EnsureUserResult, error) {
return ports.EnsureUserResult{}, nil
}
func (d *countingUserDirectory) BlockByUserID(context.Context, ports.BlockUserByIDInput) (ports.BlockUserResult, error) {
return ports.BlockUserResult{}, nil
}
func (d *countingUserDirectory) BlockByEmail(context.Context, ports.BlockUserByEmailInput) (ports.BlockUserResult, error) {
return ports.BlockUserResult{}, nil
}