Files
silo-server/internal/api/handlers/admin_intro_test.go

411 lines
13 KiB
Go

package handlers
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/Silo-Server/silo-server/internal/intromarkers"
"github.com/Silo-Server/silo-server/internal/markers"
"github.com/Silo-Server/silo-server/internal/models"
)
type fakeIntroAnalyzer struct {
started chan string
release chan struct{}
summary intromarkers.RunSummary
err error
}
func (f *fakeIntroAnalyzer) AnalyzeEpisode(ctx context.Context, episodeID string) (intromarkers.RunSummary, error) {
if f.started != nil {
f.started <- episodeID
}
if f.release != nil {
select {
case <-f.release:
case <-ctx.Done():
return intromarkers.RunSummary{}, ctx.Err()
}
}
if f.err != nil {
return intromarkers.RunSummary{}, f.err
}
if f.summary.FilesConsidered != 0 {
return f.summary, nil
}
return intromarkers.RunSummary{FilesConsidered: 1}, nil
}
type fakeIntroEligibility struct {
result *intromarkers.EpisodeIntroEligibility
err error
}
func (f fakeIntroEligibility) EpisodeIntroEligibility(context.Context, string) (*intromarkers.EpisodeIntroEligibility, error) {
if f.err != nil {
return nil, f.err
}
return f.result, nil
}
type fakeMarkerSettings struct {
values map[string]string
err error
}
func (f fakeMarkerSettings) Get(_ context.Context, key string) (string, error) {
if f.err != nil {
return "", f.err
}
return f.values[key], nil
}
type fakeAdminIntroFileResolver struct {
files []*models.MediaFile
err error
called chan string
}
func (f fakeAdminIntroFileResolver) GetByEpisodeID(_ context.Context, episodeID string) ([]*models.MediaFile, error) {
if f.called != nil {
f.called <- episodeID
}
if f.err != nil {
return nil, f.err
}
return f.files, nil
}
type fakeAdminIntroMarkerNotifier struct {
ch chan *models.MediaFile
}
func (n fakeAdminIntroMarkerNotifier) MarkersUpdated(_ context.Context, file *models.MediaFile) {
if n.ch == nil {
return
}
n.ch <- file
}
func TestAdminIntroRedetectQueuesAndDedupsInFlightEpisode(t *testing.T) {
analyzer := &fakeIntroAnalyzer{
started: make(chan string, 1),
release: make(chan struct{}),
}
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
first := httptest.NewRecorder()
router.ServeHTTP(first, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if first.Code != http.StatusAccepted {
t.Fatalf("expected first request 202, got %d: %s", first.Code, first.Body.String())
}
if status := decodeRedetectStatus(t, first); status != "queued" {
t.Fatalf("expected queued, got %q", status)
}
select {
case id := <-analyzer.started:
if id != "ep1" {
t.Fatalf("expected analyzer to start ep1, got %q", id)
}
case <-time.After(time.Second):
t.Fatal("analyzer did not start")
}
second := httptest.NewRecorder()
router.ServeHTTP(second, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if second.Code != http.StatusAccepted {
t.Fatalf("expected second request 202, got %d: %s", second.Code, second.Body.String())
}
if status := decodeRedetectStatus(t, second); status != "already_running" {
t.Fatalf("expected already_running, got %q", status)
}
close(analyzer.release)
}
func TestAdminIntroRedetectRejectsModesWithoutLocalAnalysis(t *testing.T) {
for _, tt := range []struct {
name string
mode string
}{
{name: "off", mode: string(markers.ModeOff)},
{name: "online", mode: string(markers.ModeOnline)},
} {
t.Run(tt.name, func(t *testing.T) {
analyzer := &fakeIntroAnalyzer{started: make(chan string, 1), release: make(chan struct{})}
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: tt.mode}}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusConflict {
t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
}
select {
case id := <-analyzer.started:
t.Fatalf("expected analyzer not to start, got %q", id)
case <-time.After(50 * time.Millisecond):
}
})
}
}
func TestAdminIntroRedetectRejectsNonEpisode(t *testing.T) {
handler := NewAdminIntroHandler(
&fakeIntroAnalyzer{started: make(chan string, 1), release: make(chan struct{})},
fakeIntroEligibility{err: intromarkers.ErrEpisodeNotFound},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/movie1/redetect-intro", nil))
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
}
}
func TestAdminIntroRedetectRejectsIntroDisabledLibrary(t *testing.T) {
handler := NewAdminIntroHandler(
&fakeIntroAnalyzer{started: make(chan string, 1), release: make(chan struct{})},
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: false,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusConflict {
t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
}
}
func TestAdminIntroRedetectRejectsEpisodeWithoutMediaFiles(t *testing.T) {
analyzer := &fakeIntroAnalyzer{started: make(chan string, 1), release: make(chan struct{})}
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: false,
IntroDetectionEnabled: false,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusConflict {
t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
}
select {
case id := <-analyzer.started:
t.Fatalf("expected analyzer not to start, got %q", id)
case <-time.After(50 * time.Millisecond):
}
}
func TestAdminIntroRedetectRejectsMissingSettings(t *testing.T) {
handler := NewAdminIntroHandler(
&fakeIntroAnalyzer{started: make(chan string, 1), release: make(chan struct{})},
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d: %s", rec.Code, rec.Body.String())
}
}
func TestAdminIntroRedetectNotifiesMarkedFilesAfterAnalyzerSuccess(t *testing.T) {
start := 12.0
end := 75.0
analyzer := &fakeIntroAnalyzer{started: make(chan string, 1)}
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
handler.FileResolver = fakeAdminIntroFileResolver{files: []*models.MediaFile{
{ID: 1, EpisodeID: "ep1", IntroStart: &start, IntroEnd: &end},
{ID: 2, EpisodeID: "ep1"},
}}
notifier := fakeAdminIntroMarkerNotifier{ch: make(chan *models.MediaFile, 1)}
handler.MarkerUpdateNotifier = notifier
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusAccepted {
t.Fatalf("expected request 202, got %d: %s", rec.Code, rec.Body.String())
}
select {
case id := <-analyzer.started:
if id != "ep1" {
t.Fatalf("expected analyzer to start ep1, got %q", id)
}
case <-time.After(time.Second):
t.Fatal("analyzer did not start")
}
select {
case notified := <-notifier.ch:
if notified.ID != 1 {
t.Fatalf("notified file ID = %d, want 1", notified.ID)
}
case <-time.After(time.Second):
t.Fatal("expected marker update notification")
}
select {
case notified := <-notifier.ch:
t.Fatalf("unexpected second notification: %#v", notified)
case <-time.After(25 * time.Millisecond):
}
}
func TestAdminIntroRedetectDoesNotNotifyFilesStillMissingMarkers(t *testing.T) {
analyzer := &fakeIntroAnalyzer{started: make(chan string, 1)}
resolverCalled := make(chan string, 1)
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
handler.FileResolver = fakeAdminIntroFileResolver{
files: []*models.MediaFile{{ID: 1, EpisodeID: "ep1"}},
called: resolverCalled,
}
notifier := fakeAdminIntroMarkerNotifier{ch: make(chan *models.MediaFile, 1)}
handler.MarkerUpdateNotifier = notifier
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusAccepted {
t.Fatalf("expected request 202, got %d: %s", rec.Code, rec.Body.String())
}
select {
case <-resolverCalled:
case <-time.After(time.Second):
t.Fatal("file resolver was not called")
}
select {
case notified := <-notifier.ch:
t.Fatalf("unexpected marker update: %#v", notified)
case <-time.After(25 * time.Millisecond):
}
}
func TestAdminIntroRedetectNotificationReloadFailureDoesNotFailRequest(t *testing.T) {
analyzer := &fakeIntroAnalyzer{started: make(chan string, 1)}
resolverCalled := make(chan string, 1)
handler := NewAdminIntroHandler(
analyzer,
fakeIntroEligibility{result: &intromarkers.EpisodeIntroEligibility{
EpisodeID: "ep1",
HasMediaFiles: true,
IntroDetectionEnabled: true,
}},
context.Background(),
nil,
)
handler.Settings = fakeMarkerSettings{values: map[string]string{markers.SettingMode: string(markers.ModeLocal)}}
handler.FileResolver = fakeAdminIntroFileResolver{
err: errors.New("reload failed"),
called: resolverCalled,
}
handler.MarkerUpdateNotifier = fakeAdminIntroMarkerNotifier{ch: make(chan *models.MediaFile, 1)}
router := chi.NewRouter()
router.Post("/admin/items/{id}/redetect-intro", handler.HandleRedetectEpisodeIntro)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/admin/items/ep1/redetect-intro", nil))
if rec.Code != http.StatusAccepted {
t.Fatalf("expected request 202, got %d: %s", rec.Code, rec.Body.String())
}
select {
case <-resolverCalled:
case <-time.After(time.Second):
t.Fatal("file resolver was not called")
}
}
func decodeRedetectStatus(t *testing.T, rec *httptest.ResponseRecorder) string {
t.Helper()
var response redetectIntroResponse
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatalf("decoding response: %v", err)
}
return response.Status
}