From b650a121b53dcc8cc969e85cad1066d968503d7a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 6 May 2026 22:26:16 -0400 Subject: [PATCH] feat(server/m7-recurring-scans): admin schedule HTTP handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new endpoints under existing RequireAdmin middleware: - GET /api/admin/scan/schedule — returns config + computed next_scheduled_at (ISO 8601; null when mode='off') - PATCH /api/admin/scan/schedule — validates per-mode required fields, normalizes mode='off' to NULL the per-mode fields, writes via UpdateScanSchedule, calls scheduler.Refresh(), returns the updated record. server.New + api.Mount gain a *library.Scheduler parameter; handlers struct + Server struct gain the scheduler field. Test stubs (server_test.go, library_test.go's Mount call) updated to pass nil. Tests gated on MINSTREL_TEST_DATABASE_URL: GET default is mode=off with null next; PATCH daily produces a 03:00 next-fire; PATCH weekly without weekly_day → 400; PATCH off normalizes lingering per-mode fields to NULL; non-admin → 403. Co-Authored-By: Claude Sonnet 4.6 --- cmd/minstrel/main.go | 3 +- internal/api/admin_scan_schedule.go | 157 ++++++++++++++++++++ internal/api/admin_scan_schedule_test.go | 177 +++++++++++++++++++++++ internal/api/api.go | 6 +- internal/api/library_test.go | 2 +- internal/server/server.go | 7 +- internal/server/server_test.go | 12 +- 7 files changed, 351 insertions(+), 13 deletions(-) create mode 100644 internal/api/admin_scan_schedule.go create mode 100644 internal/api/admin_scan_schedule_test.go diff --git a/cmd/minstrel/main.go b/cmd/minstrel/main.go index b5ff5480..3562b08a 100644 --- a/cmd/minstrel/main.go +++ b/cmd/minstrel/main.go @@ -166,10 +166,9 @@ func run() error { scheduler := library.NewScheduler(pool, logger.With("component", "scheduler"), scanner, coverEnricher, scanCfg) scheduler.Start(ctx) - _ = scheduler // keep reachable for future wiring (Task 4) srv := server.New(logger, pool, scanner, subsonic.Config{ AllowPlaintextPassword: cfg.Subsonic.AllowPlaintextPassword, - }, cfg.Events, cfg.Recommendation, cfg.Storage.DataDir, cfg.Branding, coverEnricher, cfg.Library.CoverArtBackfillCap, coverSettings, scanner, scanCfg) + }, cfg.Events, cfg.Recommendation, cfg.Storage.DataDir, cfg.Branding, coverEnricher, cfg.Library.CoverArtBackfillCap, coverSettings, scanner, scanCfg, scheduler) playlists.StartSystemPlaylistCron(ctx, pool, logger.With("component", "system_playlist_cron")) httpServer := &http.Server{ Addr: cfg.Server.Address, diff --git a/internal/api/admin_scan_schedule.go b/internal/api/admin_scan_schedule.go new file mode 100644 index 00000000..aa3816f8 --- /dev/null +++ b/internal/api/admin_scan_schedule.go @@ -0,0 +1,157 @@ +package api + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/jackc/pgx/v5" + + "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" + "git.fabledsword.com/bvandeusen/minstrel/internal/library" +) + +type scanScheduleResp struct { + Mode string `json:"mode"` + IntervalHours *int `json:"interval_hours"` + TimeOfDay *string `json:"time_of_day"` + WeeklyDay *int `json:"weekly_day"` + NextScheduledAt *string `json:"next_scheduled_at"` +} + +type scanScheduleReq struct { + Mode string `json:"mode"` + IntervalHours *int `json:"interval_hours"` + TimeOfDay *string `json:"time_of_day"` + WeeklyDay *int `json:"weekly_day"` +} + +func (h *handlers) handleGetScanSchedule(w http.ResponseWriter, r *http.Request) { + q := dbq.New(h.pool) + row, err := q.GetScanSchedule(r.Context()) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + h.logger.Error("admin: scan_schedule singleton row missing") + writeErr(w, http.StatusInternalServerError, "server_error", "schedule row missing") + return + } + h.logger.Error("admin: get scan schedule", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") + return + } + + writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, h.scheduler.NextFire())) +} + +func (h *handlers) handlePatchScanSchedule(w http.ResponseWriter, r *http.Request) { + var req scanScheduleReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeErr(w, http.StatusBadRequest, "bad_body", "invalid JSON") + return + } + + if err := validateScheduleReq(&req); err != nil { + writeErr(w, http.StatusBadRequest, "validation", err.Error()) + return + } + + q := dbq.New(h.pool) + if err := q.UpdateScanSchedule(r.Context(), dbq.UpdateScanScheduleParams{ + Mode: req.Mode, + IntervalHours: pgInt32Ptr(req.IntervalHours), + TimeOfDay: req.TimeOfDay, + WeeklyDay: pgInt32Ptr(req.WeeklyDay), + }); err != nil { + h.logger.Error("admin: update scan schedule", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "update failed") + return + } + + h.scheduler.Refresh() + + row, err := q.GetScanSchedule(r.Context()) + if err != nil { + h.logger.Error("admin: re-read scan schedule", "err", err) + writeErr(w, http.StatusInternalServerError, "server_error", "re-read failed") + return + } + cfg := library.ScheduleConfig{Mode: row.Mode} + if row.IntervalHours != nil { + cfg.IntervalHours = int(*row.IntervalHours) + } + if row.TimeOfDay != nil { + cfg.TimeOfDay = *row.TimeOfDay + } + if row.WeeklyDay != nil { + cfg.WeeklyDay = int(*row.WeeklyDay) + } + next := cfg.NextFire(time.Now()) + + writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, next)) +} + +func validateScheduleReq(req *scanScheduleReq) error { + switch req.Mode { + case "off": + req.IntervalHours = nil + req.TimeOfDay = nil + req.WeeklyDay = nil + return nil + case "interval": + if req.IntervalHours == nil || *req.IntervalHours <= 0 { + return fmt.Errorf("interval_hours required and > 0 when mode=interval") + } + req.TimeOfDay = nil + req.WeeklyDay = nil + return nil + case "daily": + if req.TimeOfDay == nil || *req.TimeOfDay == "" { + return fmt.Errorf("time_of_day required when mode=daily") + } + req.IntervalHours = nil + req.WeeklyDay = nil + return nil + case "weekly": + if req.TimeOfDay == nil || *req.TimeOfDay == "" { + return fmt.Errorf("time_of_day required when mode=weekly") + } + if req.WeeklyDay == nil || *req.WeeklyDay < 1 || *req.WeeklyDay > 7 { + return fmt.Errorf("weekly_day required and in [1, 7] when mode=weekly") + } + req.IntervalHours = nil + return nil + default: + return fmt.Errorf("invalid mode %q (want off / interval / daily / weekly)", req.Mode) + } +} + +func scanScheduleRespFromRow(row dbq.GetScanScheduleRow, nextFire time.Time) scanScheduleResp { + resp := scanScheduleResp{Mode: row.Mode} + if row.IntervalHours != nil { + v := int(*row.IntervalHours) + resp.IntervalHours = &v + } + if row.TimeOfDay != nil { + v := *row.TimeOfDay + resp.TimeOfDay = &v + } + if row.WeeklyDay != nil { + v := int(*row.WeeklyDay) + resp.WeeklyDay = &v + } + if !nextFire.IsZero() { + s := nextFire.UTC().Format(time.RFC3339) + resp.NextScheduledAt = &s + } + return resp +} + +func pgInt32Ptr(src *int) *int32 { + if src == nil { + return nil + } + v := int32(*src) + return &v +} diff --git a/internal/api/admin_scan_schedule_test.go b/internal/api/admin_scan_schedule_test.go new file mode 100644 index 00000000..09f5aff8 --- /dev/null +++ b/internal/api/admin_scan_schedule_test.go @@ -0,0 +1,177 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/go-chi/chi/v5" + + "git.fabledsword.com/bvandeusen/minstrel/internal/auth" + "git.fabledsword.com/bvandeusen/minstrel/internal/library" +) + +func newAdminScheduleRouter(h *handlers) chi.Router { + r := chi.NewRouter() + r.Route("/api/admin", func(admin chi.Router) { + admin.Use(auth.RequireAdmin()) + admin.Get("/scan/schedule", h.handleGetScanSchedule) + admin.Patch("/scan/schedule", h.handlePatchScanSchedule) + }) + return r +} + +func TestAdminScanSchedule_Get_DefaultIsOff(t *testing.T) { + if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { + t.Skip("MINSTREL_TEST_DATABASE_URL not set") + } + h, pool := testHandlers(t) + s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{}) + h.scheduler = s + admin := seedUser(t, pool, "schedget", "pw", true) + + req := httptest.NewRequest(http.MethodGet, "/api/admin/scan/schedule", nil) + req = withUser(req, admin) + rec := httptest.NewRecorder() + newAdminScheduleRouter(h).ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + var resp scanScheduleResp + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Mode != "off" { + t.Errorf("Mode = %q, want off", resp.Mode) + } + if resp.NextScheduledAt != nil { + t.Errorf("NextScheduledAt = %v, want nil for mode=off", resp.NextScheduledAt) + } +} + +func TestAdminScanSchedule_Patch_DailyMode(t *testing.T) { + if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { + t.Skip("MINSTREL_TEST_DATABASE_URL not set") + } + h, pool := testHandlers(t) + s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{}) + h.scheduler = s + admin := seedUser(t, pool, "schedpatch", "pw", true) + + body := `{"mode":"daily","time_of_day":"03:00"}` + req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule", + bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + req = withUser(req, admin) + rec := httptest.NewRecorder() + newAdminScheduleRouter(h).ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + var resp scanScheduleResp + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Mode != "daily" { + t.Errorf("Mode = %q, want daily", resp.Mode) + } + if resp.TimeOfDay == nil || *resp.TimeOfDay != "03:00" { + t.Errorf("TimeOfDay = %v, want '03:00'", resp.TimeOfDay) + } + if resp.NextScheduledAt == nil { + t.Errorf("NextScheduledAt = nil, want a future ISO timestamp") + } else { + next, err := time.Parse(time.RFC3339, *resp.NextScheduledAt) + if err != nil { + t.Errorf("NextScheduledAt parse: %v", err) + } else if next.UTC().Hour() != 3 || next.UTC().Minute() != 0 { + t.Errorf("NextScheduledAt time = %v, want 03:00 UTC in some day", next) + } + } +} + +func TestAdminScanSchedule_Patch_WeeklyMissingDay_400(t *testing.T) { + if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { + t.Skip("MINSTREL_TEST_DATABASE_URL not set") + } + h, pool := testHandlers(t) + s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{}) + h.scheduler = s + admin := seedUser(t, pool, "schedweekly", "pw", true) + + body := `{"mode":"weekly","time_of_day":"03:00"}` + req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule", + bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + req = withUser(req, admin) + rec := httptest.NewRecorder() + newAdminScheduleRouter(h).ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Errorf("status = %d, want 400; body=%s", rec.Code, rec.Body.String()) + } +} + +func TestAdminScanSchedule_Patch_OffNormalizes(t *testing.T) { + if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { + t.Skip("MINSTREL_TEST_DATABASE_URL not set") + } + h, pool := testHandlers(t) + s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{}) + h.scheduler = s + admin := seedUser(t, pool, "schedoff", "pw", true) + + if _, err := pool.Exec(context.Background(), + `UPDATE scan_schedule SET mode='daily', time_of_day='03:00' WHERE id=true`, + ); err != nil { + t.Fatalf("seed: %v", err) + } + + body := `{"mode":"off","time_of_day":"03:00"}` + req := httptest.NewRequest(http.MethodPatch, "/api/admin/scan/schedule", + bytes.NewReader([]byte(body))) + req.Header.Set("Content-Type", "application/json") + req = withUser(req, admin) + rec := httptest.NewRecorder() + newAdminScheduleRouter(h).ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + + var timeOfDay *string + if err := pool.QueryRow(context.Background(), + `SELECT time_of_day FROM scan_schedule WHERE id=true`, + ).Scan(&timeOfDay); err != nil { + t.Fatalf("verify: %v", err) + } + if timeOfDay != nil { + t.Errorf("time_of_day = %v, want nil after mode=off normalize", timeOfDay) + } +} + +func TestAdminScanSchedule_NonAdmin_403(t *testing.T) { + if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" { + t.Skip("MINSTREL_TEST_DATABASE_URL not set") + } + h, pool := testHandlers(t) + s := library.NewScheduler(pool, h.logger, nil, nil, library.RunScanConfig{}) + h.scheduler = s + user := seedUser(t, pool, "schednonadmin", "pw", false) + + req := httptest.NewRequest(http.MethodGet, "/api/admin/scan/schedule", nil) + req = withUser(req, user) + rec := httptest.NewRecorder() + newAdminScheduleRouter(h).ServeHTTP(rec, req) + + if rec.Code != http.StatusForbidden { + t.Errorf("status = %d, want 403", rec.Code) + } +} diff --git a/internal/api/api.go b/internal/api/api.go index aab56661..8cda7a5c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -26,7 +26,7 @@ import ( // Mount attaches /api/* handlers to r. Public endpoints (login) are outside // RequireUser; everything else is gated by the middleware. The events writer // is shared with the Subsonic mount so /rest/scrobble feeds the same store. -func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer, recCfg config.RecommendationConfig, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverBackfillCap int, coverSettings *coverart.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, dataDir string) { +func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer, recCfg config.RecommendationConfig, lidarrCfg *lidarrconfig.Service, lidarrReqs *lidarrrequests.Service, lidarrQuar *lidarrquarantine.Service, tracksSvc *tracks.Service, playlistsSvc *playlists.Service, coverEnricher *coverart.Enricher, coverBackfillCap int, coverSettings *coverart.SettingsService, scanner *library.Scanner, scanCfg library.RunScanConfig, scheduler *library.Scheduler, dataDir string) { rng := rand.New(rand.NewSource(rand.Int63())) h := &handlers{ pool: pool, logger: logger, events: events, recCfg: recCfg, @@ -41,6 +41,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev coverSettings: coverSettings, scanner: scanner, scanCfg: scanCfg, + scheduler: scheduler, dataDir: dataDir, } @@ -115,6 +116,8 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev admin.Get("/scan/status", h.handleGetScanStatus) admin.Post("/scan/run", h.handleTriggerScan) + admin.Get("/scan/schedule", h.handleGetScanSchedule) + admin.Patch("/scan/schedule", h.handlePatchScanSchedule) admin.Get("/library/coverage", h.handleGetLibraryCoverage) @@ -152,5 +155,6 @@ type handlers struct { coverSettings *coverart.SettingsService scanner *library.Scanner scanCfg library.RunScanConfig + scheduler *library.Scheduler dataDir string } diff --git a/internal/api/library_test.go b/internal/api/library_test.go index 951d1bd5..46fd3e45 100644 --- a/internal/api/library_test.go +++ b/internal/api/library_test.go @@ -443,7 +443,7 @@ func TestRoutesRegisteredInMount(t *testing.T) { r := chi.NewRouter() w := playevents.NewWriter(h.pool, slog.New(slog.NewTextHandler(io.Discard, nil)), 30*time.Minute, 0.5, 30000) - Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverArtBackfillCap, h.coverSettings, h.scanner, h.scanCfg, h.dataDir) + Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverArtBackfillCap, h.coverSettings, h.scanner, h.scanCfg, nil, h.dataDir) paths := []string{ "/api/artists", diff --git a/internal/server/server.go b/internal/server/server.go index af477ac3..471745bf 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -77,10 +77,11 @@ type Server struct { CoverSettings *coverart.SettingsService LibraryScanner *library.Scanner ScanCfg library.RunScanConfig + Scheduler *library.Scheduler } -func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig, dataDir string, brandingCfg config.BrandingConfig, coverEnricher *coverart.Enricher, coverArtBackfillCap int, coverSettings *coverart.SettingsService, libraryScanner *library.Scanner, scanCfg library.RunScanConfig) *Server { - return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg, DataDir: dataDir, BrandingCfg: brandingCfg, CoverEnricher: coverEnricher, CoverArtBackfillCap: coverArtBackfillCap, CoverSettings: coverSettings, LibraryScanner: libraryScanner, ScanCfg: scanCfg} +func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig, dataDir string, brandingCfg config.BrandingConfig, coverEnricher *coverart.Enricher, coverArtBackfillCap int, coverSettings *coverart.SettingsService, libraryScanner *library.Scanner, scanCfg library.RunScanConfig, scheduler *library.Scheduler) *Server { + return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg, DataDir: dataDir, BrandingCfg: brandingCfg, CoverEnricher: coverEnricher, CoverArtBackfillCap: coverArtBackfillCap, CoverSettings: coverSettings, LibraryScanner: libraryScanner, ScanCfg: scanCfg, Scheduler: scheduler} } func (s *Server) Router() http.Handler { @@ -114,7 +115,7 @@ func (s *Server) Router() http.Handler { lidarrQuar := lidarrquarantine.NewService(s.Pool, lidarrCfg, lidarrClientFn) tracksSvc := tracks.NewService(s.Pool, s.Logger, lidarrUnmonitorAdapter{fn: lidarrClientFn}, s.DataDir) playlistsSvc := playlists.NewService(s.Pool, s.Logger, s.DataDir) - api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.DataDir) + api.Mount(r, s.Pool, s.Logger, writer, s.RecommendationCfg, lidarrCfg, lidarrReqs, lidarrQuar, tracksSvc, playlistsSvc, s.CoverEnricher, s.CoverArtBackfillCap, s.CoverSettings, s.LibraryScanner, s.ScanCfg, s.Scheduler, s.DataDir) // /api/admin/scan is the only admin route owned by the server package // (it needs the Scanner). Register it as a single inline-middleware // route — using r.Route("/api/admin", ...) here would create a second diff --git a/internal/server/server_test.go b/internal/server/server_test.go index e2f35947..0be2d436 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -24,7 +24,7 @@ import ( ) func TestHealthz(t *testing.T) { - s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close() @@ -73,7 +73,7 @@ func TestHealthz_IncludesMinClientVersion(t *testing.T) { } func TestRouter_ServesSPAAtRoot(t *testing.T) { - s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close() @@ -92,7 +92,7 @@ func TestRouter_ServesSPAAtRoot(t *testing.T) { } func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) { - s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close() @@ -111,7 +111,7 @@ func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) { } func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) { - s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close() @@ -130,7 +130,7 @@ func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) { } func TestRouter_RestPathNotSwallowedBySPA(t *testing.T) { - s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close() @@ -206,7 +206,7 @@ func TestRouter_AdminSubtreeNotShadowed(t *testing.T) { } s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), pool, - stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}) + stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{}, nil) ts := httptest.NewServer(s.Router()) defer ts.Close()