package mock import ( "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "git.avsdev.uk/Go/job-search/internal/mock/findatender/ocds" ) func TestUpdatedFromIsLocalTime(t *testing.T) { london, err := time.LoadLocation("Europe/London") if err != nil { t.Fatal(err) } store, err := OpenStore(filepath.Join(t.TempDir(), "r.jsonl")) if err != nil { t.Fatal(err) } defer store.Close() // Two releases at 11:45Z and 12:15Z, i.e. 12:45 and 13:15 BST. for i, at := range []time.Time{ time.Date(2026, 10, 8, 11, 45, 0, 0, time.UTC), time.Date(2026, 10, 8, 12, 15, 0, 0, time.UTC), } { if err := store.Append( ocds.Release{ OCID: "ocds-test", ID: []string{"000001-2026", "000002-2026"}[i], Date: at, Tag: []string{"tender"}, }, ); err != nil { t.Fatal(err) } } h := NewHandler(store, "", london) count := func(query string) int { rec := httptest.NewRecorder() h(rec, httptest.NewRequest("GET", BasePath+"?"+query, nil)) var pkg ocds.ReleasePackage if err := json.Unmarshal(rec.Body.Bytes(), &pkg); err != nil { t.Fatalf("%s: %v (%s)", query, err, rec.Body.String()) } return len(pkg.Releases) } cases := map[string]int{ "updatedFrom=2026-10-08T13:00:00": 1, // 13:00 BST = 12:00Z "updatedFrom=2026-10-08T12:30:00": 2, // 12:30 BST = 11:30Z "updatedFrom=2026-10-08T12:00:00Z": 1, // explicit UTC still honoured "updatedFrom=2026-10-08T12:00:00%2B01:00": 2, // explicit offset still honoured "updatedFrom=2026-10-08T12:00:00&updatedTo=2026-10-08T13:00:00": 1, // window in BST } for q, want := range cases { if got := count(q); got != want { t.Errorf("%s: got %d releases, want %d", q, got, want) } } // In winter (GMT) London local time equals UTC. winter := time.Date(2026, 12, 1, 9, 0, 0, 0, time.UTC) if got, _ := parseTime("2026-12-01T09:00:00", london); !got.Equal(winter) { t.Errorf("GMT: got %s, want %s", got, winter) } } func openTestStore(t *testing.T) *Store { t.Helper() store, err := OpenStore(filepath.Join(t.TempDir(), "r.jsonl")) if err != nil { t.Fatal(err) } t.Cleanup(func() { store.Close() }) return store } func TestHandlerRejectsBadRequests(t *testing.T) { h := NewHandler(openTestStore(t), "", nil) rec := httptest.NewRecorder() h(rec, httptest.NewRequest("POST", BasePath, nil)) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("POST: status %d, want 405", rec.Code) } cursor := func(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s)) } cases := map[string]string{ "limit=0": "limit must be an integer between 1 and 100", "limit=101": "limit must be an integer between 1 and 100", "limit=ten": "limit must be an integer between 1 and 100", "updatedFrom=yesterday": "updatedFrom must be in format yyyy-MM-ddTHH:mm:ss", "updatedTo=2026-13-01": "updatedTo must be in format yyyy-MM-ddTHH:mm:ss", "updatedFrom=2026-10-08T12:00:00&updatedTo=2026-10-08T11:00:00": "updatedTo must not be before updatedFrom", "cursor=!!!": "invalid cursor", "cursor=" + cursor("pos:1"): "invalid cursor", "cursor=" + cursor("idx:one"): "invalid cursor", "cursor=" + cursor("idx:-1"): "invalid cursor", } for q, want := range cases { rec := httptest.NewRecorder() h(rec, httptest.NewRequest("GET", BasePath+"?"+q, nil)) if rec.Code != http.StatusBadRequest { t.Errorf("%s: status %d, want 400", q, rec.Code) continue } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("%s: Content-Type %q", q, ct) } var body map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil || body["error"] != want { t.Errorf("%s: body %s, want error %q", q, rec.Body.String(), want) } } } func TestCursorRoundTrip(t *testing.T) { for _, i := range []int{0, 1, 99, 123456} { if got, err := decodeCursor(encodeCursor(i)); err != nil || got != i { t.Errorf("decodeCursor(encodeCursor(%d)) = %d, %v", i, got, err) } } } func TestParseTimeFormats(t *testing.T) { cases := map[string]time.Time{ "2026-10-08T12:00:00Z": time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC), "2026-10-08T12:00:00+02:00": time.Date(2026, 10, 8, 10, 0, 0, 0, time.UTC), "2026-10-08T12:00:00": time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC), "2026-10-08": time.Date(2026, 10, 8, 0, 0, 0, 0, time.UTC), } for v, want := range cases { got, err := parseTime(v, nil) // nil location means UTC if err != nil || !got.Equal(want) { t.Errorf("parseTime(%q) = %s, %v; want %s", v, got, err, want) } } for _, v := range []string{ "", "08/10/2026", "2026-10-08 12:00:00", "2026-10-08T25:00:00", } { if _, err := parseTime(v, nil); err == nil { t.Errorf("parseTime(%q): expected error", v) } } } // Following links.next walks every release exactly once, keeping the other // query parameters, and the last page has no next link. func TestHandlerPagination(t *testing.T) { store := openTestStore(t) base := time.Date(2026, 10, 8, 9, 0, 0, 0, time.UTC) for i := 0; i < 5; i++ { r := ocds.Release{ OCID: fmt.Sprintf("ocds-test-%d", i), ID: fmt.Sprintf("%06d-2026", i+1), Date: base.Add(time.Duration(i) * time.Minute), Tag: []string{"tender"}, } if err := store.Append(r); err != nil { t.Fatal(err) } } const baseURL = "http://mock.test" h := NewHandler(store, baseURL, nil) next := BasePath + "?limit=2&updatedFrom=2026-10-08T09:01:00" var ids []string for pages := 0; next != ""; pages++ { if pages > 5 { t.Fatal("too many pages") } rec := httptest.NewRecorder() h(rec, httptest.NewRequest("GET", next, nil)) if rec.Code != http.StatusOK { t.Fatalf("%s: status %d: %s", next, rec.Code, rec.Body.String()) } var pkg ocds.ReleasePackage if err := json.Unmarshal(rec.Body.Bytes(), &pkg); err != nil { t.Fatal(err) } if pkg.URI != baseURL+next { t.Errorf("uri = %q, want %q", pkg.URI, baseURL+next) } for _, r := range pkg.Releases { ids = append(ids, r.ID) } next = "" if pkg.Links != nil && pkg.Links.Next != "" { if !strings.HasPrefix(pkg.Links.Next, baseURL+BasePath+"?") { t.Fatalf("next = %q", pkg.Links.Next) } if !strings.Contains(pkg.Links.Next, "limit=2") || !strings.Contains(pkg.Links.Next, "updatedFrom=") { t.Errorf("next lost query parameters: %q", pkg.Links.Next) } next = strings.TrimPrefix(pkg.Links.Next, baseURL) } } want := "000002-2026,000003-2026,000004-2026,000005-2026" if got := strings.Join(ids, ","); got != want { t.Errorf("walked %s, want %s", got, want) } }