feat: mail service

This commit is contained in:
Ilia Denisov
2026-04-17 18:39:16 +02:00
committed by GitHub
parent 23ffcb7535
commit 5b7593e6f6
183 changed files with 31215 additions and 248 deletions
@@ -0,0 +1,184 @@
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")
}