273 lines
7.7 KiB
Go
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 gRPC 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
|
|
}
|