package oltp
import (
"path/filepath"
"sync"
"testing"
"time"
)
func openSetupStartDB(t *testing.T) *DB {
t.Helper()
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestASetupStartIsHeldHashedClaimedOnceAndLapses(t *testing.T) {
db := openSetupStartDB(t)
now := time.Now()
pick := "abcd"
code, err := db.StartSetup(SetupStart{Pickup: pick, Name: "e-desktop", Kind: "read", Addr: "192.0.2.1"}, 10, 100, now)
if err != nil {
t.Fatal(err)
}
if len(code) != 8 {
t.Fatalf("FAIL: code %q is not eight characters", code)
}
var stored int
if err := db.db.QueryRow(`SELECT count(*) FROM setup_start WHERE hash = ? OR hash LIKE ?`, code, "%"+code+"%").Scan(&stored); err != nil || stored != 0 {
t.Errorf("FAIL: the short code is stored in the clear (%d, %v)", stored, err)
}
// Typed any way a person might: lower case, dashes, look-alikes.
typed := code[:4] + "-" + code[4:]
if s, ok, err := db.SetupStartByCode(typed, now); err != nil || !ok || s.Name != "e-desktop" || s.Kind != "read" || s.Addr != "192.0.2.1" || s.Pickup != pick {
t.Errorf("FAIL: the start by its code: %+v %v %v", s, ok, err)
}
// Two claims at once: exactly one gets the row.
var wg sync.WaitGroup
var mu sync.Mutex
won := 0
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if _, ok, err := db.ClaimSetupStart(code, now); err == nil && ok {
mu.Lock()
won++
mu.Unlock()
}
}()
}
wg.Wait()
if won != 1 {
t.Errorf("FAIL: %d claims won", won)
}
if _, ok, _ := db.SetupStartByCode(code, now); ok {
t.Errorf("FAIL: a claimed start is still held")
}
// A lapsed start is not found, and the next write removes the row.
old := now.Add(-SetupStartLife - time.Minute)
lapsed, err := db.StartSetup(SetupStart{Pickup: pick, Name: "x", Kind: "push", Addr: "a"}, 10, 100, old)
if err != nil {
t.Fatal(err)
}
if _, ok, _ := db.SetupStartByCode(lapsed, now); ok {
t.Errorf("FAIL: a lapsed start is found")
}
if n, _ := db.SetupStartRows(); n != 1 {
t.Fatalf("FAIL: %d rows before the sweep", n)
}
if _, err := db.StartSetup(SetupStart{Pickup: pick, Name: "y", Kind: "push", Addr: "a"}, 10, 100, now); err != nil {
t.Fatal(err)
}
if n, _ := db.SetupStartRows(); n != 1 {
t.Errorf("FAIL: the lapsed row was not removed: %d rows", n)
}
// A start put back after a claim is as it was.
c2, _ := db.StartSetup(SetupStart{Pickup: pick, Name: "z", Kind: "read", Addr: "a"}, 10, 100, now)
held, ok, _ := db.ClaimSetupStart(c2, now)
if !ok {
t.Fatal("FAIL: claim")
}
if err := db.RestoreSetupStart(c2, held); err != nil {
t.Fatal(err)
}
if s, ok, _ := db.SetupStartByCode(c2, now); !ok || s != held {
t.Errorf("FAIL: the restored start differs: %+v", s)
}
if !t.Failed() {
t.Log("ok")
}
}
func TestSetupStartsAreCapped(t *testing.T) {
db := openSetupStartDB(t)
now := time.Now()
for i := 0; i < 3; i++ {
if _, err := db.StartSetup(SetupStart{Pickup: "p", Name: "n", Kind: "read", Addr: "a"}, 3, 100, now); err != nil {
t.Fatal(err)
}
}
if _, err := db.StartSetup(SetupStart{Pickup: "p", Name: "n", Kind: "read", Addr: "a"}, 3, 100, now); err != ErrSetupStartsFull {
t.Errorf("FAIL: past the cap: %v", err)
}
// Lapsed ones do not count against it.
if _, err := db.StartSetup(SetupStart{Pickup: "p", Name: "n", Kind: "read", Addr: "a"}, 3, 100, now.Add(SetupStartLife+time.Minute)); err != nil {
t.Errorf("FAIL: lapsed starts held the cap: %v", err)
}
}
func TestSetupCodesAreReadTheWayTheyAreTyped(t *testing.T) {
for typed, want := range map[string]string{"abcd-efgh": "ABCDEFGH", "0o1i-lL22": "00111122"} {
if got, ok := NormalizeSetupCode(typed); !ok || got != want {
t.Errorf("FAIL: %q reads as %q (%v), want %q", typed, got, ok, want)
}
}
for _, bad := range []string{"", "ABC", "ABCD-EFGHJ", "ABCD-EFGU", "ABCD EFG!"} {
if _, ok := NormalizeSetupCode(bad); ok {
t.Errorf("FAIL: %q is read as a code", bad)
}
}
}