Merge pull request #19 from jacisjake/task/12-nest-e2e-mock
(#12) Mock-brain end-to-end conversation
This commit was merged in pull request #19.
This commit is contained in:
1 file changed
+129
+129
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestNestConversationEndToEndMock drives the whole loop with the offline mock:
|
||||
// an owner request auto-gets exactly one crow response, and an SSE reconnect
|
||||
// replays the same records the store holds.
|
||||
func TestNestConversationEndToEndMock(t *testing.T) {
|
||||
ts, s := newTestNestServer(t)
|
||||
if s.p.Flock.brain.Name() != "offline mock" {
|
||||
t.Fatalf("B5 must run on the offline mock, got %q", s.p.Flock.brain.Name())
|
||||
}
|
||||
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 || req["kind"] != "request" {
|
||||
t.Fatalf("request post: %d %v", code, req)
|
||||
}
|
||||
|
||||
// The addressed crow responds (must-respond), asynchronously.
|
||||
msgs := awaitMessages(t, ts, id, 2)
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("want exactly 2 messages, got %d: %v", len(msgs), msgs)
|
||||
}
|
||||
if msgs[0]["kind"] != "request" || msgs[1]["author"] != "Hugin" {
|
||||
t.Fatalf("bad conversation shape: %v", msgs)
|
||||
}
|
||||
respKind := msgs[1]["kind"].(string)
|
||||
if respKind != "claim" && respKind != "deliver" && respKind != "decline" {
|
||||
t.Fatalf("crow response must be responsive, got %v", msgs[1]["kind"])
|
||||
}
|
||||
if msgs[1]["refers_to"] != req["msg_id"] || msgs[1]["to"] != "owner" {
|
||||
t.Fatalf("response must answer the request and its author: %v", msgs[1])
|
||||
}
|
||||
|
||||
// The crow response is appended and emitted asynchronously; make sure the
|
||||
// hub has caught up before replaying from it.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
past, ch, _ := s.hub.Subscribe(id, 0)
|
||||
n := len(past)
|
||||
s.hub.Unsubscribe(id, ch)
|
||||
if n >= 2 || time.Now().After(deadline) {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// SSE reconnect with Last-Event-ID replays both records in order.
|
||||
events := readNestSSE(t, ts, id, 0)
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("want 2 replayed events, got %d: %v", len(events), events)
|
||||
}
|
||||
for i, ev := range events {
|
||||
if ev.Name != "nest.message" || ev.Data["msg_id"] != msgs[i]["msg_id"] || ev.Data["text"] != msgs[i]["text"] {
|
||||
t.Fatalf("replay mismatch at %d: event %v vs stored %v", i, ev.Data, msgs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Partial replay from Last-Event-ID: only the second message.
|
||||
tail := readNestSSE(t, ts, id, 1)
|
||||
if len(tail) != 1 || tail[0].Data["msg_id"] != msgs[1]["msg_id"] {
|
||||
t.Fatalf("Last-Event-ID 1 should skip the request, got %v", tail)
|
||||
}
|
||||
}
|
||||
|
||||
// readNestSSE connects to the nest stream, reads the replayed history, and
|
||||
// closes once a heartbeat marks the end of what was already there.
|
||||
func readNestSSE(t *testing.T, ts *httptest.Server, nestID string, lastID int) []sseEvent {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest("GET", ts.URL+"/api/events?nest_id="+nestID, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if lastID > 0 {
|
||||
req.Header.Set("Last-Event-ID", strconv.Itoa(lastID))
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
events := make(chan sseEvent, 32)
|
||||
go func() {
|
||||
var name string
|
||||
sc := bufio.NewScanner(resp.Body)
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
switch {
|
||||
case line == ": heartbeat":
|
||||
close(events)
|
||||
return
|
||||
case strings.HasPrefix(line, "event: "):
|
||||
name = strings.TrimPrefix(line, "event: ")
|
||||
case strings.HasPrefix(line, "data: "):
|
||||
var d map[string]any
|
||||
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
|
||||
events <- sseEvent{name, d}
|
||||
}
|
||||
}
|
||||
close(events)
|
||||
}()
|
||||
|
||||
var out []sseEvent
|
||||
deadline := time.After(2 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-events:
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = append(out, ev)
|
||||
case <-deadline:
|
||||
t.Fatal("nest SSE stalled before heartbeat")
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user