test(nest): red for #10 — post/get endpoints, event-driven trigger, SSE stream
This commit is contained in:
1 parent
027eb34f6a
commit
ba5dbbc7ae
2 files changed
+179
No files matched your search
+126
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestNestStreamSSE checks GET /api/events?nest_id= streams nest.message events
|
||||
// and replays history from Last-Event-ID (the EVENTS.md nest SSE contract).
|
||||
func TestNestStreamSSE(t *testing.T) {
|
||||
ts, s := newTestNestServer(t)
|
||||
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||
s.hub.OpenNest(id)
|
||||
if _, out := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","text":"hello"}`); out["msg_id"] == nil {
|
||||
t.Fatalf("post failed: %v", out)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL+"/api/events?nest_id="+id, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "text/event-stream" {
|
||||
t.Fatalf("content type %q", ct)
|
||||
}
|
||||
|
||||
// Read in a goroutine with a deadline so a stuck stream fails, not hangs.
|
||||
got := make(chan bool, 1)
|
||||
go func() {
|
||||
sc := bufio.NewScanner(resp.Body)
|
||||
for sc.Scan() {
|
||||
if sc.Text() == "event: nest.message" {
|
||||
got <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
got <- false
|
||||
}()
|
||||
select {
|
||||
case ok := <-got:
|
||||
if !ok {
|
||||
t.Fatal("stream ended before nest.message appeared")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("no nest.message event within 2s")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user