FEAT: Init BDD Tests
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user