153 lines
4.6 KiB
Go
153 lines
4.6 KiB
Go
package main
|
|
|
|
// api_test.go — bearer auth 401s, events endpoint, spawn validation.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestAPIAuthRejectsMissingOrBadToken(t *testing.T) {
|
|
ts, _ := newTestServer(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
header string
|
|
}{
|
|
{"none", ""},
|
|
{"wrong", "Bearer nope"},
|
|
{"not-bearer", testToken},
|
|
{"empty", "Bearer "},
|
|
}
|
|
for _, tc := range cases {
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/sessions", nil)
|
|
if tc.header != "" {
|
|
req.Header.Set("Authorization", tc.header)
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("%s: status = %d, want 401", tc.name, resp.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPIEventsEndpointParams(t *testing.T) {
|
|
ts, _ := newTestServer(t)
|
|
|
|
get := func(path string) int {
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+path, nil)
|
|
req.Header.Set("Authorization", "Bearer "+testToken)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("get %s: %v", path, err)
|
|
}
|
|
resp.Body.Close()
|
|
return resp.StatusCode
|
|
}
|
|
if code := get("/api/sessions/s1/events?after=abc"); code != http.StatusBadRequest {
|
|
t.Fatalf("after=abc → %d, want 400", code)
|
|
}
|
|
if code := get("/api/sessions/s1/events?limit=-1"); code != http.StatusBadRequest {
|
|
t.Fatalf("limit=-1 → %d, want 400", code)
|
|
}
|
|
if code := get("/api/sessions/unknown/events"); code != http.StatusOK {
|
|
t.Fatalf("unknown session → %d, want 200 with []", code)
|
|
}
|
|
}
|
|
|
|
func TestAPIEventsReplayShape(t *testing.T) {
|
|
ts, store := newTestServer(t)
|
|
|
|
// Simulate a connected session by writing directly through the store.
|
|
name := "sess"
|
|
if err := store.UpsertSession(SessionInfo{ID: "s1", Name: &name, Cwd: "/w", Model: "glm-5.3", Provider: "zai-renaud", StartedAt: 1}); err != nil {
|
|
t.Fatalf("upsert: %v", err)
|
|
}
|
|
if err := store.AppendEvent(Event{SessionID: "s1", Seq: 1, TS: 10, Type: evMessageEnd, Payload: []byte(`{"message":{"role":"user","id":"m1","text":"hi"}}`)}); err != nil {
|
|
t.Fatalf("append: %v", err)
|
|
}
|
|
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+"/api/sessions/s1/events?after=0&limit=10", nil)
|
|
req.Header.Set("Authorization", "Bearer "+testToken)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("events: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("events status = %d", resp.StatusCode)
|
|
}
|
|
if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
|
|
t.Fatalf("content-type = %q", ct)
|
|
}
|
|
var frames []map[string]any
|
|
if err := json.NewDecoder(resp.Body).Decode(&frames); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(frames) != 1 {
|
|
t.Fatalf("frames = %d, want 1", len(frames))
|
|
}
|
|
f := frames[0]
|
|
if f["type"] != evMessageEnd || f["sessionId"] != "s1" || f["seq"].(float64) != 1 || f["v"].(float64) != 1 {
|
|
t.Fatalf("frame = %v", f)
|
|
}
|
|
msg := f["message"].(map[string]any)
|
|
if msg["text"] != "hi" {
|
|
t.Fatalf("message = %v", msg)
|
|
}
|
|
}
|
|
|
|
func TestAPISpawnValidation(t *testing.T) {
|
|
ts, _ := newTestServer(t)
|
|
post := func(body string) int {
|
|
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/spawn", strings.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+testToken)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("spawn: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
return resp.StatusCode
|
|
}
|
|
if code := post(`{"repo":"no-slash"}`); code != http.StatusBadRequest {
|
|
t.Fatalf("bad repo → %d, want 400", code)
|
|
}
|
|
// branch is passed to git: anything outside the safe charset → 400
|
|
for _, branch := range []string{
|
|
"main; rm -rf /",
|
|
"feature one", // space
|
|
"-oProxyCommand=x", // leading option-ish
|
|
"main$(id)",
|
|
} {
|
|
if code := post(`{"repo":"group/project","branch":"` + branch + `"}`); code != http.StatusBadRequest {
|
|
t.Fatalf("branch %q → %d, want 400", branch, code)
|
|
}
|
|
}
|
|
// empty branch (default) stays accepted at this validation layer
|
|
if code := post(`{"repo":"group/project","branch":""}`); code == http.StatusBadRequest {
|
|
t.Fatal("empty branch must not be rejected as invalid")
|
|
}
|
|
}
|
|
|
|
func TestAPIRecoverMiddleware(t *testing.T) {
|
|
// A handler that panics must yield 500, not kill the server.
|
|
handler := recoverMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
panic("boom")
|
|
}))
|
|
rec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("panic → %d, want 500", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "error") {
|
|
t.Fatalf("body = %q", rec.Body.String())
|
|
}
|
|
}
|