Files
galaxy-game/authsession/mail_service_rest_compatibility_test.go
T
2026-04-08 16:23:07 +02:00

246 lines
7.7 KiB
Go

package authsession
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"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())
}
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())
}
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
}
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
}
s.mu.Lock()
s.callCount++
s.mu.Unlock()
decoder := json.NewDecoder(request.Body)
decoder.DisallowUnknownFields()
var body struct {
Email string `json:"email"`
Code string `json:"code"`
}
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
}
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
}