175 lines
6.9 KiB
Go
175 lines
6.9 KiB
Go
package tests
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-bdd/gobdd"
|
|
)
|
|
|
|
// world is the state one scenario accumulates as its steps run: the exchange
|
|
// the last step performed, and the upload slot it is working with.
|
|
//
|
|
// It is stored in the gobdd context behind worldKey and handed out as a
|
|
// pointer, because gobdd clones the context between the Background steps and
|
|
// the scenario steps — a value would leave the Background's work behind.
|
|
type world struct {
|
|
client *client
|
|
|
|
// last is the most recent exchange with the CMS. Every "Then the request
|
|
// …" step reads it.
|
|
last response
|
|
|
|
// The upload slot handed out by POST /api/videos/presign, and whether the
|
|
// file has been PUT to it.
|
|
slot presignBody
|
|
slotType string
|
|
slotFile string
|
|
uploaded bool
|
|
|
|
// sent is the last registration body posted to POST /api/videos, and video
|
|
// is the record that came back from it.
|
|
sent registerRequest
|
|
video videoBody
|
|
|
|
// categoriesByName caches GET /api/categories so scenarios can name a
|
|
// category instead of hard-coding the id the seed migration happened to
|
|
// give it.
|
|
categoriesByName map[string]int16
|
|
}
|
|
|
|
type worldKey struct{}
|
|
|
|
func worldOf(t gobdd.StepTest, ctx gobdd.Context) *world {
|
|
value, err := ctx.Get(worldKey{})
|
|
if err != nil {
|
|
t.Fatalf("no world in the scenario context: %s", err)
|
|
return nil
|
|
}
|
|
|
|
w, ok := value.(*world)
|
|
if !ok {
|
|
t.Fatalf("the scenario context holds a %T, not a *world", value)
|
|
return nil
|
|
}
|
|
|
|
return w
|
|
}
|
|
|
|
// categoryID resolves a category name to the id the API issues for it, so the
|
|
// feature file can say "documentary" instead of "1".
|
|
func (w *world) categoryID(t gobdd.StepTest, name string) int16 {
|
|
if w.categoriesByName == nil {
|
|
result, err := w.client.get("/api/categories")
|
|
if err != nil {
|
|
t.Fatalf("could not load the category list: %s", err)
|
|
return 0
|
|
}
|
|
if result.status != 200 {
|
|
t.Fatalf("could not load the category list: %s", result.summary())
|
|
return 0
|
|
}
|
|
|
|
var body categoriesBody
|
|
if err := result.json(&body); err != nil {
|
|
t.Fatalf("could not decode the category list: %s", err)
|
|
return 0
|
|
}
|
|
|
|
w.categoriesByName = make(map[string]int16, len(body.Categories))
|
|
for _, c := range body.Categories {
|
|
w.categoriesByName[c.Name] = c.ID
|
|
}
|
|
}
|
|
|
|
id, ok := w.categoriesByName[name]
|
|
if !ok {
|
|
t.Fatalf("the CMS knows no category named %q; it offers %v", name, w.categoriesByName)
|
|
return 0
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
// categoryIDs turns a comma-separated list of category names into the ids the
|
|
// register endpoint expects. An empty list stays empty — that is a scenario in
|
|
// its own right.
|
|
func (w *world) categoryIDs(t gobdd.StepTest, names string) []int16 {
|
|
ids := []int16{}
|
|
for _, name := range strings.Split(names, ",") {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
ids = append(ids, w.categoryID(t, name))
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// TestVideoUpload runs features/*.feature against a live CMS.
|
|
//
|
|
// The suite is black-box on purpose: it speaks only HTTP, so the same
|
|
// scenarios run against the docker-compose stack and against a deployed
|
|
// environment. Point it somewhere else with CMS_BASE_URL.
|
|
func TestVideoUpload(t *testing.T) {
|
|
c := newClient()
|
|
|
|
// Skip rather than fail when nothing is serving: a bare `go test ./...`
|
|
// in a fresh checkout should not go red because the stack is down.
|
|
if _, err := c.get("/health"); err != nil {
|
|
t.Skipf("no CMS at %s (%s) — start one with `docker compose up` from the repo root, "+
|
|
"or set CMS_BASE_URL to point at a running instance", c.base, err)
|
|
}
|
|
|
|
suite := gobdd.NewSuite(t,
|
|
gobdd.WithFeaturesPath("features/*.feature"),
|
|
gobdd.WithBeforeScenario(func(ctx gobdd.Context) {
|
|
ctx.Set(worldKey{}, &world{client: newClient()})
|
|
}),
|
|
)
|
|
|
|
// Given — the stack is up, and steps that arrange state a later When acts on.
|
|
suite.AddStep(`^the CMS API is available$`, theAPIIsAvailable)
|
|
suite.AddStep(`^I have requested an upload slot for "(.*)" of type "(.*)"$`, requestUploadSlot)
|
|
suite.AddStep(`^I have uploaded the file to the upload URL$`, uploadTheFile)
|
|
suite.AddStep(`^I have registered the uploaded video titled "(.*)" under categories "(.*)"$`, registerUploadedVideo)
|
|
|
|
// When — the action under test.
|
|
suite.AddStep(`^I ask the CMS for the list of categories$`, askForCategories)
|
|
suite.AddStep(`^I request an upload slot for "(.*)" of type "(.*)"$`, requestUploadSlot)
|
|
suite.AddStep(`^I upload the file to the upload URL$`, uploadTheFile)
|
|
suite.AddStep(`^I send malformed JSON to the upload slot endpoint$`, sendMalformedJSON)
|
|
suite.AddStep(`^I ask the CMS for that video$`, askForThatVideo)
|
|
suite.AddStep(`^I ask the CMS for the video with id "(.*)"$`, askForVideoWithID)
|
|
suite.AddStep(`^I have registered a video that is being transcoded$`, registerVideoBeingTranscoded)
|
|
suite.AddStep(`^I have registered a video$`, registerVideoBeingTranscoded)
|
|
suite.AddStep(`^I ask the CMS for that video with its id written (.*)$`, askForThatVideoWrittenAs)
|
|
suite.AddStep(`^MediaConvert reports that the job reached "(.*)"$`, mediaConvertReportsState)
|
|
suite.AddStep(`^MediaConvert reports that the job reached "(.*)" without a playlist$`, mediaConvertReportsStateWithoutPlaylist)
|
|
suite.AddStep(`^MediaConvert reports that the job "(.*)" reached "(.*)"$`, mediaConvertReportsStateForJob)
|
|
suite.AddStep(`^the video eventually has status "(.*)"$`, theVideoEventuallyHasStatus)
|
|
suite.AddStep(`^the video keeps status "(.*)"$`, theVideoKeepsStatus)
|
|
suite.AddStep(`^the video has a playback URL for its HLS playlist$`, theVideoHasAPlaybackURL)
|
|
suite.AddStep(`^the video has no playback URL$`, theVideoHasNoPlaybackURL)
|
|
suite.AddStep(`^I register the uploaded video titled "(.*)" under categories "(.*)"$`, registerUploadedVideo)
|
|
suite.AddStep(`^I register the uploaded video titled "(.*)" under category id (\d+)$`, registerUnderCategoryID)
|
|
suite.AddStep(`^I register a video titled "(.*)" with the key "(.*)" under categories "(.*)"$`, registerWithKey)
|
|
|
|
// Then — assertions on the last exchange.
|
|
suite.AddStep(`^the request succeeds with status (\d+)$`, theRequestSucceedsWith)
|
|
suite.AddStep(`^the request is rejected with status (\d+)$`, theRequestIsRejectedWith)
|
|
suite.AddStep(`^the problem title is "(.*)"$`, theProblemTitleIs)
|
|
suite.AddStep(`^the category list is not empty$`, theCategoryListIsNotEmpty)
|
|
suite.AddStep(`^every category has an id and a name$`, everyCategoryHasAnIDAndAName)
|
|
suite.AddStep(`^I am given an upload URL$`, iAmGivenAnUploadURL)
|
|
suite.AddStep(`^the storage key is under "(.*)" and ends with "(.*)"$`, theStorageKeyIs)
|
|
suite.AddStep(`^the video is registered with status "(.*)"$`, theVideoIsRegisteredWithStatus)
|
|
suite.AddStep(`^the video has an id$`, theVideoHasAnID)
|
|
suite.AddStep(`^the video is stored under the key from the upload slot$`, theVideoIsStoredUnderTheSlotKey)
|
|
suite.AddStep(`^the video has a transcoding job id$`, theVideoHasATranscodingJobID)
|
|
suite.AddStep(`^the video is filed under categories "(.*)"$`, theVideoIsFiledUnder)
|
|
suite.AddStep(`^the video keeps the metadata I sent$`, theVideoKeepsTheMetadataISent)
|
|
|
|
suite.Run()
|
|
}
|