package internalhttp import ( "net/http" "net/http/httptest" "strings" "testing" "galaxy/mail/internal/domain/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDecodeLoginCodeDeliveryCommandSuccess(t *testing.T) { t.Parallel() request := httptest.NewRequest(http.MethodPost, LoginCodeDeliveriesPath, strings.NewReader(`{"email":" pilot@example.com ","code":"123456","locale":" en "}`)) request.Header.Set("Content-Type", "application/json") request.Header.Set(IdempotencyKeyHeader, "challenge-1") command, err := DecodeLoginCodeDeliveryCommand(request) require.NoError(t, err) assert.Equal(t, LoginCodeDeliveryCommand{ IdempotencyKey: common.IdempotencyKey("challenge-1"), Email: common.Email("pilot@example.com"), Code: "123456", Locale: common.Locale("en"), }, command) } func TestDecodeLoginCodeDeliveryCommandRejectsInvalidRequests(t *testing.T) { t.Parallel() tests := []struct { name string contentType string headerValue string body string wantErr string }{ { name: "missing content type", headerValue: "challenge-1", body: `{"email":"pilot@example.com","code":"123456","locale":"en"}`, wantErr: "Content-Type must be application/json", }, { name: "missing idempotency key", contentType: "application/json", body: `{"email":"pilot@example.com","code":"123456","locale":"en"}`, wantErr: "Idempotency-Key header must not be empty", }, { name: "idempotency key surrounding whitespace", contentType: "application/json", headerValue: " challenge-1 ", body: `{"email":"pilot@example.com","code":"123456","locale":"en"}`, wantErr: "Idempotency-Key header must not contain surrounding whitespace", }, { name: "unknown field", contentType: "application/json", headerValue: "challenge-1", body: `{"email":"pilot@example.com","code":"123456","locale":"en","extra":true}`, wantErr: "decode request body", }, { name: "trailing json", contentType: "application/json", headerValue: "challenge-1", body: `{"email":"pilot@example.com","code":"123456","locale":"en"}{}`, wantErr: "unexpected trailing JSON input", }, { name: "code surrounding whitespace", contentType: "application/json", headerValue: "challenge-1", body: `{"email":"pilot@example.com","code":" 123456 ","locale":"en"}`, wantErr: "code must not contain surrounding whitespace", }, { name: "invalid locale", contentType: "application/json", headerValue: "challenge-1", body: `{"email":"pilot@example.com","code":"123456","locale":"english"}`, wantErr: "locale:", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() request := httptest.NewRequest(http.MethodPost, LoginCodeDeliveriesPath, strings.NewReader(tt.body)) if tt.contentType != "" { request.Header.Set("Content-Type", tt.contentType) } if tt.headerValue != "" { request.Header.Set(IdempotencyKeyHeader, tt.headerValue) } _, err := DecodeLoginCodeDeliveryCommand(request) require.Error(t, err) assert.ErrorContains(t, err, tt.wantErr) }) } } func TestDecodeLoginCodeDeliveryCommandRepeatedEquivalentRequestsMatch(t *testing.T) { t.Parallel() first := httptest.NewRequest(http.MethodPost, LoginCodeDeliveriesPath, strings.NewReader(`{"email":"pilot@example.com","code":"123456","locale":"en"}`)) first.Header.Set("Content-Type", "application/json") first.Header.Set(IdempotencyKeyHeader, "challenge-1") second := httptest.NewRequest(http.MethodPost, LoginCodeDeliveriesPath, strings.NewReader(`{"email":" pilot@example.com ","code":"123456","locale":" en "}`)) second.Header.Set("Content-Type", "application/json") second.Header.Set(IdempotencyKeyHeader, "challenge-1") firstCommand, err := DecodeLoginCodeDeliveryCommand(first) require.NoError(t, err) secondCommand, err := DecodeLoginCodeDeliveryCommand(second) require.NoError(t, err) assert.Equal(t, firstCommand, secondCommand) } func TestLoginCodeDeliveryCommandFingerprintStableForEquivalentRequests(t *testing.T) { t.Parallel() first := LoginCodeDeliveryCommand{ IdempotencyKey: common.IdempotencyKey("challenge-1"), Email: common.Email("pilot@example.com"), Code: "123456", Locale: common.Locale("en"), } second := LoginCodeDeliveryCommand{ IdempotencyKey: common.IdempotencyKey("challenge-1"), Email: common.Email("pilot@example.com"), Code: "123456", Locale: common.Locale("en"), } firstFingerprint, err := first.Fingerprint() require.NoError(t, err) secondFingerprint, err := second.Fingerprint() require.NoError(t, err) assert.Equal(t, firstFingerprint, secondFingerprint) } func TestLoginCodeDeliveryResponseValidate(t *testing.T) { t.Parallel() require.NoError(t, LoginCodeDeliveryResponse{Outcome: LoginCodeDeliveryOutcomeSent}.Validate()) require.NoError(t, LoginCodeDeliveryResponse{Outcome: LoginCodeDeliveryOutcomeSuppressed}.Validate()) err := LoginCodeDeliveryResponse{Outcome: LoginCodeDeliveryOutcome("queued")}.Validate() require.Error(t, err) assert.ErrorContains(t, err, "unsupported") } func TestErrorResponseValidate(t *testing.T) { t.Parallel() require.NoError(t, ErrorResponse{ Error: ErrorBody{ Code: ErrorCodeInvalidRequest, Message: "field-specific validation detail", }, }.Validate()) err := ErrorResponse{ Error: ErrorBody{ Code: " invalid_request ", Message: "", }, }.Validate() require.Error(t, err) assert.ErrorContains(t, err, "error code") }