clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / tinyclient / chat_activity_http_test.go
chat_activity_http_test.go 3.3K
1package tinyclient23import (4 "context"5 "errors"6 "net/http"7 "net/http/httptest"8 "strings"9 "testing"1011 "github.com/FelineStateMachine/tinyrelay/internal/policy"12)1314type activityHTTPBackend struct {15 roomsBackend16 seenActor, seenRoom, seenRoot string17 denied bool18}1920func (b *activityHTTPBackend) ReadChatActivity(_ context.Context, actor, room, root string) (any, error) {21 b.seenActor, b.seenRoom, b.seenRoot = actor, room, root22 if b.denied {23 return nil, errors.New("private details that must stay hidden")24 }25 return map[string]any{"approvals": []any{map[string]any{"id": roomReply, "asker": roomAgent, "asked": []string{roomOwner}, "kind": 9, "state": "open", "subject": "Approve <script>bad()</script>", "content": "Review the preview", "type": "question", "interaction": "approval", "selection": "single", "options": []any{map[string]any{"id": "yes", "label": "Yes"}, map[string]any{"id": "no", "label": "No"}}}}}, nil26}2728func TestChatActivityEndpointScopesAndEscapesServerRenderedCards(t *testing.T) {29 b := &activityHTTPBackend{roomsBackend: roomsBackend{fakeBackend: fakeBackend{policy: policy.Defaults(roomOwner)}}}30 actor := roomOwner31 app, err := New(b, Options{Actor: func(*http.Request) (string, error) { return actor, nil }})32 if err != nil {33 t.Fatal(err)34 }35 req := httptest.NewRequest(http.MethodGet, "/chat/activity?room=general&root="+roomThread, nil)36 req.RequestURI = "/r/team/chat/activity?room=general&root=" + roomThread37 res := httptest.NewRecorder()38 app.ServeHTTP(res, req)39 body := res.Body.String()40 if res.Code != 200 || res.Header().Get("Cache-Control") != "private, no-store" || b.seenActor != roomOwner || b.seenRoom != "general" || b.seenRoot != roomThread {41 t.Fatalf("request context: %d %v %#v body=%s", res.Code, res.Header(), b, res.Body.String())42 }43 for _, want := range []string{`<chat-decision`, `type="radio"`, `value="yes"`, `interaction="approval"`, `selection="single"`, `<strong>Approval request</strong>`, `href="/r/team/approvals?id=` + roomReply, `Approve &lt;script&gt;bad()&lt;/script&gt;`, `<noscript>`} {44 if !strings.Contains(body, want) {45 t.Errorf("missing %q in %s", want, body)46 }47 }48 if strings.Contains(body, "class=") || strings.Contains(body, "<script>bad()") {49 t.Fatal("unsafe markup")50 }51 actor = roomGuest52 res = httptest.NewRecorder()53 app.ServeHTTP(res, req)54 if strings.Contains(res.Body.String(), "chat-decision") || strings.Contains(res.Body.String(), "Review the preview") {55 t.Fatal("approval shown to unaddressed actor")56 }57 b.denied = true58 res = httptest.NewRecorder()59 app.ServeHTTP(res, req)60 if res.Code != http.StatusForbidden || strings.Contains(res.Body.String(), "private details") {61 t.Fatalf("private error leaked: %d %s", res.Code, res.Body.String())62 }63}6465func TestChatActivityEndpointRejectsInvalidRoutesAndWrites(t *testing.T) {66 app, _ := roomsApp(t, roomOwner)67 for _, test := range []struct {68 method, path string69 status int70 }{71 {http.MethodPost, "/chat/activity?room=general", 405},72 {http.MethodGet, "/chat/activity?room=../private", 400},73 {http.MethodGet, "/chat/activity?room=general&root=bad", 400},74 } {75 t.Run(test.method+test.path, func(t *testing.T) {76 res := httptest.NewRecorder()77 app.ServeHTTP(res, httptest.NewRequest(test.method, test.path, nil))78 if res.Code != test.status {79 t.Fatalf("status %d", res.Code)80 }81 })82 }83}