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.
252 lines
6.0 KiB
Go
252 lines
6.0 KiB
Go
package mock
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"math/rand"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/mock/findatender/ocds"
|
|
)
|
|
|
|
func quietLog(t *testing.T) {
|
|
t.Helper()
|
|
w := log.Writer()
|
|
log.SetOutput(io.Discard)
|
|
t.Cleanup(func() {
|
|
log.SetOutput(w)
|
|
})
|
|
}
|
|
|
|
func TestGap(t *testing.T) {
|
|
//nolint:gosec // G404: seeded math/rand keeps the test deterministic
|
|
g := &Generator{
|
|
rnd: rand.New(rand.NewSource(1)),
|
|
}
|
|
if d := g.gap(time.Second, time.Second); d != time.Second {
|
|
t.Errorf("min == max: %s", d)
|
|
}
|
|
if d := g.gap(2*time.Second, time.Second); d != 2*time.Second {
|
|
t.Errorf("max < min: %s, want min", d)
|
|
}
|
|
for i := 0; i < 1000; i++ {
|
|
if d := g.gap(time.Second, 2*time.Second); d < time.Second || d > 2*time.Second {
|
|
t.Fatalf("gap out of range: %s", d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScaleValue(t *testing.T) {
|
|
scaleValue(nil, 2) // must not panic
|
|
|
|
v := &ocds.Value{
|
|
Amount: ptr(12345.0),
|
|
Currency: "GBP",
|
|
}
|
|
scaleValue(v, 1.5)
|
|
if *v.Amount != 18500 { // 18517.5 rounded to the nearest 100
|
|
t.Errorf("amount = %v, want 18500", *v.Amount)
|
|
}
|
|
if v.AmountGross != nil {
|
|
t.Error("nil gross amount was set")
|
|
}
|
|
|
|
v = &ocds.Value{
|
|
Amount: ptr(1000.0),
|
|
AmountGross: ptr(1200.0),
|
|
}
|
|
scaleValue(v, 0.5)
|
|
if *v.Amount != 500 || *v.AmountGross != 600 {
|
|
t.Errorf("amounts = %v / %v, want 500 / 600", *v.Amount, *v.AmountGross)
|
|
}
|
|
}
|
|
|
|
func TestEnsurePartyID(t *testing.T) {
|
|
p := ocds.Organization{
|
|
ID: "GB-EXISTING",
|
|
Name: "Council",
|
|
}
|
|
ensurePartyID(&p)
|
|
if p.ID != "GB-EXISTING" {
|
|
t.Errorf("existing id replaced: %s", p.ID)
|
|
}
|
|
|
|
a := ocds.Organization{
|
|
Name: "Somewhere Council",
|
|
}
|
|
b := ocds.Organization{
|
|
Name: "Somewhere Council",
|
|
}
|
|
c := ocds.Organization{
|
|
Name: "Elsewhere Council",
|
|
}
|
|
ensurePartyID(&a)
|
|
ensurePartyID(&b)
|
|
ensurePartyID(&c)
|
|
if !strings.HasPrefix(a.ID, "GB-FTS-") {
|
|
t.Errorf("id = %q", a.ID)
|
|
}
|
|
if a.ID != b.ID {
|
|
t.Errorf("same name gave %s and %s", a.ID, b.ID)
|
|
}
|
|
if a.ID == c.ID {
|
|
t.Errorf("different names gave the same id %s", a.ID)
|
|
}
|
|
}
|
|
|
|
func TestAdvance(t *testing.T) {
|
|
cat := loadTestCatalog(t)
|
|
store, err := OpenStore(filepath.Join(t.TempDir(), "r.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
g := NewGenerator(cat, store, 1)
|
|
at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
|
|
// A planning notice without a tender becomes an active tender.
|
|
r := g.advance(
|
|
ocds.Release{
|
|
OCID: "ocds-x",
|
|
Tag: []string{"planning"},
|
|
},
|
|
at,
|
|
)
|
|
if r.Tag[0] != "tender" || r.Tender == nil || r.Tender.Status != "active" ||
|
|
r.Tender.TenderPeriod == nil || r.Tender.TenderPeriod.EndDate == nil {
|
|
t.Fatalf("planning -> %v, tender %+v", r.Tag, r.Tender)
|
|
}
|
|
if string(r.Tender.ID) != "ocds-x" || !r.Date.Equal(at) {
|
|
t.Errorf("tender id %s, date %s", r.Tender.ID, r.Date)
|
|
}
|
|
|
|
// A tender is either updated (deadline + 14 days) or awarded.
|
|
end := at.AddDate(0, 0, 20)
|
|
prev := ocds.Release{
|
|
OCID: "ocds-y",
|
|
Tag: []string{"tender"},
|
|
Tender: &ocds.Tender{
|
|
ID: "ocds-y",
|
|
Value: &ocds.Value{
|
|
Amount: ptr(10000.0),
|
|
Currency: "GBP",
|
|
},
|
|
Lots: []ocds.Lot{{ID: "1"}, {ID: ""}},
|
|
TenderPeriod: &ocds.Period{
|
|
EndDate: &end,
|
|
},
|
|
},
|
|
}
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 50; i++ {
|
|
r := g.advance(prev, at)
|
|
seen[r.Tag[0]] = true
|
|
switch r.Tag[0] {
|
|
case "tenderUpdate":
|
|
if got := *r.Tender.TenderPeriod.EndDate; !got.Equal(end.AddDate(0, 0, 14)) {
|
|
t.Fatalf("updated deadline %s", got)
|
|
}
|
|
case "award":
|
|
if r.Tender.Status != "complete" || len(r.Awards) != 1 {
|
|
t.Fatalf("award: status %s, %d awards", r.Tender.Status, len(r.Awards))
|
|
}
|
|
aw := r.Awards[0]
|
|
if aw.Value == nil || *aw.Value.Amount < 8000 || *aw.Value.Amount > 10000 {
|
|
t.Fatalf("award value %+v", aw.Value)
|
|
}
|
|
if len(aw.RelatedLots) != 1 || aw.RelatedLots[0] != "1" {
|
|
t.Fatalf("related lots %v", aw.RelatedLots)
|
|
}
|
|
if len(aw.Suppliers) != 1 || len(r.Parties) != 1 || r.Parties[0].Roles[0] != "supplier" {
|
|
t.Fatalf("suppliers %v, parties %v", aw.Suppliers, r.Parties)
|
|
}
|
|
default:
|
|
t.Fatalf("tender -> %s", r.Tag[0])
|
|
}
|
|
}
|
|
if !seen["tenderUpdate"] || !seen["award"] {
|
|
t.Errorf("expected both updates and awards, saw %v", seen)
|
|
}
|
|
if *prev.Tender.TenderPeriod.EndDate != end || prev.Awards != nil {
|
|
t.Error("advance modified the previous release")
|
|
}
|
|
|
|
// An update without a deadline gets 30 + 14 days; a tender with no value
|
|
// is awarded with no value.
|
|
prev = ocds.Release{
|
|
OCID: "ocds-z",
|
|
Tag: []string{"tender"},
|
|
Tender: &ocds.Tender{
|
|
ID: "ocds-z",
|
|
},
|
|
}
|
|
for i := 0; i < 50; i++ {
|
|
r := g.advance(prev, at)
|
|
switch r.Tag[0] {
|
|
case "tenderUpdate":
|
|
if got := *r.Tender.TenderPeriod.EndDate; !got.Equal(at.AddDate(0, 0, 44)) {
|
|
t.Fatalf("default deadline %s", got)
|
|
}
|
|
case "award":
|
|
if r.Awards[0].Value != nil {
|
|
t.Fatalf("award value %+v, want nil", r.Awards[0].Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunPublishesUntilCancelled(t *testing.T) {
|
|
quietLog(t)
|
|
cat := loadTestCatalog(t)
|
|
store, err := OpenStore(filepath.Join(t.TempDir(), "r.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
NewGenerator(cat, store, 1).Run(ctx, time.Millisecond, 2*time.Millisecond)
|
|
close(done)
|
|
}()
|
|
|
|
deadline := time.After(5 * time.Second)
|
|
for store.Len() < 3 {
|
|
select {
|
|
case <-deadline:
|
|
cancel()
|
|
t.Fatalf("only %d releases published", store.Len())
|
|
case <-time.After(time.Millisecond):
|
|
}
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("Run did not return after cancel")
|
|
}
|
|
}
|
|
|
|
// A failing store is logged and Run carries on until cancelled.
|
|
func TestRunSurvivesStoreErrors(t *testing.T) {
|
|
quietLog(t)
|
|
cat := loadTestCatalog(t)
|
|
store, err := OpenStore(filepath.Join(t.TempDir(), "r.jsonl"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
NewGenerator(cat, store, 1).Run(ctx, time.Millisecond, time.Millisecond)
|
|
if store.Len() != 0 {
|
|
t.Errorf("Len = %d, want 0", store.Len())
|
|
}
|
|
}
|