370 lines
11 KiB
Go
370 lines
11 KiB
Go
package restapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewHTTPAuthServiceClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
baseURL string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "success",
|
|
baseURL: " http://127.0.0.1:8080/ ",
|
|
},
|
|
{
|
|
name: "empty base url",
|
|
wantErr: "base URL must not be empty",
|
|
},
|
|
{
|
|
name: "relative base url",
|
|
baseURL: "/authsession",
|
|
wantErr: "base URL must be absolute",
|
|
},
|
|
{
|
|
name: "malformed base url",
|
|
baseURL: "://bad",
|
|
wantErr: "parse base URL",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, err := NewHTTPAuthServiceClient(tt.baseURL)
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "http://127.0.0.1:8080", client.baseURL)
|
|
assert.NoError(t, client.Close())
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
result, err := client.SendEmailCode(context.Background(), SendEmailCodeInput{
|
|
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()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
assert.Equal(t, authServiceConfirmEmailCodePath, r.URL.Path)
|
|
|
|
payload, err := io.ReadAll(r.Body)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"challenge_id":"challenge-123","code":"123456","client_public_key":"public-key","time_zone":"Europe/Kaliningrad"}`, string(payload))
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, err = io.WriteString(w, `{"device_session_id":"device-session-123"}`)
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPAuthServiceClient(t, server)
|
|
|
|
result, err := client.ConfirmEmailCode(context.Background(), ConfirmEmailCodeInput{
|
|
ChallengeID: "challenge-123",
|
|
Code: "123456",
|
|
ClientPublicKey: "public-key",
|
|
TimeZone: "Europe/Kaliningrad",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ConfirmEmailCodeResult{DeviceSessionID: "device-session-123"}, result)
|
|
}
|
|
|
|
func TestHTTPAuthServiceClientProjectsAuthServiceErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
responseBody string
|
|
call func(*HTTPAuthServiceClient) error
|
|
wantStatusCode int
|
|
wantCode string
|
|
wantMessage string
|
|
}{
|
|
{
|
|
name: "send email code error",
|
|
statusCode: http.StatusServiceUnavailable,
|
|
responseBody: `{"error":{"code":"service_unavailable","message":"service is unavailable"}}`,
|
|
call: func(client *HTTPAuthServiceClient) error {
|
|
_, err := client.SendEmailCode(context.Background(), SendEmailCodeInput{Email: "pilot@example.com"})
|
|
return err
|
|
},
|
|
wantStatusCode: http.StatusServiceUnavailable,
|
|
wantCode: "service_unavailable",
|
|
wantMessage: "service is unavailable",
|
|
},
|
|
{
|
|
name: "confirm email code error",
|
|
statusCode: http.StatusConflict,
|
|
responseBody: `{"error":{"code":"session_limit_exceeded","message":"active session limit would be exceeded"}}`,
|
|
call: func(client *HTTPAuthServiceClient) error {
|
|
_, err := client.ConfirmEmailCode(context.Background(), ConfirmEmailCodeInput{
|
|
ChallengeID: "challenge-123",
|
|
Code: "123456",
|
|
ClientPublicKey: "public-key",
|
|
TimeZone: "Europe/Kaliningrad",
|
|
})
|
|
return err
|
|
},
|
|
wantStatusCode: http.StatusConflict,
|
|
wantCode: "session_limit_exceeded",
|
|
wantMessage: "active session limit would be exceeded",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(tt.statusCode)
|
|
_, err := io.WriteString(w, tt.responseBody)
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPAuthServiceClient(t, server)
|
|
err := tt.call(client)
|
|
require.Error(t, err)
|
|
|
|
var authErr *AuthServiceError
|
|
require.ErrorAs(t, err, &authErr)
|
|
assert.Equal(t, tt.wantStatusCode, authErr.StatusCode)
|
|
assert.Equal(t, tt.wantCode, authErr.Code)
|
|
assert.Equal(t, tt.wantMessage, authErr.Message)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTPAuthServiceClientRejectsMalformedPayloads(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
statusCode int
|
|
responseBody string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "send email code rejects unknown success field",
|
|
path: authServiceSendEmailCodePath,
|
|
statusCode: http.StatusOK,
|
|
responseBody: `{"challenge_id":"challenge-123","extra":true}`,
|
|
wantErr: "decode success response",
|
|
},
|
|
{
|
|
name: "confirm email code rejects empty success field",
|
|
path: authServiceConfirmEmailCodePath,
|
|
statusCode: http.StatusOK,
|
|
responseBody: `{"device_session_id":" "}`,
|
|
wantErr: "empty device_session_id",
|
|
},
|
|
{
|
|
name: "rejects missing error object",
|
|
path: authServiceSendEmailCodePath,
|
|
statusCode: http.StatusBadRequest,
|
|
responseBody: `{}`,
|
|
wantErr: "missing error object",
|
|
},
|
|
{
|
|
name: "rejects malformed error envelope",
|
|
path: authServiceConfirmEmailCodePath,
|
|
statusCode: http.StatusBadRequest,
|
|
responseBody: `{"error":{"code":"invalid_code","message":"confirmation code is invalid","extra":true}}`,
|
|
wantErr: "decode error response",
|
|
},
|
|
{
|
|
name: "rejects unexpected status",
|
|
path: authServiceSendEmailCodePath,
|
|
statusCode: http.StatusCreated,
|
|
responseBody: `{"challenge_id":"challenge-123"}`,
|
|
wantErr: "unexpected HTTP status 201",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, tt.path, r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(tt.statusCode)
|
|
_, err := io.WriteString(w, tt.responseBody)
|
|
require.NoError(t, err)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPAuthServiceClient(t, server)
|
|
|
|
var err error
|
|
switch tt.path {
|
|
case authServiceSendEmailCodePath:
|
|
_, err = client.SendEmailCode(context.Background(), SendEmailCodeInput{Email: "pilot@example.com"})
|
|
default:
|
|
_, err = client.ConfirmEmailCode(context.Background(), ConfirmEmailCodeInput{
|
|
ChallengeID: "challenge-123",
|
|
Code: "123456",
|
|
ClientPublicKey: "public-key",
|
|
TimeZone: "Europe/Kaliningrad",
|
|
})
|
|
}
|
|
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, tt.wantErr)
|
|
assert.NotErrorAs(t, err, new(*AuthServiceError))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTPAuthServiceClientUsesCallerContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"challenge_id":"challenge-123"}`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPAuthServiceClient(t, server)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err := client.SendEmailCode(ctx, SendEmailCodeInput{Email: "pilot@example.com"})
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "send email code via auth service")
|
|
assert.True(t, errors.Is(err, context.DeadlineExceeded))
|
|
}
|
|
|
|
func TestHTTPAuthServiceClientRejectsNilContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.FailNow(t, "unexpected request", r.URL.Path)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPAuthServiceClient(t, server)
|
|
|
|
_, err := client.SendEmailCode(nil, SendEmailCodeInput{Email: "pilot@example.com"})
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "nil context")
|
|
}
|
|
|
|
func newTestHTTPAuthServiceClient(t *testing.T, server *httptest.Server) *HTTPAuthServiceClient {
|
|
t.Helper()
|
|
|
|
client, err := newHTTPAuthServiceClient(server.URL, server.Client())
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, client.Close())
|
|
})
|
|
|
|
return client
|
|
}
|
|
|
|
func TestDecodeStrictJSONPayloadRejectsTrailingJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var target struct {
|
|
Value string `json:"value"`
|
|
}
|
|
err := decodeStrictJSONPayload([]byte(`{"value":"ok"}{}`), &target)
|
|
require.Error(t, err)
|
|
assert.Equal(t, "unexpected trailing JSON input", err.Error())
|
|
}
|
|
|
|
func TestDecodeAuthServiceErrorPreservesBlankFieldsForLaterNormalization(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
authErr, err := decodeAuthServiceError(http.StatusBadGateway, []byte(`{"error":{"code":" ","message":" "}}`))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusBadGateway, authErr.StatusCode)
|
|
assert.True(t, strings.TrimSpace(authErr.Code) == "")
|
|
assert.True(t, strings.TrimSpace(authErr.Message) == "")
|
|
}
|