package sendemailcode import ( "context" "testing" "time" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/userresolution" authtelemetry "galaxy/authsession/internal/telemetry" "galaxy/authsession/internal/testkit" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" ) func TestExecuteRecordsSentMetric(t *testing.T) { t.Parallel() runtime, reader := newObservedTelemetryRuntime(t) service, _, mailSender := newObservedSendService(t, observedSendOptions{ Telemetry: runtime, }) _, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"}) require.NoError(t, err) require.Len(t, mailSender.RecordedInputs(), 1) assertMetricCount(t, reader, "authsession.send_email_code.attempts", map[string]string{ "outcome": "sent", }, 1) } func TestExecuteRecordsBlockedSuppressedMetric(t *testing.T) { t.Parallel() runtime, reader := newObservedTelemetryRuntime(t) service, _, _ := newObservedSendService(t, observedSendOptions{ Telemetry: runtime, SeedBlockedEmail: true, }) _, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"}) require.NoError(t, err) assertMetricCount(t, reader, "authsession.send_email_code.attempts", map[string]string{ "outcome": "suppressed", "reason": "blocked", }, 1) } func TestExecuteRecordsThrottledMetric(t *testing.T) { t.Parallel() runtime, reader := newObservedTelemetryRuntime(t) abuseProtector := &testkit.InMemorySendEmailCodeAbuseProtector{} now := time.Unix(10, 0).UTC() require.NoError(t, reserveSendCooldown(abuseProtector, common.Email("pilot@example.com"), now)) service, _, mailSender := newObservedSendService(t, observedSendOptions{ Telemetry: runtime, AbuseProtector: abuseProtector, Clock: testkit.FixedClock{Time: now}, }) _, err := service.Execute(context.Background(), Input{Email: "pilot@example.com"}) require.NoError(t, err) assert.Empty(t, mailSender.RecordedInputs()) assertMetricCount(t, reader, "authsession.send_email_code.attempts", map[string]string{ "outcome": "throttled", "reason": "throttled", }, 1) } type observedSendOptions struct { Telemetry *authtelemetry.Runtime AbuseProtector *testkit.InMemorySendEmailCodeAbuseProtector SeedBlockedEmail bool Clock portsClock } type portsClock interface { Now() time.Time } func newObservedSendService(t *testing.T, options observedSendOptions) (*Service, *testkit.InMemoryChallengeStore, *testkit.RecordingMailSender) { t.Helper() challengeStore := &testkit.InMemoryChallengeStore{} userDirectory := &testkit.InMemoryUserDirectory{} if options.SeedBlockedEmail { require.NoError(t, userDirectory.SeedBlockedEmail(common.Email("pilot@example.com"), userresolution.BlockReasonCode("policy_block"))) } mailSender := &testkit.RecordingMailSender{} clock := options.Clock if clock == nil { clock = testkit.FixedClock{Time: time.Unix(10, 0).UTC()} } service, err := NewWithRuntime( challengeStore, userDirectory, &testkit.SequenceIDGenerator{ChallengeIDs: []common.ChallengeID{"challenge-1"}}, testkit.FixedCodeGenerator{Code: "654321"}, testkit.DeterministicCodeHasher{}, mailSender, options.AbuseProtector, clock, options.Telemetry, ) require.NoError(t, err) return service, challengeStore, mailSender } func newObservedTelemetryRuntime(t *testing.T) (*authtelemetry.Runtime, *sdkmetric.ManualReader) { t.Helper() reader := sdkmetric.NewManualReader() provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) runtime, err := authtelemetry.New(provider) require.NoError(t, err) return runtime, reader } func assertMetricCount(t *testing.T, reader *sdkmetric.ManualReader, metricName string, wantAttrs map[string]string, wantValue int64) { t.Helper() var resourceMetrics metricdata.ResourceMetrics require.NoError(t, reader.Collect(context.Background(), &resourceMetrics)) for _, scopeMetrics := range resourceMetrics.ScopeMetrics { for _, metric := range scopeMetrics.Metrics { if metric.Name != metricName { continue } sum, ok := metric.Data.(metricdata.Sum[int64]) require.True(t, ok) for _, point := range sum.DataPoints { if hasMetricAttributes(point.Attributes.ToSlice(), wantAttrs) { assert.Equal(t, wantValue, point.Value) return } } } } require.Failf(t, "test failed", "metric %q with attrs %v not found", metricName, wantAttrs) } func hasMetricAttributes(values []attribute.KeyValue, want map[string]string) bool { if len(values) != len(want) { return false } for _, value := range values { if want[string(value.Key)] != value.Value.AsString() { return false } } return true }