165 lines
4.5 KiB
Go
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
|
|
}
|