package sources import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "ymhut-box/server/unified-management/internal/config" "ymhut-box/server/unified-management/internal/db" ) func TestCheckOneTreatsRedirectToOKAsRedirected(t *testing.T) { target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) })) defer target.Close() redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, http.StatusFound) })) defer redirector.Close() cfg, store := testStore(t) service := NewService(cfg, store) item, err := store.UpsertSource(db.Source{ CategoryID: "test", CategoryName: "Test", SourceID: "redirect", Name: "Redirect", Method: "GET", APIURL: redirector.URL, TimeoutMS: 3000, CheckIntervalSec: 300, Enabled: true, ClientVisible: true, }) if err != nil { t.Fatal(err) } if err := service.CheckOne(context.Background(), item); err != nil { t.Fatal(err) } checked, err := store.GetSourceBySourceID("redirect") if err != nil { t.Fatal(err) } if checked.LastStatus != "redirected" { t.Fatalf("LastStatus = %q, want redirected", checked.LastStatus) } if !strings.Contains(checked.LastError, `"redirected":true`) { t.Fatalf("LastError does not contain redirect metadata: %s", checked.LastError) } if checked.ConsecutiveFailure != 0 { t.Fatalf("ConsecutiveFailure = %d, want 0", checked.ConsecutiveFailure) } } func TestQueueCheckAllUsesBackgroundContext(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) })) defer server.Close() cfg, store := testStore(t) service := NewService(cfg, store) if _, err := store.UpsertSource(db.Source{ CategoryID: "test", CategoryName: "Test", SourceID: "slow-ok", Name: "Slow OK", Method: "GET", APIURL: server.URL, TimeoutMS: 1000, CheckIntervalSec: 300, Enabled: true, ClientVisible: true, }); err != nil { t.Fatal(err) } job := service.QueueCheckAll() deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { current, ok := service.CheckJob(job.ID) if ok && current.Status == "completed" { if current.Stats["ok"] != 1 { t.Fatalf("stats = %#v, want one ok", current.Stats) } return } time.Sleep(20 * time.Millisecond) } t.Fatalf("job did not complete: %#v", job) } func TestSubscribeEventsBroadcastsToAllSubscribers(t *testing.T) { cfg, store := testStore(t) service := NewService(cfg, store) eventsA, unsubscribeA := service.SubscribeEvents() defer unsubscribeA() eventsB, unsubscribeB := service.SubscribeEvents() defer unsubscribeB() service.emit("source_check.completed", map[string]any{"jobId": "demo"}) assertEvent := func(name string, events <-chan Event) { t.Helper() select { case event := <-events: if event.Type != "source_check.completed" || event.Data["jobId"] != "demo" { t.Fatalf("%s received unexpected event: %#v", name, event) } case <-time.After(time.Second): t.Fatalf("%s did not receive broadcast event", name) } } assertEvent("subscriber A", eventsA) assertEvent("subscriber B", eventsB) } func testStore(t *testing.T) (*config.Config, *db.Store) { t.Helper() dir := t.TempDir() cfg := &config.Config{ BaseDir: dir, StorageDir: filepath.Join(dir, "storage"), DataDir: filepath.Join(dir, "data"), UpdatePublicDir: filepath.Join(dir, "data", "update", "public"), DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"), BaseURL: "https://update.ymhut.cn", Database: config.DatabaseConfig{ Provider: "sqlite", SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"), HealthIntervalSec: 30, }, } store, err := db.Open(cfg) if err != nil { t.Fatal(err) } t.Cleanup(func() { store.Close() }) return cfg, store }