package app import ( "bytes" "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" ) func prefetchFixtureApp(t *testing.T) (*UIApp, *playbackSession) { t.Helper() timer := time.AfterFunc(time.Hour, func() {}) t.Cleanup(func() { timer.Stop() }) session := &playbackSession{id: "fixture", run: 4, currentIndex: 1, state: "ended", timer: timer, tasks: []Task{{Index: 1}, {Index: 2}, {Index: 3}}} app := &UIApp{playbacks: map[string]*playbackSession{"fixture": session}, downloader: rankingTestDownloader(t, func(*http.Request) (*http.Response, error) { t.Error("cache hit unexpectedly requested an upstream resource") return nil, errors.New("unexpected network request") })} app.cfg = app.downloader.cfg session.viewer = fixtureViewer(app) session.viewer.retain() t.Cleanup(app.closePlaybacks) return app, session } func prefetchRequest(app *UIApp, body string) *httptest.ResponseRecorder { request := httptest.NewRequest(http.MethodPost, "http://localhost/api/ui/playback/prefetch", strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") writer := httptest.NewRecorder() app.handlePlaybackPrefetch(writer, viewerFixtureRequest(app, request)) return writer } func TestPlaybackPrefetchReusesEncodedBytesAndRun(t *testing.T) { app, session := prefetchFixtureApp(t) cache := newPlaybackPrefetch(2, session.run) session.prefetch = cache cache.Header().Set("Content-Type", "video/mp4") cache.Header().Set("X-Playback-Duration", "12.500") cache.Header().Set("X-Playback-Source", "online") fixture := bytes.Repeat([]byte("locally-generated-encoded-fixture"), 4000) if _, err := cache.Write(fixture); err != nil { t.Fatal(err) } cache.finish(nil) writer := httptest.NewRecorder() app.handlePlaybackStream(writer, viewerFixtureRequest(app, httptest.NewRequest(http.MethodGet, "http://localhost/api/ui/playback/stream?session=fixture&episode=2", nil))) if writer.Code != 200 || !bytes.Equal(writer.Body.Bytes(), fixture) || writer.Header().Get("X-Playback-Prefetched") != "1" || writer.Header().Get("X-Playback-Run") != "5" { t.Fatal("cached episode was restarted or corrupted") } view, _ := app.playbackStatus("fixture", false) if view.State != "ended" || view.Run != 5 || view.Duration != 12.5 || session.currentIndex != 2 || session.prefetch != nil { t.Fatal("cached stream did not become the active episode") } } func TestPlaybackPrefetchBoundsBufferAndCancelsProducer(t *testing.T) { cache := newPlaybackPrefetch(2, 1) cache.chunks = make(chan []byte, 2) finished := make(chan error, 1) go func() { _, err := cache.Write(make([]byte, 4*playbackPrefetchChunkSize)) cache.finish(err) finished <- err }() deadline := time.After(time.Second) for len(cache.chunks) < 2 { select { case <-deadline: t.Fatal("producer did not fill the bounded cache") default: time.Sleep(time.Millisecond) } } select { case <-finished: t.Fatal("producer exceeded its buffer capacity") default: } cache.cancel() select { case err := <-finished: if !errors.Is(err, context.Canceled) { t.Fatal("producer ignored cancellation", err) } case <-time.After(time.Second): t.Fatal("canceled producer stayed blocked") } if len(cache.chunks) > 2 { t.Fatal("unbounded cache") } } func TestPlaybackPrefetchFailureFallsBackWithoutLeakingJSON(t *testing.T) { cache := newPlaybackPrefetch(2, 1) defer cache.cancel() cache.WriteHeader(http.StatusBadGateway) _, _ = cache.Write([]byte(`{"error":"fixture failed"}`)) cache.finish(errors.New("fixture failed")) writer := httptest.NewRecorder() used, err := cache.serve(context.Background(), writer, 2, func(float64) { t.Error("failed cache became ready") }) if used || err != nil || writer.Body.Len() != 0 || writer.Header().Get("Content-Type") != "" { t.Fatal("failed prefetch must leave the response available for normal playback") } } func TestPlaybackPrefetchValidatesEpisodeAndStaleCommands(t *testing.T) { app, session := prefetchFixtureApp(t) for _, body := range []string{ `{"session":"fixture","episode":3,"run":4,"version":1}`, `{"session":"fixture","episode":0,"run":4,"version":1}`, `{"session":"fixture","episode":2,"run":3,"version":1}`, `{"session":"fixture","episode":2,"run":4,"version":0}`, } { if result := prefetchRequest(app, body); result.Code < 400 { t.Fatal("accepted an invalid prefetch request", body) } } session.state = "streaming" if result := prefetchRequest(app, `{"session":"fixture","episode":2,"run":4,"version":1}`); result.Code != 409 { t.Fatal("prefetch competed with a current stream") } session.state = "ended" cache := newPlaybackPrefetch(2, 4) session.prefetch = cache if result := prefetchRequest(app, `{"session":"fixture","cancel":true,"run":4,"version":3}`); result.Code != 200 { t.Fatal("could not turn prefetch off") } if cache.ctx.Err() == nil || session.prefetch != nil { t.Fatal("disabled prefetch kept its producer") } if result := prefetchRequest(app, `{"session":"fixture","episode":2,"run":4,"version":2}`); result.Code != 409 { t.Fatal("late start undid a newer cancellation") } cache.finish(context.Canceled) } func TestPlaybackCloseAndExpiryCancelPrefetch(t *testing.T) { for _, mode := range []string{"close", "expire", "all"} { t.Run(mode, func(t *testing.T) { app, session := prefetchFixtureApp(t) cache := newPlaybackPrefetch(2, 4) session.prefetch = cache switch mode { case "close": app.closePlayback("fixture") case "expire": app.expirePlayback("fixture") case "all": app.closePlaybacks() } if cache.ctx.Err() == nil { t.Fatal("closed session retained its prefetch") } cache.finish(context.Canceled) }) } }