tinyrelayfile
tinyrelay/tinyclient/chat_activity_http_test.go/

tinyclient/chat_activity_http_test.go raw
1package tinyclient
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/policy"
12)
13
14type activityHTTPBackend struct {
15 roomsBackend
16 seenActor, seenRoom, seenRoot string
17 denied bool
18}
19
20func (b *activityHTTPBackend) ReadChatActivity(_ context.Context, actor, room, root string) (any, error) {
21 b.seenActor, b.seenRoom, b.seenRoot = actor, room, root
22 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"}}}, nil
26}
27
28func TestChatActivityEndpointScopesAndEscapesServerRenderedCards(t *testing.T) {
29 b := &activityHTTPBackend{roomsBackend: roomsBackend{fakeBackend: fakeBackend{policy: policy.Defaults(roomOwner)}}}
30 actor := roomOwner
31 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=" + roomThread
37 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", res.Code, res.Header(), b)
42 }
43 for _, want := range []string{`<chat-decision`, `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 = roomGuest
52 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 = true
58 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}
64
65func TestChatActivityEndpointRejectsInvalidRoutesAndWrites(t *testing.T) {
66 app, _ := roomsApp(t, roomOwner)
67 for _, test := range []struct {
68 method, path string
69 status int
70 }{
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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs