b650a121b5
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 <noreply@anthropic.com>
178 lines
5.5 KiB
Go
178 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|