Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
214 lines
6.6 KiB
Go
214 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)
|
|
}
|
|
}
|