FEAT: Video Search In Discovery
Build, Push and Deploy Discovery / build-push-deploy (push) Successful in 1m57s
Build, Push and Deploy Discovery / build-push-deploy (push) Successful in 1m57s
This commit is contained in:
@@ -3,8 +3,10 @@ package tests
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -902,3 +904,316 @@ func theCatalogueEventuallyHoldsThatVideoWithNoPlaybackURL(t gobdd.StepTest, ctx
|
||||
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)
|
||||
}
|
||||
|
||||
// --- Catalogue search ------------------------------------------------------
|
||||
|
||||
// searchLimit is the page size the search scenarios ask for unless they are
|
||||
// about paging itself.
|
||||
const searchLimit = 20
|
||||
|
||||
// uniqueTitle appends a nonce to the title a scenario names.
|
||||
//
|
||||
// Scenarios write real rows and nothing cleans up after them, so a fixed title
|
||||
// accumulates a copy per run and "the search returns exactly this video" stops
|
||||
// meaning anything. The nonce keeps every run's video findable on its own
|
||||
// while the feature file still reads in plain words.
|
||||
func uniqueTitle(title string) string {
|
||||
return fmt.Sprintf("%s %s", title, strings.ToUpper(strconv.FormatInt(time.Now().UnixNano(), 36)))
|
||||
}
|
||||
|
||||
// theCatalogueHoldsAVideoTitled puts one video into the catalogue under a
|
||||
// title the scenario chose: register it, report its transcode finished, and
|
||||
// wait for the announcement to land on the read side.
|
||||
func theCatalogueHoldsAVideoTitled(t gobdd.StepTest, ctx gobdd.Context, title string) {
|
||||
catalogueVideoTitledUnder(t, ctx, title, "other")
|
||||
}
|
||||
|
||||
// catalogueVideoTitledUnder is the whole write-to-read round trip for one
|
||||
// video, which is what "the catalogue holds …" costs: cms issues the id, the
|
||||
// job completes, cms announces it, discovery ingests it.
|
||||
func catalogueVideoTitledUnder(t gobdd.StepTest, ctx gobdd.Context, title, categories string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
full := uniqueTitle(title)
|
||||
|
||||
requestUploadSlot(t, ctx, "searchable.mp4", "video/mp4")
|
||||
uploadTheFile(t, ctx)
|
||||
registerUploadedVideo(t, ctx, full, categories)
|
||||
|
||||
if w.last.status != 201 {
|
||||
t.Fatalf("could not register a video to search for: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
|
||||
w.searchTitle = full
|
||||
|
||||
mediaConvertReportsState(t, ctx, "COMPLETE")
|
||||
theCatalogueEventuallyHoldsThatVideo(t, ctx)
|
||||
}
|
||||
|
||||
// searchTheCatalogueForThatTitle searches for the exact title the scenario's
|
||||
// video was catalogued under, nonce included — the scenario says "that video's
|
||||
// title" precisely so it does not have to know about the nonce.
|
||||
func searchTheCatalogueForThatTitle(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.searchTitle) == "" {
|
||||
t.Fatalf("no video has been catalogued, so there is no title to search for")
|
||||
return
|
||||
}
|
||||
|
||||
searchTheCatalogue(t, w, searchRequest{Title: w.searchTitle, Limit: searchLimit})
|
||||
}
|
||||
|
||||
// searchTheCatalogue performs the query and decodes the page, leaving both the
|
||||
// raw exchange and the decoded results on the world so a "Then" step can
|
||||
// assert on either.
|
||||
func searchTheCatalogue(t gobdd.StepTest, w *world, request searchRequest) {
|
||||
if w.discovery == nil {
|
||||
w.discovery = newDiscoveryClient()
|
||||
}
|
||||
|
||||
result, err := w.discovery.queryJSON("/api/videos", request)
|
||||
if err != nil {
|
||||
t.Fatalf("could not search the catalogue: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.last = result
|
||||
w.results = searchResultsBody{}
|
||||
|
||||
// A rejected search has no page to decode; the scenario asserting the
|
||||
// rejection reads w.last instead.
|
||||
if result.status == 200 {
|
||||
if err := result.json(&w.results); err != nil {
|
||||
t.Fatalf("could not decode the search results: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func theSearchReturnsThatVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.last.status != 200 {
|
||||
t.Fatalf("the search did not succeed: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
if !w.results.holds(w.video.ID) {
|
||||
t.Errorf("the search for %q did not return video %q; it returned %v",
|
||||
w.searchTitle, w.video.ID, w.results.ids())
|
||||
}
|
||||
}
|
||||
|
||||
// searchTheCatalogueFor searches for a term the scenario spells out, rather
|
||||
// than for the title of the video it catalogued.
|
||||
func searchTheCatalogueFor(t gobdd.StepTest, ctx gobdd.Context, term string) {
|
||||
searchTheCatalogue(t, worldOf(t, ctx), searchRequest{Title: term, Limit: searchLimit})
|
||||
}
|
||||
|
||||
// theCatalogueHoldsAVideoTitledUnder is theCatalogueHoldsAVideoTitled for a
|
||||
// scenario that cares which categories the video is filed under.
|
||||
func theCatalogueHoldsAVideoTitledUnder(t gobdd.StepTest, ctx gobdd.Context, title, categories string) {
|
||||
catalogueVideoTitledUnder(t, ctx, title, categories)
|
||||
}
|
||||
|
||||
func searchTheCatalogueForThatTitleInCategory(t gobdd.StepTest, ctx gobdd.Context, category string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.searchTitle) == "" {
|
||||
t.Fatalf("no video has been catalogued, so there is no title to search for")
|
||||
return
|
||||
}
|
||||
|
||||
searchTheCatalogue(t, w, searchRequest{
|
||||
Title: w.searchTitle,
|
||||
Categories: []string{category},
|
||||
Limit: searchLimit,
|
||||
})
|
||||
}
|
||||
|
||||
func theSearchDoesNotReturnThatVideo(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.last.status != 200 {
|
||||
t.Fatalf("the search did not succeed: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
if w.results.holds(w.video.ID) {
|
||||
t.Errorf("the search returned video %q, which it should have left out; it returned %v",
|
||||
w.video.ID, w.results.ids())
|
||||
}
|
||||
}
|
||||
|
||||
// searchTheCatalogueForThatTitleInCategories is the several-categories form,
|
||||
// for pinning that they mean "any of" rather than "all of".
|
||||
func searchTheCatalogueForThatTitleInCategories(t gobdd.StepTest, ctx gobdd.Context, categories string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.searchTitle) == "" {
|
||||
t.Fatalf("no video has been catalogued, so there is no title to search for")
|
||||
return
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
for _, name := range strings.Split(categories, ",") {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
searchTheCatalogue(t, w, searchRequest{
|
||||
Title: w.searchTitle,
|
||||
Categories: names,
|
||||
Limit: searchLimit,
|
||||
})
|
||||
}
|
||||
|
||||
// theCatalogueHoldsVideosTitled catalogues several videos under one shared
|
||||
// title, which is what a paging scenario needs: enough matches to fill more
|
||||
// than one page.
|
||||
//
|
||||
// The nonce is generated once and shared, rather than per video, so that
|
||||
// searching for it matches this scenario's videos and no others. Runs leave
|
||||
// their videos behind, so a term that also matched a previous run's would make
|
||||
// "the search returns 2 videos" mean nothing.
|
||||
func theCatalogueHoldsVideosTitled(t gobdd.StepTest, ctx gobdd.Context, count int, title string) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
w.searchTitle = uniqueTitle(title)
|
||||
w.cohort = nil
|
||||
w.pageSeen = nil
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
requestUploadSlot(t, ctx, "searchable.mp4", "video/mp4")
|
||||
uploadTheFile(t, ctx)
|
||||
registerUploadedVideo(t, ctx, w.searchTitle, "other")
|
||||
|
||||
if w.last.status != 201 {
|
||||
t.Fatalf("could not register video %d of %d to page through: %s", i+1, count, w.last.summary())
|
||||
return
|
||||
}
|
||||
|
||||
mediaConvertReportsState(t, ctx, "COMPLETE")
|
||||
theCatalogueEventuallyHoldsThatVideo(t, ctx)
|
||||
|
||||
w.cohort = append(w.cohort, w.video.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func searchTheCatalogueForThatTitleAPageAtATime(t gobdd.StepTest, ctx gobdd.Context, size int) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
w.pageSeen = nil
|
||||
searchTheCatalogue(t, w, searchRequest{Title: w.searchTitle, Limit: size})
|
||||
recordPage(w)
|
||||
}
|
||||
|
||||
// followTheCursor asks for the page after the one just returned, with the same
|
||||
// term and page size — a cursor says where to carry on from, not what to look
|
||||
// for.
|
||||
func followTheCursor(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.results.NextCursor == "" {
|
||||
t.Fatalf("the last page offered no cursor, so there is no next page to follow")
|
||||
return
|
||||
}
|
||||
|
||||
searchTheCatalogue(t, w, searchRequest{
|
||||
Title: w.searchTitle,
|
||||
Limit: len(w.results.Videos),
|
||||
Cursor: w.results.NextCursor,
|
||||
})
|
||||
recordPage(w)
|
||||
}
|
||||
|
||||
// recordPage remembers what a page held, so a later step can check the pages
|
||||
// tile the results.
|
||||
func recordPage(w *world) {
|
||||
w.pageSeen = append(w.pageSeen, w.results.ids()...)
|
||||
}
|
||||
|
||||
func theSearchReturnsNVideos(t gobdd.StepTest, ctx gobdd.Context, count int) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.last.status != 200 {
|
||||
t.Fatalf("the search did not succeed: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
if len(w.results.Videos) != count {
|
||||
t.Errorf("expected %d videos in the page, got %d: %v", count, len(w.results.Videos), w.results.ids())
|
||||
}
|
||||
}
|
||||
|
||||
func theSearchOffersACursor(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if strings.TrimSpace(w.results.NextCursor) == "" {
|
||||
t.Errorf("the page offered no cursor, so there is no way to ask for the next one")
|
||||
}
|
||||
}
|
||||
|
||||
func theSearchOffersNoFurtherCursor(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.results.NextCursor != "" {
|
||||
t.Errorf("the last page still offered cursor %q, so a reader cannot tell they have reached the end",
|
||||
w.results.NextCursor)
|
||||
}
|
||||
}
|
||||
|
||||
// thePagesTileTheCohort checks the pages between them held each of the
|
||||
// scenario's videos exactly once — the property a cursor exists to give, and
|
||||
// the one an offset loses as soon as the catalogue is written to.
|
||||
func thePagesTileTheCohort(t gobdd.StepTest, ctx gobdd.Context) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
seen := map[string]int{}
|
||||
for _, id := range w.pageSeen {
|
||||
seen[id]++
|
||||
}
|
||||
|
||||
for _, id := range w.cohort {
|
||||
switch seen[id] {
|
||||
case 1:
|
||||
case 0:
|
||||
t.Errorf("video %q was catalogued but appeared in none of the pages; the pages held %v",
|
||||
id, w.pageSeen)
|
||||
default:
|
||||
t.Errorf("video %q appeared in %d pages; a video should be on exactly one", id, seen[id])
|
||||
}
|
||||
}
|
||||
|
||||
if len(w.pageSeen) != len(w.cohort) {
|
||||
t.Errorf("the pages held %d videos between them, but %d were catalogued: %v",
|
||||
len(w.pageSeen), len(w.cohort), w.pageSeen)
|
||||
}
|
||||
}
|
||||
|
||||
func searchTheCatalogueFromCursor(t gobdd.StepTest, ctx gobdd.Context, cursor string) {
|
||||
w := worldOf(t, ctx)
|
||||
searchTheCatalogue(t, w, searchRequest{
|
||||
Title: w.searchTitle,
|
||||
Limit: searchLimit,
|
||||
Cursor: cursor,
|
||||
})
|
||||
}
|
||||
|
||||
// searchTheWholeCatalogue searches with no term and no categories, which is
|
||||
// how a reader browses rather than searches.
|
||||
func searchTheWholeCatalogue(t gobdd.StepTest, ctx gobdd.Context, size int) {
|
||||
searchTheCatalogue(t, worldOf(t, ctx), searchRequest{Limit: size})
|
||||
}
|
||||
|
||||
func theSearchReturnsAtMostNVideos(t gobdd.StepTest, ctx gobdd.Context, most int) {
|
||||
w := worldOf(t, ctx)
|
||||
|
||||
if w.last.status != 200 {
|
||||
t.Fatalf("the search did not succeed: %s", w.last.summary())
|
||||
return
|
||||
}
|
||||
if len(w.results.Videos) > most {
|
||||
t.Errorf("the search returned %d videos, more than the %d it should cap at",
|
||||
len(w.results.Videos), most)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user