tinyrelayfile
tinyrelay/tinyclient/browse_test.go/

tinyclient/browse_test.go raw
1package tinyclient
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/policy"
12 gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit"
13)
14
15type browseBackend struct {
16 fakeBackend
17 method string
18 params []json.RawMessage
19}
20
21func (b *browseBackend) Query(_ context.Context, method string, params []json.RawMessage, _ string) (any, error) {
22 b.method, b.params = method, params
23 switch method {
24 case "browserepos":
25 return map[string]any{"items": []any{map[string]any{"owner": "alice", "identifier": "notes", "head": "abc"}}}, nil
26 case "browserepo":
27 return map[string]any{"view": "file", "content": "one\ntwo", "entries": []any{}}, nil
28 case "browsefiles":
29 return map[string]any{"items": []any{map[string]any{"sha256": "deadbeef", "type": "text/plain"}}}, nil
30 case "browsefile":
31 return map[string]any{"content": "blob", "binary": false}, nil
32 case "browsestatus":
33 return map[string]any{"jobs": []any{}, "storage": map[string]any{"ok": true}}, nil
34 default:
35 return nil, nil
36 }
37}
38
39func TestBrowseRoutesUseObjectContractsAndRenderData(t *testing.T) {
40 b := &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}
41 app, err := New(b, Options{})
42 if err != nil {
43 t.Fatal(err)
44 }
45 tests := []struct {
46 path, method, marker string
47 }{
48 {"/repos", "browserepos", "notes"},
49 {"/repo?owner=alice&repo=notes&view=file", "browserepo", "one"},
50 {"/files", "browsefiles", "deadbeef"},
51 {"/file?hash=deadbeef", "browsefile", "blob"},
52 {"/manage/health", "browsestatus", "manage this relay"},
53 }
54 for _, test := range tests {
55 recorder := httptest.NewRecorder()
56 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
57 if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), test.marker) {
58 t.Fatalf("%s: status=%d body=%s", test.path, recorder.Code, recorder.Body.String())
59 }
60 if b.method != test.method || len(b.params) != 1 {
61 t.Fatalf("%s: method=%q params=%d", test.path, b.method, len(b.params))
62 }
63 var object map[string]any
64 if err := json.Unmarshal(b.params[0], &object); err != nil {
65 t.Fatalf("%s: params are not an object: %v", test.path, err)
66 }
67 if test.method == "browserepo" && object["view"] != "file" {
68 t.Fatalf("repo view=%v", object["view"])
69 }
70 }
71}
72
73func TestConnectPublishesGRASPAndDeliveryKinds(t *testing.T) {
74 b := &fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}
75 app, err := New(b, Options{Actor: func(*http.Request) (string, error) { return b.policy.Owner, nil }})
76 if err != nil {
77 t.Fatal(err)
78 }
79 recorder := httptest.NewRecorder()
80 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/manage/connect", nil))
81 body := recorder.Body.String()
82 for _, marker := range []string{`<publish-list kind="10317" tag="g" scheme="ws">`, `<publish-list kind="10063" tag="server">`, `name="lines"`, `src="/scripts/components.js?v=`} {
83 if !strings.Contains(body, marker) {
84 t.Fatalf("connect page missing %q", marker)
85 }
86 }
87 components := servedScript(t, app, "/scripts/components.js")
88 for _, marker := range []string{`customElements.define("publish-list"`, `tiny.signedFetch("/events", "POST"`} {
89 if !strings.Contains(components, marker) {
90 t.Fatalf("components script missing %q", marker)
91 }
92 }
93}
94
95func TestBrowseRendersTypedGitRelayPage(t *testing.T) {
96 b := &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}
97 // Keep the fixture typed exactly as daemon.executeBrowse returns it. This
98 // catches template regressions where fields are assumed to be map values.
99 page := gitrelay.BrowsePage{Owner: "alice", Identifier: "notes", View: "file", Content: "<script>\nline two", Clone: []string{"https://example.test/notes.git"}}
100 browse := &typedBrowseBackend{browseBackend: b, page: page}
101 app, err := New(browse, Options{})
102 if err != nil {
103 t.Fatal(err)
104 }
105 recorder := httptest.NewRecorder()
106 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/repo?owner=alice&repo=notes&view=file", nil))
107 body := recorder.Body.String()
108 if recorder.Code != http.StatusOK || !strings.Contains(body, "&lt;script&gt;") || !strings.Contains(body, "line two") || !strings.Contains(body, ">raw</a>") {
109 t.Fatalf("typed browse page status=%d body=%s", recorder.Code, body)
110 }
111}
112
113func TestFileViewerEscapesHashAndExplainsEmptyFiles(t *testing.T) {
114 b := &emptyFileBackend{browseBackend: &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}}
115 app, err := New(b, Options{})
116 if err != nil {
117 t.Fatal(err)
118 }
119 recorder := httptest.NewRecorder()
120 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/file?hash=a%26b", nil))
121 body := recorder.Body.String()
122 if recorder.Code != http.StatusOK || !strings.Contains(body, `fx-ignore href="/files/raw?hash=a%26b"`) {
123 t.Fatalf("file hash was not URL encoded: status=%d body=%s", recorder.Code, body)
124 }
125 if !strings.Contains(body, "This file is empty.") {
126 t.Fatalf("empty file did not get an explanatory preview: %s", body)
127 }
128}
129
130type emptyFileBackend struct{ *browseBackend }
131
132func (b *emptyFileBackend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) {
133 if method == "browsefile" {
134 return map[string]any{"sha256": "a&b", "type": "text/plain", "content": ""}, nil
135 }
136 return b.browseBackend.Query(ctx, method, params, actor)
137}
138
139type typedBrowseBackend struct {
140 *browseBackend
141 page gitrelay.BrowsePage
142}
143
144func (b *typedBrowseBackend) Query(_ context.Context, method string, _ []json.RawMessage, _ string) (any, error) {
145 if method == "browserepo" {
146 return b.page, nil
147 }
148 return b.browseBackend.Query(context.Background(), method, nil, "")
149}
150
151// servedScript fetches one of the cached browser scripts through the app.
152func servedScript(t *testing.T, app *App, path string) string {
153 t.Helper()
154 recorder := httptest.NewRecorder()
155 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, path, nil))
156 if recorder.Code != http.StatusOK {
157 t.Fatalf("%s: status=%d", path, recorder.Code)
158 }
159 return recorder.Body.String()
160}
161
162func TestNostrNameBundleIsServedAndLoadedOnEveryPage(t *testing.T) {
163 b := &browseBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}}
164 app, err := New(b, Options{})
165 if err != nil {
166 t.Fatal(err)
167 }
168 bundle := servedScript(t, app, "/scripts/nostr-name.js")
169 for _, marker := range []string{`customElements.define("nostr-name"`, "nostrSharedMetadataLoader", "tiny:names:v1"} {
170 if !strings.Contains(bundle, marker) {
171 t.Fatalf("nostr-name bundle missing %q", marker)
172 }
173 }
174 if strings.Contains(bundle, "cdn.jsdelivr.net") {
175 t.Fatal("nostr-name bundle reaches for a CDN")
176 }
177 recorder := httptest.NewRecorder()
178 app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/repos", nil))
179 if body := recorder.Body.String(); !strings.Contains(body, `src="/scripts/nostr-name.js?v=`) {
180 t.Fatal("page does not load the nostr-name bundle")
181 }
182}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs