package main import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func newTestAPI(t *testing.T) (*http.ServeMux, *API) { t.Helper() gh, _ := newFake(http.StatusOK, searchBody) store, err := OpenStore(t.TempDir(), 0) if err != nil { t.Fatal(err) } a := &API{gh: gh, store: store, sched: NewScheduler(store, gh)} mux := http.NewServeMux() a.Routes(mux) return mux, a } func call(t *testing.T, mux *http.ServeMux, method, path, body string, out any) int { t.Helper() req := httptest.NewRequest(method, path, strings.NewReader(body)) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if out != nil && rec.Code < 300 { if err := json.Unmarshal(rec.Body.Bytes(), out); err != nil { t.Fatalf("%s %s: %v (%s)", method, path, err, rec.Body) } } return rec.Code } func TestSavedSearchAPI(t *testing.T) { mux, a := newTestAPI(t) for _, bad := range []string{ `{"name": "", "params": "q=x", "schedule": {"every": "week", "weekday": 1, "hour": 8}}`, `{"name": "x", "params": "sort=nope", "schedule": {"every": "week", "weekday": 1, "hour": 8}}`, `{"name": "x", "params": "q=x", "schedule": {"every": "month"}}`, `{"name": "x", "params": "q=x", "maxResults": 7, "schedule": {"every": "day", "hour": 8}}`, } { if code := call(t, mux, "POST", "/api/saved", bad, nil); code != http.StatusBadRequest { t.Errorf("%s: got %d, want 400", bad, code) } } var x SavedSearch code := call(t, mux, "POST", "/api/saved", `{"name": "Go syslog", "params": "q=syslog&language=Go&page=3", "schedule": {"every": "week", "weekday": 1, "hour": 8}}`, &x) if code != http.StatusCreated || x.ID == "" || x.MaxResults != 50 || !x.Enabled || x.NextRunAt.IsZero() { t.Fatalf("create: %d %+v", code, x) } if strings.Contains(x.Params, "page") { t.Errorf("page should be dropped from params: %s", x.Params) } // The first run starts in the background; run once more by hand. var info RunInfo if code := call(t, mux, "POST", "/api/saved/"+x.ID+"/run", "", &info); code != http.StatusOK || info.Error != "" || info.Count != 1 { t.Fatalf("run: %d %+v", code, info) } deadline := time.Now().Add(2 * time.Second) var runs []RunInfo for time.Now().Before(deadline) { call(t, mux, "GET", "/api/saved/"+x.ID+"/runs", "", &runs) if len(runs) == 2 { break } time.Sleep(20 * time.Millisecond) } if len(runs) != 2 { t.Fatalf("want 2 runs (background + manual), got %d", len(runs)) } var run Run if code := call(t, mux, "GET", "/api/saved/"+x.ID+"/runs/latest", "", &run); code != http.StatusOK || len(run.Items) != 1 || run.Items[0].FullName != "mcuadros/go-syslog" { t.Fatalf("latest run: %d %+v", code, run) } if d := run.Items[0].StarsDelta; d == nil || *d != 0 || run.Items[0].New { t.Errorf("second run: repository should be known with a 0 delta: %+v", run.Items[0]) } var upd SavedSearch code = call(t, mux, "PUT", "/api/saved/"+x.ID, `{"name": "Renamed", "params": "q=syslog", "maxResults": 100, "enabled": false, "schedule": {"every": "hours", "hours": 6}}`, &upd) if code != http.StatusOK || upd.Name != "Renamed" || upd.Enabled || !upd.NextRunAt.IsZero() || upd.LastRun == nil { t.Fatalf("update: %d %+v", code, upd) } if code := call(t, mux, "GET", "/api/saved/"+x.ID+"/runs/../../x", "", nil); code == http.StatusOK { t.Error("path traversal should not succeed") } if code := call(t, mux, "DELETE", "/api/saved/"+x.ID, "", nil); code != http.StatusNoContent { t.Errorf("delete: %d", code) } if code := call(t, mux, "GET", "/api/saved/"+x.ID, "", nil); code != http.StatusNotFound { t.Errorf("get after delete: %d", code) } _ = a } func TestSchedulerRunsDueSearches(t *testing.T) { _, a := newTestAPI(t) x, _ := a.store.Create(SavedSearch{Name: "due", Params: "q=syslog", MaxResults: 30, Enabled: true, Schedule: Schedule{Every: "day", Hour: 8}, NextRunAt: time.Now().Add(-time.Minute)}) a.store.Create(SavedSearch{Name: "paused", Params: "q=syslog", MaxResults: 30, Schedule: Schedule{Every: "day", Hour: 8}, NextRunAt: time.Now().Add(-time.Minute)}) a.sched.runDue(context.Background()) got, _ := a.store.Get(x.ID) if got.LastRun == nil || !got.NextRunAt.After(time.Now()) { t.Errorf("due search should have run and be planned again: %+v", got) } for _, s := range a.store.List() { if s.Name == "paused" && s.LastRun != nil { t.Error("a paused search must not run") } } }