Files
Ilia Denisov 89bf7e6576 phase 4: drop stale gRPC nomenclature from integration tests
Phase 4 replaced the gateway's authenticated edge listener with a
Connect-Go HTTP/h2c bootstrap that natively serves Connect, gRPC,
and gRPC-Web. Sweep the integration suite so test names, comments,
and helper docs match the new transport posture: rename
TestUserAccount_GetThroughGatewayGRPC to TestUserAccount_GetThroughGatewayEdge,
flip "authenticated gRPC" / "signed gRPC" / "gateway gRPC" comments
to "authenticated edge", and align testenv doc strings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 11:52:17 +02:00

273 lines
7.7 KiB
Go

package testenv
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// PublicRESTClient exposes the public REST surface of the gateway
// (`/api/v1/public/*`). Tests use it for unauthenticated registration
// flows.
type PublicRESTClient struct {
BaseURL string
HTTP *http.Client
}
// NewPublicRESTClient constructs a client targeting baseURL.
func NewPublicRESTClient(baseURL string) *PublicRESTClient {
return &PublicRESTClient{
BaseURL: strings.TrimRight(baseURL, "/"),
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// SendEmailCodeResponse mirrors the wire shape of
// `POST /api/v1/public/auth/send-email-code`.
type SendEmailCodeResponse struct {
ChallengeID string `json:"challenge_id"`
}
// ConfirmEmailCodeResponse mirrors the wire shape of
// `POST /api/v1/public/auth/confirm-email-code`.
type ConfirmEmailCodeResponse struct {
DeviceSessionID string `json:"device_session_id"`
}
// SendEmailCode triggers an email-code challenge. The `locale` value
// is sent through the public REST contract as the `Accept-Language`
// header (gateway derives `preferred_language` from it; the body
// schema rejects unknown fields).
func (c *PublicRESTClient) SendEmailCode(ctx context.Context, email string, locale string) (*SendEmailCodeResponse, *http.Response, error) {
body := map[string]any{"email": email}
headers := http.Header{}
if locale != "" {
headers.Set("Accept-Language", locale)
}
resp, raw, err := c.doWithHeaders(ctx, http.MethodPost, "/api/v1/public/auth/send-email-code", body, headers)
if err != nil {
return nil, raw, err
}
if raw.StatusCode/100 != 2 {
return nil, raw, fmt.Errorf("send-email-code: status %d: %s", raw.StatusCode, string(resp))
}
var out SendEmailCodeResponse
if err := json.Unmarshal(resp, &out); err != nil {
return nil, raw, err
}
return &out, raw, nil
}
// ConfirmEmailCode confirms a challenge and registers a device
// session.
func (c *PublicRESTClient) ConfirmEmailCode(ctx context.Context, challengeID, code, clientPublicKey, timeZone string) (*ConfirmEmailCodeResponse, *http.Response, error) {
body := map[string]any{
"challenge_id": challengeID,
"code": code,
"client_public_key": clientPublicKey,
"time_zone": timeZone,
}
resp, raw, err := c.do(ctx, http.MethodPost, "/api/v1/public/auth/confirm-email-code", body)
if err != nil {
return nil, raw, err
}
if raw.StatusCode/100 != 2 {
return nil, raw, fmt.Errorf("confirm-email-code: status %d: %s", raw.StatusCode, string(resp))
}
var out ConfirmEmailCodeResponse
if err := json.Unmarshal(resp, &out); err != nil {
return nil, raw, err
}
return &out, raw, nil
}
func (c *PublicRESTClient) do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
return c.doWithHeaders(ctx, method, path, body, nil)
}
func (c *PublicRESTClient) doWithHeaders(ctx context.Context, method, path string, body any, headers http.Header) ([]byte, *http.Response, error) {
var reader io.Reader
if body != nil {
buf, err := json.Marshal(body)
if err != nil {
return nil, nil, err
}
reader = bytes.NewReader(buf)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
if err != nil {
return nil, nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
for k, vs := range headers {
for _, v := range vs {
req.Header.Add(k, v)
}
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp, err
}
return raw, resp, nil
}
// BackendInternalClient hits backend's `/api/v1/internal/*` endpoints
// directly. Per ARCHITECTURE.md the trust boundary is the network, so
// integration tests act as a trusted gateway-equivalent caller.
type BackendInternalClient struct {
BaseURL string
HTTP *http.Client
}
// NewBackendInternalClient targets backend's HTTP base URL.
func NewBackendInternalClient(baseURL string) *BackendInternalClient {
return &BackendInternalClient{
BaseURL: strings.TrimRight(baseURL, "/"),
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// Do issues an internal request. The caller decodes the body.
func (c *BackendInternalClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
var reader io.Reader
if body != nil {
buf, err := json.Marshal(body)
if err != nil {
return nil, nil, err
}
reader = bytes.NewReader(buf)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
if err != nil {
return nil, nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp, err
}
return raw, resp, nil
}
// BackendUserClient hits backend's `/api/v1/user/*` endpoints
// directly with `X-User-ID` set, mirroring what gateway does after
// authenticated traffic verification. Used by scenarios whose
// message_type is not registered in gateway's downstream router
// (lobby create, soft delete, etc.).
type BackendUserClient struct {
BaseURL string
UserID string
HTTP *http.Client
}
// NewBackendUserClient targets backend's HTTP base URL with userID
// pre-bound.
func NewBackendUserClient(baseURL, userID string) *BackendUserClient {
return &BackendUserClient{
BaseURL: strings.TrimRight(baseURL, "/"),
UserID: userID,
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// Do issues a user-scoped backend request.
func (c *BackendUserClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
var reader io.Reader
if body != nil {
buf, err := json.Marshal(body)
if err != nil {
return nil, nil, err
}
reader = bytes.NewReader(buf)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
if err != nil {
return nil, nil, err
}
req.Header.Set("X-User-ID", c.UserID)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp, err
}
return raw, resp, nil
}
// BackendAdminClient hits backend's admin surface directly with HTTP
// Basic Auth. Per ARCHITECTURE.md §14 the admin surface is on the
// backend HTTP listener (not gateway), so tests address it directly.
type BackendAdminClient struct {
BaseURL string
Username string
Password string
HTTP *http.Client
}
// NewBackendAdminClient targets backend's HTTP base URL with the
// supplied credentials.
func NewBackendAdminClient(baseURL, username, password string) *BackendAdminClient {
return &BackendAdminClient{
BaseURL: strings.TrimRight(baseURL, "/"),
Username: username,
Password: password,
HTTP: &http.Client{Timeout: 30 * time.Second},
}
}
// Do performs a request against an admin endpoint. The caller decodes
// the body. Returned http.Response is always non-nil on success.
func (c *BackendAdminClient) Do(ctx context.Context, method, path string, body any) ([]byte, *http.Response, error) {
var reader io.Reader
if body != nil {
buf, err := json.Marshal(body)
if err != nil {
return nil, nil, err
}
reader = bytes.NewReader(buf)
}
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, reader)
if err != nil {
return nil, nil, err
}
req.SetBasicAuth(c.Username, c.Password)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp, err
}
return raw, resp, nil
}