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