Every save that stores new opportunities or new releases records them in a change batch (migration 00008: opportunity_change_batches and opportunity_change_batch_opportunities; the opportunities table is unchanged), as does every change of marks. A batch is named by a random SHA-256 hash, so it can't be guessed, and ordered by its created_at, which is kept strictly increasing even if the clock goes back. GET /api/opportunities pages now carry the newest batch, read before the page, and GET /api/opportunities?changed_after=<batch> returns every opportunity changed since that batch, unpaged, with the newest batch it includes. Offset and limit can't be combined with it, and an unknown batch is 410 Gone, telling the client to reload the whole list. OpportunitiesUpdated now carries its save's batch, and a "large" flag: over 50 opportunities, new and updated are left empty and only the summary counts them, so a big catch-up no longer sends tens of thousands of UUIDs.
56 lines
1.2 KiB
Go
56 lines
1.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/types"
|
|
)
|
|
|
|
// A batch recorded after the clock went back is still created after the
|
|
// newest batch, so asking for the batches after that one finds it.
|
|
func TestChangeBatchAfterClockWentBack(t *testing.T) {
|
|
db := openTestDB(t)
|
|
s := &Store{db: db}
|
|
ctx := context.Background()
|
|
|
|
_, err := s.SaveOpportunity(
|
|
ctx,
|
|
&types.Opportunity{
|
|
UUID: "ocds-1",
|
|
LatestReleaseDate: time.Now(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
future := time.Now().UTC().Add(time.Hour)
|
|
db.MustExec(insertChangeBatch, "future", future)
|
|
|
|
err = s.SetMarks(ctx, types.OpportunityMarks{
|
|
UUID: "ocds-1",
|
|
Won: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var latest changeBatch
|
|
if err := db.Get(&latest, selectLatestChangeBatch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !latest.CreatedAt.After(future) {
|
|
t.Errorf("batch created at %v, not after %v", latest.CreatedAt, future)
|
|
}
|
|
|
|
got, hash, err := s.ListChangedOpportunities(ctx, "future")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 1 || got[0].UUID != "ocds-1" || hash != latest.Hash {
|
|
t.Errorf("changed after future: %v, %q", got, hash)
|
|
}
|
|
}
|