Files
thamanyah/tests/suite_test.go
T
FahdShalhoub 6f9e04ee97
Build, Push and Deploy CMS / build-push-deploy (push) Successful in 2m35s
Build, Push and Deploy Discovery / build-push-deploy (push) Successful in 1m55s
Deploy Infrastructure / pulumi-up (push) Successful in 2s
FEAT: Discovery Video Service Subscription
2026-08-29 16:50:58 +03:00

210 lines
8.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
// discovery is the read side, set by the Background step that checks it is
// up. Nil in scenarios that never mention the catalogue.
discovery *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
// announcement is the message the catalogue topic carried for this
// scenario's video, once a step has waited for it.
announcement *catalogueAnnouncement
// catalogued is the read side's own copy of the video, once a step has
// waited for it to arrive.
catalogued *catalogueVideoBody
}
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
}
// categoryName resolves a category id back to its name, for asserting on a
// message that names categories rather than numbering them.
func (w *world) categoryName(t gobdd.StepTest, id int16) string {
w.categoryID(t, "other") // ensures the list is loaded
for name, known := range w.categoriesByName {
if known == id {
return name
}
}
t.Fatalf("the CMS knows no category with id %d; it offers %v", id, w.categoriesByName)
return ""
}
// 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(`^the Discovery API is available$`, theDiscoveryAPIIsAvailable)
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 being transcoded under categories "(.*)"$`, registerVideoBeingTranscodedUnder)
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(`^the catalogue is told about the video$`, theCatalogueIsToldAboutTheVideo)
suite.AddStep(`^the catalogue is told nothing about the video$`, theCatalogueIsToldNothingAboutTheVideo)
suite.AddStep(`^the announcement carries the video's title, playback URL and categories$`, theAnnouncementCarriesTheVideosDetails)
suite.AddStep(`^the announcement files it under "(.*)"$`, theAnnouncementFilesItUnder)
suite.AddStep(`^the catalogue eventually holds that video$`, theCatalogueEventuallyHoldsThatVideo)
suite.AddStep(`^the catalogue does not hold that video$`, theCatalogueDoesNotHoldThatVideo)
suite.AddStep(`^the catalogue eventually holds that video with no playback URL$`, theCatalogueEventuallyHoldsThatVideoWithNoPlaybackURL)
suite.AddStep(`^the catalogue's copy carries its title, playback URL and categories$`, theCataloguesCopyCarriesTheDetails)
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()
}