feat: backend service
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package backendclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/gateway/internal/backendclient"
|
||||
"galaxy/gateway/internal/session"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newRESTClient(t *testing.T, server *httptest.Server) *backendclient.RESTClient {
|
||||
t.Helper()
|
||||
cfg := backendclient.Config{
|
||||
HTTPBaseURL: server.URL,
|
||||
GRPCPushURL: "passthrough://test",
|
||||
GatewayClientID: "test-gateway",
|
||||
HTTPTimeout: time.Second,
|
||||
PushReconnectBaseBackoff: 10 * time.Millisecond,
|
||||
PushReconnectMaxBackoff: 100 * time.Millisecond,
|
||||
}
|
||||
client, err := backendclient.NewRESTClient(cfg)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = client.Close() })
|
||||
return client
|
||||
}
|
||||
|
||||
func TestRESTClientLookupSessionReturnsActiveRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
require.Equal(t, "/api/v1/internal/sessions/device-1", r.URL.Path)
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"device_session_id": "device-1",
|
||||
"user_id": "user-1",
|
||||
"status": "active",
|
||||
"client_public_key": "pk-1",
|
||||
"created_at": "2026-04-01T00:00:00Z",
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
rec, err := client.LookupSession(context.Background(), "device-1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, session.Record{
|
||||
DeviceSessionID: "device-1",
|
||||
UserID: "user-1",
|
||||
ClientPublicKey: "pk-1",
|
||||
Status: session.StatusActive,
|
||||
}, rec)
|
||||
}
|
||||
|
||||
func TestRESTClientLookupSessionReturnsRevokedRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"device_session_id": "device-2",
|
||||
"user_id": "user-2",
|
||||
"status": "revoked",
|
||||
"client_public_key": "pk-2",
|
||||
"created_at": "2026-04-01T00:00:00Z",
|
||||
"revoked_at": "2026-04-01T00:01:00Z",
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
rec, err := client.LookupSession(context.Background(), "device-2")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, session.StatusRevoked, rec.Status)
|
||||
require.NotNil(t, rec.RevokedAtMS)
|
||||
assert.Equal(t, time.Date(2026, 4, 1, 0, 1, 0, 0, time.UTC).UnixMilli(), *rec.RevokedAtMS)
|
||||
}
|
||||
|
||||
func TestRESTClientLookupSessionMapsNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusNotFound, map[string]any{"error": map[string]any{"code": "subject_not_found", "message": "missing"}})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
_, err := client.LookupSession(context.Background(), "missing")
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, session.ErrNotFound))
|
||||
}
|
||||
|
||||
func TestRESTClientLookupSessionRejectsMismatchedID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"device_session_id": "other",
|
||||
"user_id": "user-1",
|
||||
"status": "active",
|
||||
"client_public_key": "pk-1",
|
||||
"created_at": "2026-04-01T00:00:00Z",
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
_, err := client.LookupSession(context.Background(), "device-1")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not match requested")
|
||||
}
|
||||
|
||||
func TestRESTClientSendEmailCodeForwardsAcceptLanguage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "/api/v1/public/auth/send-email-code", r.URL.Path)
|
||||
require.Equal(t, "ru-RU", r.Header.Get("Accept-Language"))
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"challenge_id": "challenge-1"})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
out, err := client.SendEmailCode(context.Background(), backendclient.SendEmailCodeInput{
|
||||
Email: "user@example.com",
|
||||
PreferredLanguage: "ru-RU",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "challenge-1", out.ChallengeID)
|
||||
}
|
||||
|
||||
func TestRESTClientSendEmailCodeProjectsAuthError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(t, w, http.StatusBadRequest, map[string]any{
|
||||
"error": map[string]any{"code": "invalid_request", "message": "bad email"},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
_, err := client.SendEmailCode(context.Background(), backendclient.SendEmailCodeInput{Email: "user@example.com"})
|
||||
require.Error(t, err)
|
||||
var authErr *backendclient.AuthError
|
||||
require.ErrorAs(t, err, &authErr)
|
||||
assert.Equal(t, http.StatusBadRequest, authErr.StatusCode)
|
||||
assert.Equal(t, "invalid_request", authErr.Code)
|
||||
assert.Equal(t, "bad email", authErr.Message)
|
||||
}
|
||||
|
||||
func TestRESTClientConfirmEmailCodeReturnsDeviceSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/api/v1/public/auth/confirm-email-code", r.URL.Path)
|
||||
|
||||
var body backendclient.ConfirmEmailCodeInput
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&body))
|
||||
assert.Equal(t, "challenge-1", body.ChallengeID)
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{"device_session_id": "device-1"})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
out, err := client.ConfirmEmailCode(context.Background(), backendclient.ConfirmEmailCodeInput{
|
||||
ChallengeID: "challenge-1",
|
||||
Code: "12345",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "device-1", out.DeviceSessionID)
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(body))
|
||||
}
|
||||
|
||||
// guard ensures package keeps testify dependency.
|
||||
var _ = strings.TrimSpace
|
||||
Reference in New Issue
Block a user