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
@@ -10,6 +10,7 @@ import (
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
@@ -57,7 +58,9 @@ func TestUserServiceRESTCompatibilityPublicSendUsesResolveByEmailOutcomes(t *tes
attempts := harness.mailSender.RecordedAttempts()
require.Len(t, attempts, 2)
assert.Equal(t, common.Email("existing@example.com"), attempts[0].Input.Email)
assert.Equal(t, "en", attempts[0].Input.Locale)
assert.Equal(t, common.Email("creatable@example.com"), attempts[1].Input.Email)
assert.Equal(t, "en", attempts[1].Input.Locale)
}
func TestUserServiceRESTCompatibilityPublicConfirmUsesEnsureOutcomes(t *testing.T) {
@@ -162,12 +165,34 @@ func TestUserServiceRESTCompatibilityInternalBlockUserUsesRESTClient(t *testing.
})
}
func TestUserServiceRESTCompatibilityAcceptLanguageDrivesMailLocaleAndRegistrationContext(t *testing.T) {
t.Parallel()
harness := newUserServiceRESTCompatibilityHarness(t)
require.NoError(t, harness.directory.QueueCreatedUserIDs(common.UserID("user-created")))
challengeID := harness.sendChallengeIDWithAcceptLanguage(t, "localized@example.com", "fr-FR, en;q=0.8", "fr-FR")
attempts := harness.mailSender.RecordedAttempts()
require.Len(t, attempts, 1)
assert.Equal(t, "fr-FR", attempts[0].Input.Locale)
response := gatewayCompatibilityPostJSONValue(
t,
harness.publicBaseURL+"/api/v1/public/auth/confirm-email-code",
gatewayCompatibilityConfirmRequest(challengeID, userServiceRESTCompatibilityCode, gatewayCompatibilityClientPublicKey),
)
assert.Equal(t, http.StatusOK, response.StatusCode)
assert.JSONEq(t, `{"device_session_id":"device-session-1"}`, response.Body)
}
type userServiceRESTCompatibilityHarness struct {
publicBaseURL string
internalBaseURL string
mailSender *mail.StubSender
sessionStore *testkit.InMemorySessionStore
directory *userservice.StubDirectory
publicBaseURL string
internalBaseURL string
mailSender *mail.StubSender
sessionStore *testkit.InMemorySessionStore
directory *userservice.StubDirectory
preferredLanguageExpectations *preferredLanguageExpectationStore
}
func newUserServiceRESTCompatibilityHarness(t *testing.T) userServiceRESTCompatibilityHarness {
@@ -176,8 +201,9 @@ func newUserServiceRESTCompatibilityHarness(t *testing.T) userServiceRESTCompati
challengeStore := &testkit.InMemoryChallengeStore{}
sessionStore := &testkit.InMemorySessionStore{}
directory := &userservice.StubDirectory{}
preferredLanguageExpectations := newPreferredLanguageExpectationStore()
userServiceServer := httptest.NewServer(newUserServiceStubHandler(directory))
userServiceServer := httptest.NewServer(newUserServiceStubHandler(directory, preferredLanguageExpectations))
t.Cleanup(userServiceServer.Close)
userDirectory, err := userservice.NewRESTClient(userservice.Config{
@@ -261,18 +287,31 @@ func newUserServiceRESTCompatibilityHarness(t *testing.T) userServiceRESTCompati
gatewayCompatibilityRunServer(t, internalServer.Run, internalServer.Shutdown, internalCfg.Addr)
return userServiceRESTCompatibilityHarness{
publicBaseURL: "http://" + publicCfg.Addr,
internalBaseURL: "http://" + internalCfg.Addr,
mailSender: mailSender,
sessionStore: sessionStore,
directory: directory,
publicBaseURL: "http://" + publicCfg.Addr,
internalBaseURL: "http://" + internalCfg.Addr,
mailSender: mailSender,
sessionStore: sessionStore,
directory: directory,
preferredLanguageExpectations: preferredLanguageExpectations,
}
}
func (h userServiceRESTCompatibilityHarness) sendChallengeID(t *testing.T, email string) string {
t.Helper()
response := gatewayCompatibilityPostJSON(t, h.publicBaseURL+"/api/v1/public/auth/send-email-code", fmt.Sprintf(`{"email":"%s"}`, email))
return h.sendChallengeIDWithAcceptLanguage(t, email, "", "en")
}
func (h userServiceRESTCompatibilityHarness) sendChallengeIDWithAcceptLanguage(t *testing.T, email string, acceptLanguage string, expectedPreferredLanguage string) string {
t.Helper()
h.preferredLanguageExpectations.Set(email, expectedPreferredLanguage)
response := gatewayCompatibilityPostJSONWithHeaders(
t,
h.publicBaseURL+"/api/v1/public/auth/send-email-code",
fmt.Sprintf(`{"email":"%s"}`, email),
map[string]string{"Accept-Language": acceptLanguage},
)
assert.Equal(t, http.StatusOK, response.StatusCode)
var body struct {
@@ -284,7 +323,7 @@ func (h userServiceRESTCompatibilityHarness) sendChallengeID(t *testing.T, email
return body.ChallengeID
}
func newUserServiceStubHandler(directory *userservice.StubDirectory) http.Handler {
func newUserServiceStubHandler(directory *userservice.StubDirectory, preferredLanguageExpectations *preferredLanguageExpectationStore) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch {
case request.Method == http.MethodPost && request.URL.Path == "/api/v1/internal/user-resolutions/by-email":
@@ -349,8 +388,13 @@ func newUserServiceStubHandler(directory *userservice.StubDirectory) http.Handle
writeUserServiceStubError(writer, http.StatusBadRequest, errors.New("registration_context must be present"))
return
}
if ensureInput.RegistrationContext.PreferredLanguage != "en" {
writeUserServiceStubError(writer, http.StatusBadRequest, errors.New("registration_context.preferred_language must equal en during rollout"))
expectedPreferredLanguage := preferredLanguageExpectations.Expected(input.Email)
if ensureInput.RegistrationContext.PreferredLanguage != expectedPreferredLanguage {
writeUserServiceStubError(
writer,
http.StatusBadRequest,
fmt.Errorf("registration_context.preferred_language must equal %s", expectedPreferredLanguage),
)
return
}
if ensureInput.RegistrationContext.TimeZone != gatewayCompatibilityTimeZone {
@@ -434,6 +478,44 @@ func newUserServiceStubHandler(directory *userservice.StubDirectory) http.Handle
})
}
type preferredLanguageExpectationStore struct {
mu sync.Mutex
byEmail map[string]string
}
func newPreferredLanguageExpectationStore() *preferredLanguageExpectationStore {
return &preferredLanguageExpectationStore{
byEmail: make(map[string]string),
}
}
func (s *preferredLanguageExpectationStore) Set(email string, preferredLanguage string) {
if s == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.byEmail[email] = preferredLanguage
}
func (s *preferredLanguageExpectationStore) Expected(email string) string {
if s == nil {
return "en"
}
s.mu.Lock()
defer s.mu.Unlock()
preferredLanguage := s.byEmail[email]
if preferredLanguage == "" {
return "en"
}
return preferredLanguage
}
func decodeUserServiceStubRequest(writer http.ResponseWriter, request *http.Request, target any) bool {
decoder := json.NewDecoder(request.Body)
decoder.DisallowUnknownFields()