tinyrelayfile
tinyrelay/internal/daemon/client_files_test.go/

internal/daemon/client_files_test.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/blob"
12 "github.com/FelineStateMachine/tinyrelay/internal/event"
13 gitrelay "github.com/FelineStateMachine/tinyrelay/tinygit"
14 "github.com/FelineStateMachine/tinyrelay/internal/relay"
15 "github.com/FelineStateMachine/tinyrelay/internal/sites"
16)
17
18func fileItems(t *testing.T, value any) []map[string]any {
19 t.Helper()
20 row, ok := value.(map[string]any)
21 if !ok {
22 t.Fatalf("files result = %T", value)
23 }
24 items, ok := row["items"].([]map[string]any)
25 if !ok {
26 t.Fatalf("files items = %T", row["items"])
27 }
28 return items
29}
30
31func TestFolderItemsKeepsFoldersAndFilesPageable(t *testing.T) {
32 rows := []map[string]any{
33 {"sha256": "aaaaaaaa", "type": "text/plain", "uploader": "alice", "uploaded": int64(2), "name": "readme.txt", "path": "docs/readme.txt"},
34 {"sha256": "bbbbbbbb", "type": "image/png", "uploader": "alice", "uploaded": int64(1), "name": "cover.png", "path": "cover.png"},
35 }
36 first := (&Tenant{}).folderItems(rows, map[string]bool{}, clientBrowseRequest{BrowseRequest: browseRequestForTest(1)}, "library", true)
37 items := first["items"].([]map[string]any)
38 if len(items) != 1 || items[0]["kind"] != "folder" || first["next_cursor"] == "" {
39 t.Fatalf("first page = %#v", first)
40 }
41 second := (&Tenant{}).folderItems(rows, map[string]bool{}, clientBrowseRequest{BrowseRequest: browseRequestForTest(1), Cursor: first["next_cursor"].(string)}, "library", true)
42 items = second["items"].([]map[string]any)
43 if len(items) != 1 || items[0]["kind"] != "file" || items[0]["name"] != "cover.png" {
44 t.Fatalf("second page = %#v", second)
45 }
46}
47
48func browseRequestForTest(limit int) gitrelay.BrowseRequest {
49 return gitrelay.BrowseRequest{Limit: limit}
50}
51
52func TestContextualRowsRetainsDistinctPathsForSharedHash(t *testing.T) {
53 rows := []map[string]any{{"sha256": "same", "name": "old", "path": "old"}}
54 out := contextualRows(rows, map[string][]string{"same": {"site-a/index.html", "site-b/index.html"}}, "site")
55 if len(out) != 2 || out[0]["path"] == out[1]["path"] {
56 t.Fatalf("contextual rows = %#v", out)
57 }
58}
59
60func TestContextualFilesRespectClaimNamesAndPaginateLibrary(t *testing.T) {
61 _, tenant := testTenant(t)
62 ctx := context.Background()
63 owner := tenant.Policy().Owner
64 other := strings.Repeat("b", 64)
65 shared, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("shared"), Type: "text/plain", Uploader: owner, Name: "owner.txt", Path: "docs/owner.txt"})
66 if err != nil {
67 t.Fatal(err)
68 }
69 if _, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("shared"), Type: "text/plain", Uploader: other, Name: "private.txt", Path: "private/private.txt"}); err != nil {
70 t.Fatal(err)
71 }
72 for i := 0; i < 60; i++ {
73 if _, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader(fmt.Sprintf("library-%d", i)), Type: "text/plain", Uploader: owner, Name: fmt.Sprintf("file-%02d.txt", i), Path: fmt.Sprintf("folder-%02d/file-%02d.txt", i, i)}); err != nil {
74 t.Fatal(err)
75 }
76 }
77 firstValue, err := tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "library", "limit": 50})})
78 if err != nil {
79 t.Fatal(err)
80 }
81 first := fileItems(t, firstValue)
82 firstRow := firstValue.(map[string]any)
83 if len(first) != 50 || firstRow["next_cursor"] == "" {
84 t.Fatalf("library first page = %d, next=%v", len(first), firstRow["next_cursor"])
85 }
86 secondValue, err := tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "library", "limit": 50, "cursor": firstRow["next_cursor"]})})
87 if err != nil {
88 t.Fatal(err)
89 }
90 second := fileItems(t, secondValue)
91 if len(second) != 11 {
92 t.Fatalf("library second page = %d, want 11", len(second))
93 }
94 seen := map[string]bool{}
95 for _, item := range append(first, second...) {
96 key := item["path"].(string)
97 if seen[key] {
98 t.Fatalf("duplicate library path %q", key)
99 }
100 seen[key] = true
101 }
102 ownerFile, err := tenant.Execute(ctx, owner, "browsefile", []json.RawMessage{rawJSON(map[string]any{"hash": shared.SHA256})})
103 if err != nil {
104 t.Fatal(err)
105 }
106 if ownerFile.(map[string]any)["name"] != "owner.txt" || ownerFile.(map[string]any)["path"] != "docs/owner.txt" {
107 t.Fatalf("owner file metadata = %#v", ownerFile)
108 }
109 otherFile, err := tenant.Execute(ctx, other, "browsefile", []json.RawMessage{rawJSON(map[string]any{"hash": shared.SHA256})})
110 if err != nil {
111 t.Fatal(err)
112 }
113 if otherFile.(map[string]any)["name"] == "owner.txt" || otherFile.(map[string]any)["path"] == "docs/owner.txt" {
114 t.Fatalf("other uploader saw owner's name: %#v", otherFile)
115 }
116}
117
118func TestStorageViewDeduplicatesSharedBlobClaims(t *testing.T) {
119 _, tenant := testTenant(t)
120 ctx := context.Background()
121 owner := tenant.Policy().Owner
122 other := strings.Repeat("c", 64)
123 entry, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("same bytes"), Type: "text/plain", Uploader: owner, Name: "owner.txt"})
124 if err != nil {
125 t.Fatal(err)
126 }
127 if _, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("same bytes"), Type: "text/plain", Uploader: other, Name: "other.txt"}); err != nil {
128 t.Fatal(err)
129 }
130 value, err := tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "storage", "limit": 50})})
131 if err != nil {
132 t.Fatal(err)
133 }
134 items := fileItems(t, value)
135 count := 0
136 for _, item := range items {
137 if item["sha256"] == entry.SHA256 {
138 count++
139 }
140 }
141 if count != 1 {
142 t.Fatalf("shared blob appears %d times in storage view: %#v", count, items)
143 }
144}
145
146func TestSiteFilesRetainSharedHashPaths(t *testing.T) {
147 app, tenant := testTenant(t)
148 defer app.Close(context.Background())
149 ctx := context.Background()
150 ownerSecret := strings.Repeat("0", 63) + "1"
151 owner := tenant.Policy().Owner
152 entry, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("site bytes"), Type: "text/html", Uploader: owner})
153 if err != nil {
154 t.Fatal(err)
155 }
156 manifest := event.Event{Kind: sites.KindSite, CreatedAt: time.Now().Unix(), Tags: [][]string{{"path", "/index.html", entry.SHA256}, {"path", "/mirror.html", entry.SHA256}}}
157 if err := event.Sign(&manifest, ownerSecret); err != nil {
158 t.Fatal(err)
159 }
160 if _, err := tenant.Publish(ctx, manifest, relay.Session{PubKeys: []string{owner}}); err != nil {
161 t.Fatal(err)
162 }
163 value, err := tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "sites", "limit": 50})})
164 if err != nil {
165 t.Fatal(err)
166 }
167 paths := map[string]bool{}
168 for _, folder := range fileItems(t, value) {
169 if folder["kind"] != "folder" && folder["kind"] != "site" {
170 continue
171 }
172 child, err := tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "sites", "path": folder["path"], "limit": 50})})
173 if err != nil {
174 t.Fatal(err)
175 }
176 for _, item := range fileItems(t, child) {
177 if item["sha256"] == entry.SHA256 {
178 paths[item["path"].(string)] = true
179 }
180 }
181 }
182 if len(paths) != 2 {
183 t.Fatalf("site paths for shared hash = %#v", paths)
184 }
185}
186
187func TestRoomFilesRespectPrivateAccessAndSameNameRooms(t *testing.T) {
188 h := newRoomHarness(t)
189 ctx := h.ctx
190 owner := h.keys["owner"]
191 h.must("alice", event.KIND_CREATE_GROUP, [][]string{{"h", "private-one"}, {"name", "Twin"}, {"closed"}}, "")
192 h.must("alice", event.KIND_CREATE_GROUP, [][]string{{"h", "private-two"}, {"name", "Twin"}, {"closed"}}, "")
193 for i, roomID := range []string{"private-one", "private-two"} {
194 room, err := h.tenant.community.Room(ctx, roomID)
195 if err != nil {
196 t.Fatal(err)
197 }
198 entry, err := h.tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader(fmt.Sprintf("private room %d", i)), Type: "text/plain", Uploader: h.keys["alice"]})
199 if err != nil {
200 t.Fatal(err)
201 }
202 if _, err := h.tenant.store.DB().ExecContext(ctx, "INSERT INTO room_attachments(room_id,room_event_id,sha256,filename,created_at) VALUES(?,?,?,?,unixepoch())", roomID, room.EventID, entry.SHA256, fmt.Sprintf("note-%d.txt", i)); err != nil {
203 t.Fatal(err)
204 }
205 }
206 ownerValue, err := h.tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "rooms", "limit": 50})})
207 if err != nil {
208 t.Fatal(err)
209 }
210 paths := map[string]bool{}
211 for _, folder := range fileItems(t, ownerValue) {
212 if folder["kind"] != "folder" && folder["kind"] != "room" {
213 continue
214 }
215 child, err := h.tenant.Execute(ctx, owner, "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "rooms", "path": folder["path"], "limit": 50})})
216 if err != nil {
217 t.Fatal(err)
218 }
219 for _, item := range fileItems(t, child) {
220 if item["kind"] == "file" {
221 paths[item["path"].(string)] = true
222 }
223 }
224 }
225 if len(paths) != 2 {
226 t.Fatalf("same-name room paths = %#v", paths)
227 }
228 bobValue, err := h.tenant.Execute(ctx, h.keys["bob"], "browsefiles", []json.RawMessage{rawJSON(map[string]any{"view": "rooms", "limit": 50})})
229 if err != nil {
230 t.Fatal(err)
231 }
232 if got := len(fileItems(t, bobValue)); got != 0 {
233 t.Fatalf("private room files visible to outsider: %d", got)
234 }
235}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs