dana-medical-site/internal/storage/onec_autonomous_test.go

150 lines
4.3 KiB
Go

package storage
import (
"context"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"dana/internal/onec"
)
// Run only against the disposable DB created by the autonomy acceptance command.
func TestAutonomousBookingLifecycle(t *testing.T) {
dsn := os.Getenv("DANA_AUTONOMY_TEST_DSN")
if dsn == "" {
t.Skip("requires disposable autonomy database")
}
ctx := context.Background()
s, err := New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer s.Close()
if err = s.Migrate(ctx); err != nil {
t.Fatal(err)
}
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
doctor := fmt.Sprintf("autonomy-%d", time.Now().UnixNano())
start := time.Now().UTC().Truncate(time.Hour).Add(48 * time.Hour)
end := start.Add(30 * time.Minute)
slot := onec.Slot{DoctorID: doctor, BranchID: "branch", Start: start, End: end, Status: "free"}
from, to := start.Add(-time.Hour), end.Add(time.Hour)
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
if !s.OnecFresh(ctx, time.Hour) {
t.Fatal("push did not mark freshness")
}
var wins atomic.Int32
var winner OnecAppointment
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 12; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
a, created, err := s.BeginOnecAppointment(ctx, OnecAppointment{RequestID: fmt.Sprintf("%s-%d", doctor, i), DoctorOnec: doctor, BranchID: "branch", Start: start, Patient: "test", Phone: "test", Comment: "test no calls"})
if errors.Is(err, ErrOnecSlotBusy) {
return
}
if err != nil {
t.Error(err)
return
}
if !created {
t.Error("unexpected duplicate")
return
}
wins.Add(1)
mu.Lock()
winner = a
mu.Unlock()
}(i)
}
wg.Wait()
if wins.Load() != 1 {
t.Fatalf("concurrent reservation winners=%d", wins.Load())
}
assertFree := func(n int) {
t.Helper()
v, err := s.FreeOnecSlots(ctx, doctor, from, to)
must(err)
if len(v) != n {
t.Fatalf("free slots=%d want %d", len(v), n)
}
}
assertFree(0)
same, created, err := s.BeginOnecAppointment(ctx, OnecAppointment{RequestID: winner.RequestID, DoctorOnec: doctor, Start: start})
must(err)
if created || same.ID != winner.ID {
t.Fatal("request retry duplicated booking")
}
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
assertFree(0) // stale free snapshot cannot reopen pending
pending, err := s.PendingOnecAppointments(ctx, 50)
must(err)
found := false
for _, p := range pending {
if p.ID == winner.ID {
found = true
if p.Start.Format("-07:00") != "+03:00" || p.Comment != "test no calls" {
t.Fatalf("bad pull fields: %+v", p)
}
}
}
if !found {
t.Fatal("pending booking missing")
}
uuid := "00000000-0000-0000-0000-000000000001"
must(s.ConfirmPendingAppointment(ctx, winner.ID, uuid, &end))
must(s.ConfirmPendingAppointment(ctx, winner.ID, uuid, &end))
if !errors.Is(s.ConfirmPendingAppointment(ctx, winner.ID, "different", &end), ErrOnecTransition) {
t.Fatal("mismatched confirm accepted")
}
if !errors.Is(s.RejectPendingAppointment(ctx, winner.ID, "late reject"), ErrOnecTransition) {
t.Fatal("late reject accepted")
}
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{slot}))
assertFree(0) // booked also survives full Push
must(s.QueueOnecCommand(ctx, winner.ID, "link_bitrix", "123", ""))
must(s.QueueOnecCommand(ctx, winner.ID, "link_bitrix", "123", ""))
commands, err := s.PendingOnecCommands(ctx)
must(err)
if len(commands) != 1 {
t.Fatal("duplicate link commands")
}
must(s.AckOnecCommand(ctx, commands[0].ID, false, "retry"))
commands, err = s.PendingOnecCommands(ctx)
must(err)
if len(commands) != 1 {
t.Fatal("failed ACK lost command")
}
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
must(s.QueueOnecCommand(ctx, winner.ID, "cancel", "", "cancelled"))
assertFree(0)
commands, err = s.PendingOnecCommands(ctx)
must(err)
if len(commands) != 1 || commands[0].Kind != "cancel" {
t.Fatal("cancel not queued")
}
must(s.AckOnecCommand(ctx, commands[0].ID, true, ""))
assertFree(1)
_, _, known, err := s.SetOnecAppointmentStatus(ctx, uuid, "booked")
must(err)
if known {
t.Fatal("delayed booked hook resurrected cancellation")
}
assertFree(1)
must(s.AcceptOnecSlots(ctx, from, to, []onec.Slot{}))
assertFree(0) // explicit empty horizon clears old shifts
}