168 lines
5.4 KiB
Go
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
|
|
}
|