| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 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 | |
| 18 | func 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 | |
| 31 | func 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 | |
| 48 | func browseRequestForTest(limit int) gitrelay.BrowseRequest { |
| 49 | return gitrelay.BrowseRequest{Limit: limit} |
| 50 | } |
| 51 | |
| 52 | func 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 | |
| 60 | func 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 | |
| 118 | func 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 | |
| 146 | func 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 | |
| 187 | func 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 | } |