Files
galaxy-game/authsession/internal/service/sendemailcode/service_test.go
T
2026-04-17 18:39:16 +02:00

392 lines
13 KiB
Go

package sendemailcode
import (
"context"
"errors"
"github.com/stretchr/testify/require"
"testing"
"time"
"galaxy/authsession/internal/domain/challenge"
"galaxy/authsession/internal/domain/common"
"galaxy/authsession/internal/domain/userresolution"
"galaxy/authsession/internal/ports"
"galaxy/authsession/internal/service/shared"
"galaxy/authsession/internal/testkit"
)
func TestExecuteSendsChallengeForExistingAndCreatableUsers(t *testing.T) {
t.Parallel()
tests := []struct {
name string
seed func(*testkit.InMemoryUserDirectory) error
email string
}{
{
name: "existing",
seed: func(directory *testkit.InMemoryUserDirectory) error {
return directory.SeedExisting(common.Email("pilot@example.com"), common.UserID("user-1"))
},
email: " pilot@example.com ",
},
{
name: "creatable",
seed: func(*testkit.InMemoryUserDirectory) error { return nil },
email: "new@example.com",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
userDirectory := &testkit.InMemoryUserDirectory{}
if err := tt.seed(userDirectory); err != nil {
require.Failf(t, "test failed", "seed() returned error: %v", err)
}
mailSender := &testkit.RecordingMailSender{}
service, err := New(
challengeStore,
userDirectory,
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
result, err := service.Execute(context.Background(), Input{Email: tt.email})
if err != nil {
require.Failf(t, "test failed", "Execute() returned error: %v", err)
}
if result.ChallengeID != "challenge-1" {
require.Failf(t, "test failed", "Execute().ChallengeID = %q, want %q", result.ChallengeID, "challenge-1")
}
if len(mailSender.RecordedInputs()) != 1 {
require.Failf(t, "test failed", "RecordedInputs() length = %d, want 1", len(mailSender.RecordedInputs()))
}
if mailSender.RecordedInputs()[0].Locale != "en" {
require.Failf(t, "test failed", "mail locale = %q, want %q", mailSender.RecordedInputs()[0].Locale, "en")
}
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
if err != nil {
require.Failf(t, "test failed", "Get() returned error: %v", err)
}
if record.Status != challenge.StatusSent || record.DeliveryState != challenge.DeliverySent {
require.Failf(t, "test failed", "challenge state = %q/%q", record.Status, record.DeliveryState)
}
if record.Attempts.Send != 1 {
require.Failf(t, "test failed", "Attempts.Send = %d, want 1", record.Attempts.Send)
}
if record.PreferredLanguage != "en" {
require.Failf(t, "test failed", "PreferredLanguage = %q, want %q", record.PreferredLanguage, "en")
}
if string(record.CodeHash) == "654321" {
require.FailNow(t, "CodeHash stored cleartext code")
}
})
}
}
func TestExecuteSuppressesDeliveryForBlockedEmail(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
userDirectory := &testkit.InMemoryUserDirectory{}
if err := userDirectory.SeedBlockedEmail(common.Email("pilot@example.com"), userresolution.BlockReasonCode("policy_block")); err != nil {
require.Failf(t, "test failed", "SeedBlockedEmail() returned error: %v", err)
}
mailSender := &testkit.RecordingMailSender{}
service, err := New(
challengeStore,
userDirectory,
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
result, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
if err != nil {
require.Failf(t, "test failed", "Execute() returned error: %v", err)
}
if result.ChallengeID != "challenge-1" {
require.Failf(t, "test failed", "Execute().ChallengeID = %q, want %q", result.ChallengeID, "challenge-1")
}
if len(mailSender.RecordedInputs()) != 0 {
require.Failf(t, "test failed", "RecordedInputs() length = %d, want 0", len(mailSender.RecordedInputs()))
}
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
if err != nil {
require.Failf(t, "test failed", "Get() returned error: %v", err)
}
if record.Status != challenge.StatusDeliverySuppressed || record.DeliveryState != challenge.DeliverySuppressed {
require.Failf(t, "test failed", "challenge state = %q/%q", record.Status, record.DeliveryState)
}
if record.PreferredLanguage != "en" {
require.Failf(t, "test failed", "PreferredLanguage = %q, want %q", record.PreferredLanguage, "en")
}
}
func TestExecuteHandlesMailSenderSuppressedOutcome(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
mailSender := &testkit.RecordingMailSender{
DefaultResult: ports.SendLoginCodeResult{Outcome: ports.SendLoginCodeOutcomeSuppressed},
}
service, err := New(
challengeStore,
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
_, err = service.Execute(context.Background(), Input{Email: "pilot@example.com"})
if err != nil {
require.Failf(t, "test failed", "Execute() returned error: %v", err)
}
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
if err != nil {
require.Failf(t, "test failed", "Get() returned error: %v", err)
}
if record.Status != challenge.StatusDeliverySuppressed || record.DeliveryState != challenge.DeliverySuppressed {
require.Failf(t, "test failed", "challenge state = %q/%q", record.Status, record.DeliveryState)
}
if record.PreferredLanguage != "en" {
require.Failf(t, "test failed", "PreferredLanguage = %q, want %q", record.PreferredLanguage, "en")
}
}
func TestExecuteMarksChallengeFailedWhenMailSenderFails(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
mailSender := &testkit.RecordingMailSender{Err: errors.New("mail failed")}
service, err := New(
challengeStore,
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
_, err = service.Execute(context.Background(), Input{Email: "pilot@example.com"})
if shared.CodeOf(err) != shared.ErrorCodeServiceUnavailable {
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeServiceUnavailable)
}
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
if err != nil {
require.Failf(t, "test failed", "Get() returned error: %v", err)
}
if record.Status != challenge.StatusFailed || record.DeliveryState != challenge.DeliveryFailed {
require.Failf(t, "test failed", "challenge state = %q/%q", record.Status, record.DeliveryState)
}
if record.PreferredLanguage != "en" {
require.Failf(t, "test failed", "PreferredLanguage = %q, want %q", record.PreferredLanguage, "en")
}
}
func TestExecuteReturnsInvalidRequestForBadEmail(t *testing.T) {
t.Parallel()
service, err := New(
&testkit.InMemoryChallengeStore{},
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
&testkit.RecordingMailSender{},
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
_, err = service.Execute(context.Background(), Input{Email: "pilot"})
if shared.CodeOf(err) != shared.ErrorCodeInvalidRequest {
require.Failf(t, "test failed", "Execute() error code = %q, want %q", shared.CodeOf(err), shared.ErrorCodeInvalidRequest)
}
}
func TestExecuteCreatesFreshChallengeForRepeatedSend(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
mailSender := &testkit.RecordingMailSender{}
clock := testkit.FixedClock{Time: time.Unix(10, 0).UTC()}
service, err := New(
challengeStore,
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{
ChallengeIDs: []common.ChallengeID{"challenge-1", "challenge-2"},
},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
clock,
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
first, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
if err != nil {
require.Failf(t, "test failed", "first Execute() returned error: %v", err)
}
second, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"})
if err != nil {
require.Failf(t, "test failed", "second Execute() returned error: %v", err)
}
if first.ChallengeID == second.ChallengeID {
require.Failf(t, "test failed", "challenge ids are equal: %q", first.ChallengeID)
}
firstRecord, err := challengeStore.Get(context.Background(), common.ChallengeID(first.ChallengeID))
if err != nil {
require.Failf(t, "test failed", "Get(%q) returned error: %v", first.ChallengeID, err)
}
secondRecord, err := challengeStore.Get(context.Background(), common.ChallengeID(second.ChallengeID))
if err != nil {
require.Failf(t, "test failed", "Get(%q) returned error: %v", second.ChallengeID, err)
}
if firstRecord.Status != challenge.StatusSent {
require.Failf(t, "test failed", "first challenge status = %q, want %q", firstRecord.Status, challenge.StatusSent)
}
if secondRecord.Status != challenge.StatusSent {
require.Failf(t, "test failed", "second challenge status = %q, want %q", secondRecord.Status, challenge.StatusSent)
}
if len(mailSender.RecordedInputs()) != 2 {
require.Failf(t, "test failed", "RecordedInputs() length = %d, want 2", len(mailSender.RecordedInputs()))
}
}
func TestExecuteSetsChallengeExpirationFromInitialTTL(t *testing.T) {
t.Parallel()
now := time.Unix(10, 0).UTC()
challengeStore := &testkit.InMemoryChallengeStore{}
service, err := New(
challengeStore,
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
&testkit.RecordingMailSender{},
testkit.FixedClock{Time: now},
)
if err != nil {
require.Failf(t, "test failed", "New() returned error: %v", err)
}
if _, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"}); err != nil {
require.Failf(t, "test failed", "Execute() returned error: %v", err)
}
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
if err != nil {
require.Failf(t, "test failed", "Get() returned error: %v", err)
}
wantExpiresAt := now.Add(challenge.InitialTTL)
if !record.ExpiresAt.Equal(wantExpiresAt) {
require.Failf(t, "test failed", "ExpiresAt = %s, want %s", record.ExpiresAt, wantExpiresAt)
}
}
func TestExecuteResolvesPreferredLanguageFromAcceptLanguage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
acceptLanguage string
wantPreferredLang string
}{
{
name: "canonical valid tag wins",
acceptLanguage: "fr-FR, en;q=0.8",
wantPreferredLang: "fr-FR",
},
{
name: "wildcard falls back to english",
acceptLanguage: "*",
wantPreferredLang: "en",
},
{
name: "malformed header falls back to english",
acceptLanguage: "fr-FR, @@",
wantPreferredLang: "en",
},
{
name: "missing header falls back to english",
acceptLanguage: "",
wantPreferredLang: "en",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
challengeStore := &testkit.InMemoryChallengeStore{}
mailSender := &testkit.RecordingMailSender{}
service, err := New(
challengeStore,
&testkit.InMemoryUserDirectory{},
&testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}},
testkit.FixedCodeGenerator{Code: "654321"},
testkit.DeterministicCodeHasher{},
mailSender,
testkit.FixedClock{Time: time.Unix(10, 0).UTC()},
)
require.NoError(t, err)
_, err = service.Execute(context.Background(), Input{
Email: "pilot@example.com",
AcceptLanguage: tt.acceptLanguage,
})
require.NoError(t, err)
record, err := challengeStore.Get(context.Background(), common.ChallengeID("challenge-1"))
require.NoError(t, err)
require.Equal(t, tt.wantPreferredLang, record.PreferredLanguage)
attempts := mailSender.RecordedInputs()
require.Len(t, attempts, 1)
require.Equal(t, tt.wantPreferredLang, attempts[0].Locale)
})
}
}