FEAT: Discovery Video Service Subscription
This commit is contained in:
@@ -422,6 +422,25 @@ func registerVideoBeingTranscoded(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// registerVideoBeingTranscodedUnder is registerVideoBeingTranscoded for a
|
||||
// scenario that cares which categories the video is filed under.
|
||||
func registerVideoBeingTranscodedUnder(t gobdd.StepTest, ctx gobdd.Context, categories string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
requestUploadSlot(t, ctx, "transcoding.mp4", "video/mp4")
|
||||
uploadTheFile(t, ctx)
|
||||
registerUploadedVideo(t, ctx, "A Video In Several Categories", categories)
|
||||
|
||||
if w.last.status != 201 {
|
||||
t.Fatalf("could not register a video to transcode: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(w.video.MediaConvertJobID) == "" {
|
||||
t.Fatalf("the registered video carries no transcoding job id, so no event could name it")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func mediaConvertReportsState(t gobdd.StepTest, ctx gobdd.Context, state string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
@@ -581,3 +600,305 @@ func askForThatVideoWrittenAs(t gobdd.StepTest, ctx gobdd.Context, form string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the catalogue queue ----------------------------------------------------
|
||||
|
||||
// announcementTimeout bounds how long a scenario waits for the announcement.
|
||||
// It is the status timeout's sibling: the publish happens in the same consumer
|
||||
// pass as the status write, so a video that is ready and still unannounced
|
||||
// after this is not slow, it is unannounced.
|
||||
const announcementTimeout = 20 * time.Second
|
||||
|
||||
func theCatalogueIsToldAboutTheVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.video.ID) == "" {
|
||||
t.Fatalf("no video has been registered, so nothing could be announced")
|
||||
return
|
||||
}
|
||||
|
||||
background := context.Background()
|
||||
|
||||
reader, err := newCatalogueReader(background)
|
||||
if err != nil {
|
||||
t.Fatalf("could not reach the catalogue queue: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
announcement, err := reader.await(background, w.video.ID, announcementTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the catalogue queue: %s", err)
|
||||
return
|
||||
}
|
||||
if announcement == nil {
|
||||
t.Errorf("the catalogue was never told about video %q within %s", w.video.ID, announcementTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
w.announcement = announcement
|
||||
}
|
||||
|
||||
// announcementHoldWindow is how long "told nothing" watches for before it is
|
||||
// satisfied. The announcement rides in the same consumer pass as the status
|
||||
// write, and the status is already settled by the time this step runs, so a
|
||||
// message that is not here by now is not coming.
|
||||
const announcementHoldWindow = 5 * time.Second
|
||||
|
||||
func theCatalogueIsToldNothingAboutTheVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.video.ID) == "" {
|
||||
t.Fatalf("no video has been registered, so there is nothing to look for")
|
||||
return
|
||||
}
|
||||
|
||||
background := context.Background()
|
||||
|
||||
reader, err := newCatalogueReader(background)
|
||||
if err != nil {
|
||||
t.Fatalf("could not reach the catalogue queue: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
announcement, err := reader.await(background, w.video.ID, announcementHoldWindow)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the catalogue queue: %s", err)
|
||||
return
|
||||
}
|
||||
if announcement != nil {
|
||||
t.Errorf("the catalogue was told about video %q, which has nothing to play", w.video.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func theAnnouncementCarriesTheVideosDetails(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.announcement == nil {
|
||||
t.Fatalf("no announcement has been received, so there is nothing to inspect")
|
||||
return
|
||||
}
|
||||
|
||||
if w.announcement.Title != w.sent.Title {
|
||||
t.Errorf("the announcement calls the video %q, but it was registered as %q",
|
||||
w.announcement.Title, w.sent.Title)
|
||||
}
|
||||
|
||||
if want := playbackURLFor(w.slot.Key); w.announcement.PlaybackURL != want {
|
||||
t.Errorf("the announcement points a player at %q, but the output landed at %q",
|
||||
w.announcement.PlaybackURL, want)
|
||||
}
|
||||
|
||||
// Named, not numbered: a reader of the queue has no access to the category
|
||||
// ids cms issues, so the names are what make the message self-contained.
|
||||
want := make([]string, 0, len(w.sent.CategoryIDs))
|
||||
for _, id := range w.sent.CategoryIDs {
|
||||
want = append(want, w.categoryName(t, id))
|
||||
}
|
||||
|
||||
got := slices.Clone(w.announcement.Categories)
|
||||
slices.Sort(got)
|
||||
slices.Sort(want)
|
||||
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("the announcement files the video under %v, but it was registered under %v",
|
||||
w.announcement.Categories, want)
|
||||
}
|
||||
}
|
||||
|
||||
func theAnnouncementFilesItUnder(t gobdd.StepTest, ctx gobdd.Context, names string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.announcement == nil {
|
||||
t.Fatalf("no announcement has been received, so there is nothing to inspect")
|
||||
return
|
||||
}
|
||||
|
||||
want := []string{}
|
||||
for _, name := range strings.Split(names, ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
want = append(want, name)
|
||||
}
|
||||
}
|
||||
|
||||
got := slices.Clone(w.announcement.Categories)
|
||||
slices.Sort(got)
|
||||
slices.Sort(want)
|
||||
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("the announcement files the video under %v, but it was registered under %v",
|
||||
w.announcement.Categories, want)
|
||||
}
|
||||
}
|
||||
|
||||
// --- the catalogue, as discovery serves it ----------------------------------
|
||||
|
||||
// catalogueSettleTimeout bounds how long a scenario waits for the read side to
|
||||
// catch up. It is longer than the CMS's own status timeout because two hops
|
||||
// have to happen — cms consuming the job event and announcing, then discovery
|
||||
// consuming that announcement — each with its own long poll.
|
||||
const catalogueSettleTimeout = 30 * time.Second
|
||||
|
||||
func theDiscoveryAPIIsAvailable(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
w.discovery = newDiscoveryClient()
|
||||
|
||||
result, err := w.discovery.get("/health")
|
||||
if err != nil {
|
||||
t.Fatalf("no Discovery at %s (%s) — start one with `docker compose up` from the repo root, "+
|
||||
"or set DISCOVERY_BASE_URL to point at a running instance", w.discovery.base, err)
|
||||
return
|
||||
}
|
||||
if result.status != 200 {
|
||||
t.Fatalf("Discovery at %s is not healthy: %s", w.discovery.base, result.summary())
|
||||
}
|
||||
}
|
||||
|
||||
func theCatalogueEventuallyHoldsThatVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.video.ID) == "" {
|
||||
t.Fatalf("no video has been registered, so the catalogue could hold nothing")
|
||||
return
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(catalogueSettleTimeout)
|
||||
last := response{}
|
||||
for time.Now().Before(deadline) {
|
||||
result, err := w.discovery.get("/api/videos/" + w.video.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("could not ask the catalogue for the video: %s", err)
|
||||
return
|
||||
}
|
||||
last = result
|
||||
|
||||
if result.status == 200 {
|
||||
var body catalogueVideoBody
|
||||
if err := result.json(&body); err != nil {
|
||||
t.Fatalf("could not decode the catalogue's copy: %s", err)
|
||||
return
|
||||
}
|
||||
w.catalogued = &body
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(statusPollInterval)
|
||||
}
|
||||
|
||||
t.Errorf("the catalogue never held video %q within %s; asking for it still gives %s",
|
||||
w.video.ID, catalogueSettleTimeout, last.summary())
|
||||
}
|
||||
|
||||
func theCataloguesCopyCarriesTheDetails(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.catalogued == nil {
|
||||
t.Fatalf("the catalogue holds no copy of the video, so there is nothing to inspect")
|
||||
return
|
||||
}
|
||||
|
||||
if w.catalogued.ID != w.video.ID {
|
||||
t.Errorf("the catalogue filed the video as %q, but the CMS issued id %q",
|
||||
w.catalogued.ID, w.video.ID)
|
||||
}
|
||||
|
||||
if w.catalogued.Title != w.sent.Title {
|
||||
t.Errorf("the catalogue calls the video %q, but it was registered as %q",
|
||||
w.catalogued.Title, w.sent.Title)
|
||||
}
|
||||
|
||||
if want := playbackURLFor(w.slot.Key); w.catalogued.PlaybackURL != want {
|
||||
t.Errorf("the catalogue points a player at %q, but the output landed at %q",
|
||||
w.catalogued.PlaybackURL, want)
|
||||
}
|
||||
|
||||
want := make([]string, 0, len(w.sent.CategoryIDs))
|
||||
for _, id := range w.sent.CategoryIDs {
|
||||
want = append(want, w.categoryName(t, id))
|
||||
}
|
||||
|
||||
got := slices.Clone(w.catalogued.Categories)
|
||||
slices.Sort(got)
|
||||
slices.Sort(want)
|
||||
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("the catalogue files the video under %v, but it was registered under %v",
|
||||
w.catalogued.Categories, want)
|
||||
}
|
||||
}
|
||||
|
||||
// catalogueAbsenceWindow is how long "does not hold" watches before it is
|
||||
// satisfied. Held rather than sampled once: the read side is asynchronous, so
|
||||
// a video that is absent the instant after registration proves nothing — it
|
||||
// might simply not have been announced yet.
|
||||
const catalogueAbsenceWindow = 5 * time.Second
|
||||
|
||||
func theCatalogueDoesNotHoldThatVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.video.ID) == "" {
|
||||
t.Fatalf("no video has been registered, so there is nothing to look for")
|
||||
return
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(catalogueAbsenceWindow)
|
||||
for {
|
||||
result, err := w.discovery.get("/api/videos/" + w.video.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("could not ask the catalogue for the video: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Kept as the scenario's last exchange so the assertions that follow
|
||||
// can read the status and the problem body the usual way.
|
||||
w.last = result
|
||||
|
||||
if result.status != 404 {
|
||||
t.Errorf("the catalogue holds video %q, which the CMS never announced: %s",
|
||||
w.video.ID, result.summary())
|
||||
return
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
return
|
||||
}
|
||||
time.Sleep(statusPollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func theCatalogueEventuallyHoldsThatVideoWithNoPlaybackURL(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.video.ID) == "" {
|
||||
t.Fatalf("no video has been registered, so the catalogue could hold nothing")
|
||||
return
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(catalogueSettleTimeout)
|
||||
last := ""
|
||||
for time.Now().Before(deadline) {
|
||||
result, err := w.discovery.get("/api/videos/" + w.video.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("could not ask the catalogue for the video: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if result.status == 200 {
|
||||
var body catalogueVideoBody
|
||||
if err := result.json(&body); err != nil {
|
||||
t.Fatalf("could not decode the catalogue's copy: %s", err)
|
||||
return
|
||||
}
|
||||
if body.PlaybackURL == "" {
|
||||
w.catalogued = &body
|
||||
return
|
||||
}
|
||||
last = body.PlaybackURL
|
||||
}
|
||||
|
||||
time.Sleep(statusPollInterval)
|
||||
}
|
||||
|
||||
t.Errorf("the catalogue still points a player at %q for video %q after %s; the later "+
|
||||
"announcement carried no playlist and should have replaced it", last, w.video.ID, catalogueSettleTimeout)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user