Files
thamanyah/tests/client_test.go
T
2026-08-27 18:42:35 +03:00

165 lines
4.5 KiB
Go

package tests
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// defaultBaseURL is where docker-compose publishes cms on the host.
const defaultBaseURL = "http://localhost:8081"
// baseURL is the CMS the scenarios are run against. Override it to point the
// suite at a deployed environment instead of the local compose stack.
func baseURL() string {
if v := strings.TrimSpace(os.Getenv("CMS_BASE_URL")); v != "" {
return strings.TrimRight(v, "/")
}
return defaultBaseURL
}
// response is one HTTP exchange, kept whole so a failing assertion can print
// the body it actually got rather than just a status code.
type response struct {
status int
contentType string
body []byte
}
// json decodes the body into dest. It returns an error rather than failing the
// step so callers can report the raw body alongside the decode failure.
func (r response) json(dest any) error {
if err := json.Unmarshal(r.body, dest); err != nil {
return fmt.Errorf("%w (body: %s)", err, r.summary())
}
return nil
}
func (r response) summary() string {
const max = 512
body := strings.TrimSpace(string(r.body))
if len(body) > max {
body = body[:max] + "…"
}
if body == "" {
body = "<empty>"
}
return fmt.Sprintf("%d %s: %s", r.status, r.contentType, body)
}
// problem is the RFC 9457 body every error from the CMS carries.
type problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
}
type category struct {
ID int16 `json:"id"`
Name string `json:"name"`
}
type categoriesBody struct {
Categories []category `json:"categories"`
}
type presignBody struct {
UploadURL string `json:"uploadUrl"`
Key string `json:"key"`
}
type videoBody struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CategoryIDs []int16 `json:"categoryIds"`
Tags string `json:"tags"`
FileName string `json:"fileName"`
StorageKey string `json:"storageKey"`
MediaConvertJobID string `json:"mediaConvertJobId"`
Status string `json:"status"`
SizeBytes int64 `json:"sizeBytes"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// client talks to the CMS over plain HTTP. It deliberately does not use the
// AWS SDK: the presigned URL is handed out by the API and is a complete
// request on its own, so uploading is a plain PUT — exactly what a browser
// client does, and what these scenarios are meant to exercise.
type client struct {
base string
http *http.Client
}
func newClient() *client {
return &client{
base: baseURL(),
// Generous: the registration call queues a MediaConvert job inline.
http: &http.Client{Timeout: 30 * time.Second},
}
}
// postJSON marshals payload and posts it to a path on the CMS.
func (c *client) postJSON(path string, payload any) (response, error) {
encoded, err := json.Marshal(payload)
if err != nil {
return response{}, err
}
return c.send(http.MethodPost, c.base+path, "application/json", encoded)
}
// postRaw posts a body the caller has already framed, so a scenario can send
// something that is not valid JSON.
func (c *client) postRaw(path, contentType string, body []byte) (response, error) {
return c.send(http.MethodPost, c.base+path, contentType, body)
}
func (c *client) get(path string) (response, error) {
return c.send(http.MethodGet, c.base+path, "", nil)
}
// put uploads bytes to an absolute URL — the presigned one, which points at
// storage rather than at the CMS.
func (c *client) put(url, contentType string, body []byte) (response, error) {
return c.send(http.MethodPut, url, contentType, body)
}
func (c *client) send(method, url, contentType string, body []byte) (response, error) {
var reader io.Reader
if body != nil {
reader = bytes.NewReader(body)
}
request, err := http.NewRequest(method, url, reader)
if err != nil {
return response{}, err
}
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
result, err := c.http.Do(request)
if err != nil {
return response{}, fmt.Errorf("%s %s: %w", method, url, err)
}
defer result.Body.Close()
responseBody, err := io.ReadAll(result.Body)
if err != nil {
return response{}, fmt.Errorf("%s %s: reading body: %w", method, url, err)
}
return response{
status: result.StatusCode,
contentType: result.Header.Get("Content-Type"),
body: responseBody,
}, nil
}