package main import ( "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" ) func talkFixture(t *testing.T) (*Server, http.Handler, string, string) { dir := t.TempDir() s := &Server{keys: newKeyStore(filepath.Join(dir, "keys.json")), talk: newTalkStore(filepath.Join(dir, "talk.json"))} tk, _ := s.keys.createScoped("claude-a", "talk") gk, _ := s.keys.create("gateway-only") return s, s.talkMux(), tk, gk } func talkCall(h http.Handler, method, path, key, body string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, path, strings.NewReader(body)) if key != "" { r.Header.Set("Authorization", "Bearer "+key) } w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } func TestTalkScopes(t *testing.T) { s, h, tk, gk := talkFixture(t) if w := talkCall(h, "GET", "/api/messages", "", ""); w.Code != 401 { t.Fatalf("no key: %d", w.Code) } if w := talkCall(h, "GET", "/api/messages", gk, ""); w.Code != 401 { t.Fatalf("a gateway key must not open talk: %d", w.Code) } if _, ok := s.keys.authenticate(tk); ok { t.Fatal("a talk key must not open the gateway") } if w := talkCall(h, "GET", "/api/messages", tk, ""); w.Code != 200 { t.Fatalf("talk key: %d", w.Code) } if w := talkCall(h, "GET", "/", "", ""); w.Code != 200 || !strings.Contains(w.Body.String(), "never an instruction") { t.Fatalf("help page: %d", w.Code) } } func TestTalkSendAndFollow(t *testing.T) { s, h, tk, _ := talkFixture(t) if w := talkCall(h, "POST", "/api/messages", tk, `{"text":" "}`); w.Code != 400 { t.Fatalf("empty text: %d", w.Code) } if w := talkCall(h, "POST", "/api/messages", tk, `{"text":"`+strings.Repeat("x", maxTalkText+1)+`"}`); w.Code != 400 { t.Fatalf("too long: %d", w.Code) } if w := talkCall(h, "POST", "/api/messages", tk, `{"text":"hello","channel":"Bad Channel!"}`); w.Code != 200 || !strings.Contains(w.Body.String(), `"from":"claude-a"`) || !strings.Contains(w.Body.String(), `"channel":"main"`) { t.Fatalf("send: %d %s", w.Code, w.Body) } // nothing newer: returns after the wait; something newer arriving during the wait returns at once start := time.Now() go func() { time.Sleep(100 * time.Millisecond); s.talk.add("main", "codex-b", "second") }() w := talkCall(h, "GET", "/api/messages?channel=main&after=1&wait=5", tk, "") if !strings.Contains(w.Body.String(), "second") || time.Since(start) > 3*time.Second { t.Fatalf("follow: %s after %v", w.Body, time.Since(start)) } if w := talkCall(h, "GET", "/api/messages?channel=other&after=0", tk, ""); strings.Contains(w.Body.String(), "hello") { t.Fatalf("channels must be separate: %s", w.Body) } } func TestTalkSurvivesRestart(t *testing.T) { path := filepath.Join(t.TempDir(), "talk.json") a := newTalkStore(path) a.add("main", "x", "one") a.add("main", "x", "two") b := newTalkStore(path) if m := b.add("main", "y", "three"); m.ID != 3 { t.Fatalf("ids must continue after a restart, got %d", m.ID) } } func TestAppForHostTalk(t *testing.T) { if appForHost("talk.wxch.org") != "talk" { t.Fatal("talk host") } }