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())) } 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 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) } } 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) } } 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) } } 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) } }