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
@@ -92,7 +92,9 @@ func (c *HTTPAuthServiceClient) Close() error {
// SendEmailCode delegates the public send-email-code route to the configured
// Auth / Session Service public HTTP API.
func (c *HTTPAuthServiceClient) SendEmailCode(ctx context.Context, input SendEmailCodeInput) (SendEmailCodeResult, error) {
payload, statusCode, err := c.doJSONRequest(ctx, authServiceSendEmailCodePath, input)
payload, statusCode, err := c.doJSONRequest(ctx, authServiceSendEmailCodePath, input, map[string]string{
"Accept-Language": resolvePreferredLanguage(input.PreferredLanguage),
})
if err != nil {
return SendEmailCodeResult{}, fmt.Errorf("send email code via auth service: %w", err)
}
@@ -123,7 +125,7 @@ func (c *HTTPAuthServiceClient) SendEmailCode(ctx context.Context, input SendEma
// ConfirmEmailCode delegates the public confirm-email-code route to the
// configured Auth / Session Service public HTTP API.
func (c *HTTPAuthServiceClient) ConfirmEmailCode(ctx context.Context, input ConfirmEmailCodeInput) (ConfirmEmailCodeResult, error) {
payload, statusCode, err := c.doJSONRequest(ctx, authServiceConfirmEmailCodePath, input)
payload, statusCode, err := c.doJSONRequest(ctx, authServiceConfirmEmailCodePath, input, nil)
if err != nil {
return ConfirmEmailCodeResult{}, fmt.Errorf("confirm email code via auth service: %w", err)
}
@@ -151,7 +153,7 @@ func (c *HTTPAuthServiceClient) ConfirmEmailCode(ctx context.Context, input Conf
}
}
func (c *HTTPAuthServiceClient) doJSONRequest(ctx context.Context, path string, requestBody any) ([]byte, int, error) {
func (c *HTTPAuthServiceClient) doJSONRequest(ctx context.Context, path string, requestBody any, headers map[string]string) ([]byte, int, error) {
if c == nil || c.httpClient == nil {
return nil, 0, errors.New("nil client")
}
@@ -172,6 +174,12 @@ func (c *HTTPAuthServiceClient) doJSONRequest(ctx context.Context, path string,
return nil, 0, fmt.Errorf("build request: %w", err)
}
request.Header.Set("Content-Type", "application/json")
for key, value := range headers {
if strings.TrimSpace(value) == "" {
continue
}
request.Header.Set(key, value)
}
response, err := c.httpClient.Do(request)
if err != nil {
@@ -66,12 +66,14 @@ func TestHTTPAuthServiceClientSendEmailCodeSuccess(t *testing.T) {
t.Parallel()
var requestContentType string
var requestAcceptLanguage string
var requestBody string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, authServiceSendEmailCodePath, r.URL.Path)
requestContentType = r.Header.Get("Content-Type")
requestAcceptLanguage = r.Header.Get("Accept-Language")
payload, err := io.ReadAll(r.Body)
require.NoError(t, err)
requestBody = string(payload)
@@ -85,14 +87,35 @@ func TestHTTPAuthServiceClientSendEmailCodeSuccess(t *testing.T) {
client := newTestHTTPAuthServiceClient(t, server)
result, err := client.SendEmailCode(context.Background(), SendEmailCodeInput{
Email: "pilot@example.com",
Email: "pilot@example.com",
PreferredLanguage: "fr-FR",
})
require.NoError(t, err)
assert.Equal(t, SendEmailCodeResult{ChallengeID: "challenge-123"}, result)
assert.Equal(t, "application/json", requestContentType)
assert.Equal(t, "fr-FR", requestAcceptLanguage)
assert.JSONEq(t, `{"email":"pilot@example.com"}`, requestBody)
}
func TestHTTPAuthServiceClientSendEmailCodeDefaultsAcceptLanguageToEnglish(t *testing.T) {
t.Parallel()
var requestAcceptLanguage string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestAcceptLanguage = r.Header.Get("Accept-Language")
w.Header().Set("Content-Type", "application/json")
_, err := io.WriteString(w, `{"challenge_id":"challenge-123"}`)
require.NoError(t, err)
}))
defer server.Close()
client := newTestHTTPAuthServiceClient(t, server)
_, err := client.SendEmailCode(context.Background(), SendEmailCodeInput{Email: "pilot@example.com"})
require.NoError(t, err)
assert.Equal(t, "en", requestAcceptLanguage)
}
func TestHTTPAuthServiceClientConfirmEmailCodeSuccess(t *testing.T) {
t.Parallel()
@@ -0,0 +1,24 @@
package restapi
import "golang.org/x/text/language"
const defaultPreferredLanguage = "en"
func resolvePreferredLanguage(value string) string {
tags, _, err := language.ParseAcceptLanguage(value)
if err != nil {
return defaultPreferredLanguage
}
for _, tag := range tags {
canonical := tag.String()
switch canonical {
case "", "und", "mul":
continue
default:
return canonical
}
}
return defaultPreferredLanguage
}
@@ -0,0 +1,51 @@
package restapi
import "testing"
func TestResolvePreferredLanguage(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
want string
}{
{
name: "canonical valid tag",
value: "fr-FR, en;q=0.8",
want: "fr-FR",
},
{
name: "quality ordering",
value: "en-US;q=0.9, fr",
want: "fr",
},
{
name: "wildcard falls back",
value: "*",
want: "en",
},
{
name: "malformed falls back",
value: "fr-FR, @@",
want: "en",
},
{
name: "missing falls back",
value: "",
want: "en",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := resolvePreferredLanguage(tt.value); got != tt.want {
t.Fatalf("resolvePreferredLanguage(%q) = %q, want %q", tt.value, got, tt.want)
}
})
}
}
+6
View File
@@ -55,6 +55,11 @@ type SendEmailCodeInput struct {
// Email is the single client e-mail address that should receive the login
// code challenge.
Email string `json:"email"`
// PreferredLanguage stores the canonical BCP 47 language tag derived from
// the public Accept-Language header for upstream auth-mail localization and
// create-only user registration context.
PreferredLanguage string `json:"-"`
}
// SendEmailCodeResult describes the public REST and adapter payload returned
@@ -204,6 +209,7 @@ func handleSendEmailCode(authService AuthServiceClient, timeout time.Duration) g
abortInvalidRequest(c, err.Error())
return
}
input.PreferredLanguage = resolvePreferredLanguage(c.Request.Header.Get("Accept-Language"))
callCtx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
+5 -1
View File
@@ -34,6 +34,7 @@ func TestSendEmailCodeHandlerSuccess(t *testing.T) {
strings.NewReader(`{"email":" pilot@example.com "}`),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Language", "fr-FR, en;q=0.8")
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
@@ -43,7 +44,10 @@ func TestSendEmailCodeHandlerSuccess(t *testing.T) {
assert.Equal(t, `{"challenge_id":"challenge-123"}`, recorder.Body.String())
assert.Equal(t, 1, authService.sendEmailCodeCalls)
assert.Equal(t, 0, authService.confirmEmailCodeCalls)
assert.Equal(t, SendEmailCodeInput{Email: "pilot@example.com"}, authService.sendEmailCodeInput)
assert.Equal(t, SendEmailCodeInput{
Email: "pilot@example.com",
PreferredLanguage: "fr-FR",
}, authService.sendEmailCodeInput)
assert.True(t, authService.sendEmailCodeRouteClassOK)
assert.Equal(t, PublicRouteClassPublicAuth, authService.sendEmailCodeRouteClass)
}