210 lines
6.6 KiB
Go
210 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|