feat: authsession service
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user