216 lines
5.9 KiB
Go
216 lines
5.9 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())
|
|
}
|
|
}
|