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