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"); err != nil || s != "" {
t.Errorf("FAIL: another account read the secret: %q %v", s, err)
}
if s, err := db.TakeSignOffSecret(first.ID, "bot"); 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"); s != "" {
t.Errorf("FAIL: the secret was readable twice")
}
}
func itoaTest(i int) string { return string(rune('a' + i)) }