package main import ( "encoding/json" "net/http" "net/http/httptest" "path/filepath" "testing" "time" ) // newTestNestServer is newTestServer plus a message store. func newTestNestServer(t *testing.T) (*httptest.Server, *Server) { t.Helper() ts, s := newTestServer(t, DefaultGuardrails(), time.Minute) ms, err := LoadMessageStore(filepath.Join(t.TempDir(), "messages")) if err != nil { t.Fatal(err) } s.WithMessages(ms) return ts, s } func newNest(t *testing.T, ts *httptest.Server, name, crows string) string { t.Helper() code, out := post(t, ts.URL+"/api/nests", `{"name":"`+name+`","crows":[`+crows+`]}`) if code != http.StatusCreated { t.Fatalf("create nest: %d %v", code, out) } return out["nest_id"].(string) } func nestMessages(t *testing.T, ts *httptest.Server, nestID string) []map[string]any { t.Helper() resp, err := http.Get(ts.URL + "/api/nests/" + nestID + "/messages") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("get messages: %d", resp.StatusCode) } var msgs []map[string]any if err := json.NewDecoder(resp.Body).Decode(&msgs); err != nil { t.Fatal(err) } return msgs } // awaitMessages polls until the nest has at least n messages or the deadline passes. func awaitMessages(t *testing.T, ts *httptest.Server, nestID string, n int) []map[string]any { t.Helper() deadline := time.Now().Add(2 * time.Second) for { msgs := nestMessages(t, ts, nestID) if len(msgs) >= n || time.Now().After(deadline) { return msgs } time.Sleep(10 * time.Millisecond) } } func TestPostMessageToNest(t *testing.T) { ts, _ := newTestNestServer(t) id := newNest(t, ts, "Lab", `"Hugin"`) code, msg := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","text":"hello, flock"}`) if code != http.StatusCreated { t.Fatalf("want 201, got %d %v", code, msg) } if msg["kind"] != "say" || msg["author"] != "owner" || msg["text"] != "hello, flock" { t.Fatalf("bad record: %v", msg) } if msg["msg_id"] != id+"-1" { t.Fatalf("want msg_id %s-1, got %v", id, msg["msg_id"]) } got := nestMessages(t, ts, id) if len(got) != 1 || got[0]["text"] != "hello, flock" { t.Fatalf("log missing the message: %v", got) } } func TestPostMessageUnknownNest(t *testing.T) { ts, _ := newTestNestServer(t) code, out := post(t, ts.URL+"/api/nests/nope/messages", `{"author":"owner","text":"hi"}`) if code != http.StatusNotFound || out["message"] == nil { t.Fatalf("want 404 with message, got %d %v", code, out) } } func TestPostMessageTriggersOneResponse(t *testing.T) { ts, _ := newTestNestServer(t) id := newNest(t, ts, "Lab", `"Hugin"`) code, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","text":"count the eggs","to":"Hugin"}`) if code != http.StatusCreated { t.Fatalf("request post: %d %v", code, req) } msgs := awaitMessages(t, ts, id, 2) replies := 0 for _, m := range msgs { if m["author"] == "Hugin" { replies++ if m["kind"] != "say" && m["kind"] != "claim" && m["kind"] != "deliver" { t.Fatalf("crow reply has a bad kind: %v", m["kind"]) } if m["refers_to"] != req["msg_id"] { t.Fatalf("crow reply must point at the request, got %v", m["refers_to"]) } } } if replies != 1 { t.Fatalf("want exactly one Hugin message, got %d in %v", replies, msgs) } } func TestNoIdleResponse(t *testing.T) { ts, _ := newTestNestServer(t) id := newNest(t, ts, "Lab", `"Hugin"`) code, out := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","text":"just thinking out loud"}`) if code != http.StatusCreated { t.Fatalf("say post: %d %v", code, out) } time.Sleep(300 * time.Millisecond) if got := nestMessages(t, ts, id); len(got) != 1 { t.Fatalf("an unaddressed say must not summon a crow, log: %v", got) } }