first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
package seeder_test
import (
"context"
"testing"
"time"
"github.com/krow/krow-backend/go-api/internal/seeder"
"github.com/krow/krow-backend/go-api/internal/testutil"
)
// The shift collection is a rolling 56-day window, and its stable id encodes a
// shift's POSITION in that window rather than its identity. Seed on Friday and
// re-seed on Saturday and every id means a different date; one of them —
// Marcus's, because he works Mon–Fri and a Saturday window holds one fewer of
// his shifts — is no longer generated at all.
//
// Upsert cannot express that. These tests pin the behaviour that can: after any
// seed, shift_records holds exactly what BuildShifts produced for that instant,
// and nothing left over from a previous run.
func shiftCount(t *testing.T, h *testutil.Harness) int {
t.Helper()
var n int
if err := h.Pool.QueryRow(context.Background(),
"SELECT count(*) FROM shift_records").Scan(&n); err != nil {
t.Fatalf("count shift_records: %v", err)
}
return n
}
// TestReseedOnALaterDayLeavesNoStaleShifts is the regression itself: the
// database was seeded on one day, the frontend regenerates on the next, and the
// two must still describe the same collection.
func TestReseedOnALaterDayLeavesNoStaleShifts(t *testing.T) {
h := testutil.New(t)
fx := testutil.Fixture(t)
ctx := context.Background()
// A Friday, then the Saturday after it — the exact pair that orphaned
// shift_marcus_41 in the live database.
friday := time.Date(2026, 8, 21, 12, 0, 0, 0, time.Local)
saturday := friday.AddDate(0, 0, 1)
if _, err := seeder.New(h.Pool, fx, friday).Run(ctx); err != nil {
t.Fatalf("seed on the Friday: %v", err)
}
fridayRows := shiftCount(t, h)
if want := len(seeder.BuildShifts(friday)); fridayRows != want {
t.Fatalf("after the Friday seed: %d rows, generator produced %d", fridayRows, want)
}
result, err := seeder.New(h.Pool, fx, saturday).Run(ctx)
if err != nil {
t.Fatalf("re-seed on the Saturday: %v", err)
}
generated := seeder.BuildShifts(saturday)
if got := shiftCount(t, h); got != len(generated) {
t.Errorf("after re-seeding a day later: %d rows, but the generator produced %d "+
"— %d stale record(s) survived the re-seed", got, len(generated), got-len(generated))
}
if result.Pruned != fridayRows-len(generated) {
t.Errorf("Pruned = %d, want %d", result.Pruned, fridayRows-len(generated))
}
// Every surviving row must be one this run generated, holding this run's
// date for that id — not the previous run's.
want := map[string]string{}
for _, rec := range generated {
want[rec["id"].(string)] = rec["shift_date"].(string)
}
rows, err := h.Pool.Query(ctx, "SELECT legacy_id, shift_date::text FROM shift_records")
if err != nil {
t.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var legacy, date string
if err := rows.Scan(&legacy, &date); err != nil {
t.Fatal(err)
}
switch expected, generatedNow := want[legacy]; {
case !generatedNow:
t.Errorf("%s is in the database but was not generated for this instant", legacy)
case expected != date:
t.Errorf("%s holds %s, but this run generated it as %s", legacy, date, expected)
}
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
}
// TestReseedIsConvergentAcrossAWeek walks a whole week, so the assertion does
// not depend on the one day pair that happened to expose the bug. Every day of
// the week changes the roster composition differently.
func TestReseedIsConvergentAcrossAWeek(t *testing.T) {
h := testutil.New(t)
fx := testutil.Fixture(t)
ctx := context.Background()
day := time.Date(2026, 8, 17, 9, 0, 0, 0, time.Local) // a Monday
for i := 0; i < 7; i++ {
now := day.AddDate(0, 0, i)
if _, err := seeder.New(h.Pool, fx, now).Run(ctx); err != nil {
t.Fatalf("seed on %s: %v", now.Weekday(), err)
}
want := len(seeder.BuildShifts(now))
if got := shiftCount(t, h); got != want {
t.Errorf("%s %s: %d rows, generator produced %d",
now.Weekday(), now.Format("2006-01-02"), got, want)
}
}
}
// Re-seeding the same instant twice must still change nothing — the prune must
// not delete rows it just wrote.
func TestReseedSameInstantPrunesNothing(t *testing.T) {
h := testutil.New(t)
fx := testutil.Fixture(t)
ctx := context.Background()
before := shiftCount(t, h)
result, err := seeder.New(h.Pool, fx, h.Now).Run(ctx)
if err != nil {
t.Fatalf("re-seed: %v", err)
}
if result.Pruned != 0 {
t.Errorf("re-seeding the same instant pruned %d record(s), want 0", result.Pruned)
}
if got := shiftCount(t, h); got != before {
t.Errorf("shift_records went from %d to %d rows on an identical re-seed", before, got)
}
}
// The prune is scoped to the organization being seeded. Another organization's
// shift records are none of its business — and once authentication lands, that
// is the difference between a re-seed and an incident.
func TestPruneIsScopedToTheSeededOrganization(t *testing.T) {
h := testutil.New(t)
fx := testutil.Fixture(t)
ctx := context.Background()
var otherOrg string
if err := h.Pool.QueryRow(ctx,
`INSERT INTO organizations (name, slug) VALUES ('Other', 'other-org') RETURNING id::text`).
Scan(&otherOrg); err != nil {
t.Fatal(err)
}
// Copy one of this organization's shifts into the other one, with an id and
// legacy_id no generation will ever produce.
if _, err := h.Pool.Exec(ctx,
`INSERT INTO shift_records (org_id, legacy_id, staff_id, worker_name, worker_email,
job_posting_id, role, role_category, shift_date, scheduled_start, scheduled_end,
scheduled_hours, actual_start, actual_end, actual_hours, overtime_hours,
minutes_late, status, notes, created_date, updated_date)
SELECT $1::uuid, 'shift_other_99', staff_id, worker_name, worker_email,
job_posting_id, role, role_category, shift_date, scheduled_start, scheduled_end,
scheduled_hours, actual_start, actual_end, actual_hours, overtime_hours,
minutes_late, status, notes, created_date, updated_date
FROM shift_records LIMIT 1`, otherOrg); err != nil {
t.Fatal(err)
}
if _, err := seeder.New(h.Pool, fx, h.Now.AddDate(0, 0, 1)).Run(ctx); err != nil {
t.Fatalf("re-seed: %v", err)
}
var survived int
if err := h.Pool.QueryRow(ctx,
"SELECT count(*) FROM shift_records WHERE org_id = $1::uuid", otherOrg).Scan(&survived); err != nil {
t.Fatal(err)
}
if survived != 1 {
t.Errorf("the other organization's shift record was pruned: %d survived, want 1", survived)
}
}