package main import ( "encoding/json" "errors" "io" "net/http" "strings" "testing" "testing/iotest" ) func snapshotResponse(target, body string) *http.Response { request, _ := http.NewRequest(http.MethodGet, target, nil) return &http.Response{ StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: request, } } func TestBoundSessionSnapshotKeepsOnlyRecentMessages(t *testing.T) { messages := make([]map[string]int, 30) for index := range messages { messages[index] = map[string]int{"index": index} } body, _ := json.Marshal(map[string]any{ "session_id": "resolved", "messages": messages, }) response := snapshotResponse( "http://tenant/api/sessions/root/messages?limit=24&hermes_fallback=1", string(body), ) response.Header.Set("ETag", "stale") if err := boundSessionSnapshot(response); err != nil { t.Fatal(err) } var bounded sessionSnapshot if err := json.NewDecoder(response.Body).Decode(&bounded); err != nil { t.Fatal(err) } if len(bounded.Messages) != 24 || bounded.TotalMessages != 30 { t.Fatalf("snapshot was not bounded: %#v", bounded) } var first map[string]int if err := json.Unmarshal(bounded.Messages[0], &first); err != nil || first["index"] != 6 { t.Fatalf("snapshot did not retain the recent tail: %#v, %v", first, err) } if response.Header.Get("Cache-Control") != "no-store" || response.Header.Get("ETag") != "" { t.Fatal("bounded snapshot retained cache metadata") } } func TestBoundSessionSnapshotPreservesLargerReportedTotal(t *testing.T) { response := snapshotResponse( "http://tenant/api/sessions/root/messages?hermes_fallback=1", `{"session_id":"leaf","messages":[],"total_messages":100}`, ) if err := boundSessionSnapshot(response); err != nil { t.Fatal(err) } var bounded sessionSnapshot if err := json.NewDecoder(response.Body).Decode(&bounded); err != nil { t.Fatal(err) } if bounded.SessionID != "leaf" || bounded.TotalMessages != 100 { t.Fatalf("reported total was lost: %#v", bounded) } } func TestBoundSessionSnapshotFailsClosedOnMalformedOrOversizedBody(t *testing.T) { for name, body := range map[string]string{ "malformed": `{`, "oversized": strings.Repeat("x", sessionSnapshotBytes+1), } { t.Run(name, func(t *testing.T) { response := snapshotResponse( "http://tenant/api/sessions/root/messages?hermes_fallback=1", body, ) if err := boundSessionSnapshot(response); err == nil { t.Fatal("unsafe snapshot was accepted") } }) } response := snapshotResponse( "http://tenant/api/sessions/root/messages?hermes_fallback=1", `{}`, ) response.Body = io.NopCloser(iotest.ErrReader(errors.New("read failed"))) if err := boundSessionSnapshot(response); err == nil { t.Fatal("snapshot body read failure was ignored") } } func TestBoundSessionSnapshotLeavesNativeAndErrorResponsesUntouched(t *testing.T) { for _, response := range []*http.Response{ {StatusCode: http.StatusOK}, snapshotResponse("http://tenant/api/sessions/root/messages", `{}`), snapshotResponse("http://tenant/api/sessions/root", `{}`), } { if err := boundSessionSnapshot(response); err != nil { t.Fatal(err) } } errorResponse := snapshotResponse( "http://tenant/api/sessions/root/messages?hermes_fallback=1", `{}`, ) errorResponse.StatusCode = http.StatusNotFound if err := boundSessionSnapshot(errorResponse); err != nil { t.Fatal(err) } }