302 lines
9.5 KiB
Go
302 lines
9.5 KiB
Go
package authsession
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
mailadapter "galaxy/authsession/internal/adapters/mail"
|
|
"galaxy/authsession/internal/adapters/userservice"
|
|
"galaxy/authsession/internal/api/publichttp"
|
|
"galaxy/authsession/internal/domain/common"
|
|
"galaxy/authsession/internal/domain/userresolution"
|
|
"galaxy/authsession/internal/service/confirmemailcode"
|
|
"galaxy/authsession/internal/service/sendemailcode"
|
|
"galaxy/authsession/internal/testkit"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestMailServiceRESTCompatibilitySendEmailCodeSent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusOK,
|
|
MailResponse: `{"outcome":"sent"}`,
|
|
})
|
|
|
|
response := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-1"}`, response.Body)
|
|
assert.Equal(t, 1, harness.mailServer.CallCount())
|
|
deliveries := harness.mailServer.RecordedDeliveries()
|
|
require.Len(t, deliveries, 1)
|
|
assert.Equal(t, "en", deliveries[0].Locale)
|
|
assert.Equal(t, "challenge-1", deliveries[0].IdempotencyKey)
|
|
}
|
|
|
|
func TestMailServiceRESTCompatibilitySendEmailCodeSuppressed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusOK,
|
|
MailResponse: `{"outcome":"suppressed"}`,
|
|
})
|
|
|
|
response := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-1"}`, response.Body)
|
|
assert.Equal(t, 1, harness.mailServer.CallCount())
|
|
}
|
|
|
|
func TestMailServiceRESTCompatibilitySendEmailCodeExplicitFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusServiceUnavailable,
|
|
MailResponse: `{"error":"temporary"}`,
|
|
})
|
|
|
|
response := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
assert.Equal(t, http.StatusServiceUnavailable, response.StatusCode)
|
|
assert.JSONEq(t, `{"error":{"code":"service_unavailable","message":"service is unavailable"}}`, response.Body)
|
|
assert.Equal(t, 1, harness.mailServer.CallCount())
|
|
}
|
|
|
|
func TestMailServiceRESTCompatibilityBlockedSendSkipsMailService(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusOK,
|
|
MailResponse: `{"outcome":"sent"}`,
|
|
SeedBlockedEmail: true,
|
|
})
|
|
|
|
response := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-1"}`, response.Body)
|
|
assert.Equal(t, 0, harness.mailServer.CallCount())
|
|
}
|
|
|
|
func TestMailServiceRESTCompatibilityThrottledSendSkipsMailService(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusOK,
|
|
MailResponse: `{"outcome":"sent"}`,
|
|
AbuseProtector: &testkit.InMemorySendEmailCodeAbuseProtector{},
|
|
})
|
|
|
|
first := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
second := gatewayCompatibilityPostJSON(t, harness.publicBaseURL+"/api/v1/public/auth/send-email-code", `{"email":"pilot@example.com"}`)
|
|
|
|
assert.Equal(t, http.StatusOK, first.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-1"}`, first.Body)
|
|
assert.Equal(t, http.StatusOK, second.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-2"}`, second.Body)
|
|
assert.Equal(t, 1, harness.mailServer.CallCount())
|
|
}
|
|
|
|
func TestMailServiceRESTCompatibilitySendEmailCodeForwardsLocalizedLocale(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
harness := newMailServiceRESTCompatibilityHarness(t, mailServiceRESTCompatibilityOptions{
|
|
MailStatusCode: http.StatusOK,
|
|
MailResponse: `{"outcome":"sent"}`,
|
|
})
|
|
|
|
response := gatewayCompatibilityPostJSONWithHeaders(
|
|
t,
|
|
harness.publicBaseURL+"/api/v1/public/auth/send-email-code",
|
|
`{"email":"pilot@example.com"}`,
|
|
map[string]string{"Accept-Language": "fr-FR, en;q=0.8"},
|
|
)
|
|
assert.Equal(t, http.StatusOK, response.StatusCode)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-1"}`, response.Body)
|
|
|
|
deliveries := harness.mailServer.RecordedDeliveries()
|
|
require.Len(t, deliveries, 1)
|
|
assert.Equal(t, "fr-FR", deliveries[0].Locale)
|
|
assert.Equal(t, "challenge-1", deliveries[0].IdempotencyKey)
|
|
}
|
|
|
|
type mailServiceRESTCompatibilityOptions struct {
|
|
MailStatusCode int
|
|
MailResponse string
|
|
SeedBlockedEmail bool
|
|
AbuseProtector *testkit.InMemorySendEmailCodeAbuseProtector
|
|
}
|
|
|
|
type mailServiceRESTCompatibilityHarness struct {
|
|
publicBaseURL string
|
|
mailServer *mailServiceStubServer
|
|
}
|
|
|
|
func newMailServiceRESTCompatibilityHarness(t *testing.T, options mailServiceRESTCompatibilityOptions) mailServiceRESTCompatibilityHarness {
|
|
t.Helper()
|
|
|
|
challengeStore := &testkit.InMemoryChallengeStore{}
|
|
sessionStore := &testkit.InMemorySessionStore{}
|
|
userDirectory := &userservice.StubDirectory{}
|
|
if options.SeedBlockedEmail {
|
|
require.NoError(t, userDirectory.SeedBlockedEmail(common.Email("pilot@example.com"), userresolution.BlockReasonCode("policy_blocked")))
|
|
}
|
|
|
|
mailServer := newMailServiceStubServer(options.MailStatusCode, options.MailResponse)
|
|
httpServer := httptest.NewServer(mailServer.Handler())
|
|
t.Cleanup(httpServer.Close)
|
|
|
|
mailSender, err := mailadapter.NewRESTClient(mailadapter.Config{
|
|
BaseURL: httpServer.URL,
|
|
RequestTimeout: 250 * time.Millisecond,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, mailSender.Close())
|
|
})
|
|
|
|
idGenerator := &testkit.SequenceIDGenerator{}
|
|
codeGenerator := testkit.FixedCodeGenerator{Code: "123456"}
|
|
codeHasher := testkit.DeterministicCodeHasher{}
|
|
clock := testkit.FixedClock{Time: time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC)}
|
|
configProvider := testkit.StaticConfigProvider{}
|
|
projectionPublisher := &testkit.RecordingProjectionPublisher{}
|
|
|
|
sendEmailCodeService, err := sendemailcode.NewWithObservability(
|
|
challengeStore,
|
|
userDirectory,
|
|
idGenerator,
|
|
codeGenerator,
|
|
codeHasher,
|
|
mailSender,
|
|
options.AbuseProtector,
|
|
clock,
|
|
zap.NewNop(),
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
confirmEmailCodeService, err := confirmemailcode.NewWithObservability(
|
|
challengeStore,
|
|
sessionStore,
|
|
userDirectory,
|
|
configProvider,
|
|
projectionPublisher,
|
|
idGenerator,
|
|
codeHasher,
|
|
clock,
|
|
zap.NewNop(),
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
publicCfg := publichttp.DefaultConfig()
|
|
publicCfg.Addr = gatewayCompatibilityFreeAddr(t)
|
|
publicServer, err := publichttp.NewServer(publicCfg, publichttp.Dependencies{
|
|
SendEmailCode: sendEmailCodeService,
|
|
ConfirmEmailCode: confirmEmailCodeService,
|
|
Logger: zap.NewNop(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
gatewayCompatibilityRunServer(t, publicServer.Run, publicServer.Shutdown, publicCfg.Addr)
|
|
|
|
return mailServiceRESTCompatibilityHarness{
|
|
publicBaseURL: "http://" + publicCfg.Addr,
|
|
mailServer: mailServer,
|
|
}
|
|
}
|
|
|
|
type mailServiceStubServer struct {
|
|
mu sync.Mutex
|
|
statusCode int
|
|
response string
|
|
callCount int
|
|
deliveries []mailServiceStubDelivery
|
|
}
|
|
|
|
type mailServiceStubDelivery struct {
|
|
Email string
|
|
Code string
|
|
Locale string
|
|
IdempotencyKey string
|
|
}
|
|
|
|
func newMailServiceStubServer(statusCode int, response string) *mailServiceStubServer {
|
|
return &mailServiceStubServer{
|
|
statusCode: statusCode,
|
|
response: response,
|
|
}
|
|
}
|
|
|
|
func (s *mailServiceStubServer) Handler() http.Handler {
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method != http.MethodPost || request.URL.Path != "/api/v1/internal/login-code-deliveries" {
|
|
http.NotFound(writer, request)
|
|
return
|
|
}
|
|
if strings.TrimSpace(request.Header.Get("Idempotency-Key")) == "" {
|
|
http.Error(writer, "Idempotency-Key header must not be empty", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
decoder := json.NewDecoder(request.Body)
|
|
decoder.DisallowUnknownFields()
|
|
|
|
var body struct {
|
|
Email string `json:"email"`
|
|
Code string `json:"code"`
|
|
Locale string `json:"locale"`
|
|
}
|
|
if err := decoder.Decode(&body); err != nil {
|
|
http.Error(writer, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
http.Error(writer, "unexpected trailing JSON input", http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(writer, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.callCount++
|
|
s.deliveries = append(s.deliveries, mailServiceStubDelivery{
|
|
Email: body.Email,
|
|
Code: body.Code,
|
|
Locale: body.Locale,
|
|
IdempotencyKey: request.Header.Get("Idempotency-Key"),
|
|
})
|
|
s.mu.Unlock()
|
|
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
writer.WriteHeader(s.statusCode)
|
|
_, _ = io.WriteString(writer, s.response)
|
|
})
|
|
}
|
|
|
|
func (s *mailServiceStubServer) CallCount() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
return s.callCount
|
|
}
|
|
|
|
func (s *mailServiceStubServer) RecordedDeliveries() []mailServiceStubDelivery {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
cloned := make([]mailServiceStubDelivery, len(s.deliveries))
|
|
copy(cloned, s.deliveries)
|
|
return cloned
|
|
}
|