package oltp
import (
"errors"
"path/filepath"
"testing"
"time"
)
// One open request per agent, act and input; a decision lands once,
// and only by the owner; a request nobody answered in time closes.
func TestASignOffIsDecidedOnceByItsOwner(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
ask := SignOff{Agent: "bot", Owner: "ann", Op: "repo.delete", Repo: "r", Code: "c", Input: `{"name":"r"}`, Summary: "delete r"}
first, err := db.RequestSignOff(ask)
if err != nil || first.ID == "" || first.State != "pending" {
t.Fatalf("FAIL: %+v %v", first, err)
}
if again, _ := db.RequestSignOff(ask); again.ID != first.ID {
t.Errorf("FAIL: the same request made twice is two requests")
}
if _, err := db.DecideSignOff(first.ID, "mallory", true); !errors.Is(err, ErrSignOffClosed) {
t.Errorf("FAIL: somebody else decided it: %v", err)
}
if got, err := db.DecideSignOff(first.ID, "ann", true); err != nil || got.State != "approved" {
t.Fatalf("FAIL: the owner's decision: %+v %v", got, err)
}
if _, err := db.DecideSignOff(first.ID, "ann", false); !errors.Is(err, ErrSignOffClosed) {
t.Errorf("FAIL: a second decision landed: %v", err)
}
if err := db.FailSignOff(first.ID, "it broke"); err != nil {
t.Fatal(err)
}
if got, _, _ := db.SignOffByID(first.ID); got.State != "failed" || got.Err != "it broke" {
t.Errorf("FAIL: the failure was not kept: %+v", got)
}
// Past the window: closed on read, refused on decision, and a new
// request may be made.
old, err := db.RequestSignOff(SignOff{Agent: "bot", Owner: "ann", Op: "repo.remove", Repo: "r", Code: "c", Input: "{}", Summary: "x"})
if err != nil {
t.Fatal(err)
}
stale := time.Now().UTC().Add(-SignOffWindow - time.Minute).Format(time.RFC3339)
if _, err := db.w.Exec(`UPDATE signoff SET requested = ? WHERE id = ?`, stale, old.ID); err != nil {
t.Fatal(err)
}
if _, err := db.DecideSignOff(old.ID, "ann", true); !errors.Is(err, ErrSignOffClosed) {
t.Errorf("FAIL: an expired request was approved: %v", err)
}
list, _ := db.SignOffsFor("ann", 10)
for _, s := range list {
if s.ID == old.ID && s.State == "pending" {
t.Errorf("FAIL: an expired request reads as waiting")
}
}
if fresh, err := db.RequestSignOff(SignOff{Agent: "bot", Owner: "ann", Op: "repo.remove", Repo: "r", Code: "c", Input: "{}", Summary: "x"}); err != nil || fresh.ID == old.ID {
t.Errorf("FAIL: asking again after expiry: %+v %v", fresh, err)
}
}
// RFC 0027: an agent may have at most MaxPendingSignOffs waiting; an
// approved request keeps its answer, and its secret is the asking
// agent's to read once.
func TestASignOffKeepsItsAnswerAndItsSecretOnce(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
var first SignOff
for i := 0; i < MaxPendingSignOffs; i++ {
s, err := db.RequestSignOff(SignOff{Agent: "bot", Owner: "ann", Op: "agent.new", Input: `{"n":"` + itoaTest(i) + `"}`, Summary: "x", Reason: "qa"})
if err != nil {
t.Fatalf("FAIL: request %d: %v", i, err)
}
if i == 0 {
first = s
}
}
if _, err := db.RequestSignOff(SignOff{Agent: "bot", Owner: "ann", Op: "agent.new", Input: `{"n":"over"}`}); !errors.Is(err, ErrTooManySignOffs) {
t.Errorf("FAIL: request past the limit: %v", err)
}
if first.Reason != "qa" {
t.Errorf("FAIL: the reason was not kept: %+v", first)
}
if _, err := db.DecideSignOff(first.ID, "ann", true); err != nil {
t.Fatal(err)
}
if err := db.FinishSignOff(first.ID, `{"name":"rajeev"}`, `{"token":"t0k"}`); err != nil {
t.Fatal(err)
}
if got, _, _ := db.SignOffByID(first.ID); got.Result != `{"name":"rajeev"}` {
t.Errorf("FAIL: the answer was not kept: %+v", got)
}
if s, err := db.TakeSignOffSecret(first.ID, "mallory", time.Now()); err != nil || s != "" {
t.Errorf("FAIL: another account read the secret: %q %v", s, err)
}
if s, err := db.TakeSignOffSecret(first.ID, "bot", time.Now()); err != nil || s != `{"token":"t0k"}` {
t.Errorf("FAIL: the agent could not read its secret: %q %v", s, err)
}
if s, _ := db.TakeSignOffSecret(first.ID, "bot", time.Now()); s != "" {
t.Errorf("FAIL: the secret was readable twice")
}
}
func itoaTest(i int) string { return string(rune('a' + i)) }
// approvedWithSecret is an approved request holding a secret and a lapse
// reference, decided at at.
func approvedWithSecret(t *testing.T, db *DB, name string, at time.Time) string {
t.Helper()
s, err := db.RequestSignOff(SignOff{Agent: "bot", Owner: "ann", Op: "agent.token.new", Input: `{"n":"` + name + `"}`, Summary: "x"})
if err != nil {
t.Fatal(err)
}
if _, err := db.DecideSignOff(s.ID, "ann", true); err != nil {
t.Fatal(err)
}
if err := db.SetSignOffRefs(s.ID, `[{"kind":"agent-token","token":"t"}]`); err != nil {
t.Fatal(err)
}
if err := db.FinishSignOff(s.ID, `{}`, `{"value":"v"}`); err != nil {
t.Fatal(err)
}
if _, err := db.w.Exec(`UPDATE signoff SET decided = ? WHERE id = ?`, at.UTC().Format(time.RFC3339), s.ID); err != nil {
t.Fatal(err)
}
return s.ID
}
func dueHas(t *testing.T, db *DB, now time.Time, id string) bool {
t.Helper()
due, err := db.DueSignOffLapses(SignOffSecretCutoff(now))
if err != nil {
t.Fatal(err)
}
for _, l := range due {
if l.ID == id {
return true
}
}
return false
}
// At the limit and either side of it a secret is collectable XOR due to
// lapse, never both and never neither, and a take that commits leaves
// no lapse reference behind.
func TestASecretIsCollectableXORDueToLapse(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
at := time.Now().UTC().Truncate(time.Second)
for i, off := range []time.Duration{-time.Second, 0, time.Second, 2 * time.Second} {
id := approvedWithSecret(t, db, itoaTest(i), at)
now := at.Add(SignOffSecretLife + off)
due := dueHas(t, db, now, id)
got, err := db.TakeSignOffSecret(id, "bot", now)
if err != nil {
t.Fatal(err)
}
if due == (got != "") {
t.Errorf("FAIL: offset %v: due=%v collected=%q", off, due, got)
}
var lapse string
if err := db.db.QueryRow(`SELECT lapse FROM signoff WHERE id = ?`, id).Scan(&lapse); err != nil {
t.Fatal(err)
}
if got != "" && lapse != "" {
t.Errorf("FAIL: offset %v: a collected row kept its references: %q", off, lapse)
}
if got == "" && lapse == "" {
t.Errorf("FAIL: offset %v: a refused take dropped the references", off)
}
}
if !t.Failed() {
t.Log("ok")
}
}
// A take and a sweep need no agreement between their clocks: the take
// is given a time just before the limit and the sweep one just after,
// in both orders and concurrently, and every time exactly one of "the
// value was returned" and "the credential was claimed for revoking"
// happens, never both and never neither.
func TestATakeAndASweepNeverBothWinNorBothLose(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
at := time.Now().UTC().Truncate(time.Second)
takeAt := at.Add(SignOffSecretLife - time.Second)
sweepAt := at.Add(SignOffSecretLife + time.Second)
for i := 0; i < 300; i++ {
id := approvedWithSecret(t, db, "r"+time.Duration(i).String(), at)
var got string
var claimed bool
take := func() { got, _ = db.TakeSignOffSecret(id, "bot", takeAt) }
sweep := func() {
_, claimed, _ = db.ClaimSignOffLapse(id, SignOffSecretCutoff(sweepAt))
}
switch i % 4 {
case 0:
take()
sweep()
case 1:
sweep()
take()
default:
done := make(chan struct{}, 2)
go func() { take(); done <- struct{}{} }()
go func() { sweep(); done <- struct{}{} }()
<-done
<-done
}
if (got != "") == claimed {
t.Fatalf("FAIL: round %d (order %d): collected=%v claimed=%v", i, i%4, got != "", claimed)
}
var lapse string
if err := db.db.QueryRow(`SELECT lapse FROM signoff WHERE id = ?`, id).Scan(&lapse); err != nil {
t.Fatal(err)
}
if (got != "") == (lapse != "") {
t.Fatalf("FAIL: round %d: collected=%v but references=%q", i, got != "", lapse)
}
}
t.Log("ok")
}
// A claimed row whose revoke never finished stays lapsing: refused to a
// collector, returned again to the next sweep with its references, and
// done once cleared.
func TestAClaimedRowIsLapsingUntilCleared(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
at := time.Now().UTC().Truncate(time.Second)
id := approvedWithSecret(t, db, "x", at)
cut := SignOffSecretCutoff(at.Add(SignOffSecretLife + time.Second))
if _, ok, err := db.ClaimSignOffLapse(id, cut); err != nil || !ok {
t.Fatalf("FAIL: claim: %v %v", ok, err)
}
if got, _ := db.TakeSignOffSecret(id, "bot", at); got != "" {
t.Error("FAIL: a lapsing row was collected")
}
if due, _ := db.DueSignOffLapses(cut); len(due) != 1 || due[0].Refs == "" {
t.Errorf("FAIL: a lapsing row is not due again: %+v", due)
}
if refs, ok, _ := db.ClaimSignOffLapse(id, cut); !ok || refs == "" {
t.Error("FAIL: a lapsing row was not handed back")
}
if err := db.ClearSignOffLapse(id); err != nil {
t.Fatal(err)
}
if due, _ := db.DueSignOffLapses(cut); len(due) != 0 {
t.Errorf("FAIL: a done row is still due: %+v", due)
}
if _, ok, _ := db.ClaimSignOffLapse(id, cut); ok {
t.Error("FAIL: a done row was claimed")
}
if !t.Failed() {
t.Log("ok")
}
}