| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/blob" |
| 13 | ) |
| 14 | |
| 15 | func TestClientFileBrowsingPermissionsAndDownload(t *testing.T) { |
| 16 | _, tenant := testTenant(t) |
| 17 | ctx := context.Background() |
| 18 | entry, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("<script>secret source</script>" ), Type: "text/html" , Uploader: tenant.Policy().Owner}) |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | if _, err := tenant.Execute(ctx, "" , "browsefiles" , nil); err == nil { |
| 23 | t.Fatal("anonymous uploader inventory exposed" ) |
| 24 | } |
| 25 | value, err := tenant.Execute(ctx, tenant.Policy().Owner, "browsefiles" , nil) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | raw, _ := json.Marshal(value) |
| 30 | if !strings.Contains(string(raw), entry.SHA256) { |
| 31 | t.Fatalf("missing blob %s" , raw) |
| 32 | } |
| 33 | req := httptest.NewRequest(http.MethodGet, "http://relay.test/files/raw?hash=" +entry.SHA256, nil) |
| 34 | res := httptest.NewRecorder() |
| 35 | tenant.ServeHTTP(res, req) |
| 36 | if res.Code != 200 || res.Body.String() != "<script>secret source</script>" { |
| 37 | t.Fatalf("download %d %s" , res.Code, res.Body.String()) |
| 38 | } |
| 39 | if !strings.HasPrefix(res.Header().Get("Content-Disposition" ), "attachment" ) || res.Header().Get("Content-Security-Policy" ) != "sandbox" { |
| 40 | t.Fatal("active download lacks isolation" ) |
| 41 | } |
| 42 | p := tenant.Policy() |
| 43 | p.Reads = "members" |
| 44 | if err := tenant.applyPolicy(ctx, p); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | res = httptest.NewRecorder() |
| 48 | tenant.ServeHTTP(res, req) |
| 49 | if res.Code == 200 { |
| 50 | t.Fatal("private download exposed" ) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestClientStatusRequiresOwnerAndPreservesErrors(t *testing.T) { |
| 55 | _, tenant := testTenant(t) |
| 56 | if _, err := tenant.Execute(context.Background(), "" , "browsestatus" , nil); err == nil { |
| 57 | t.Fatal("anonymous operational status exposed" ) |
| 58 | } |
| 59 | value, err := tenant.Execute(context.Background(), tenant.Policy().Owner, "browsestatus" , nil) |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | raw, _ := json.Marshal(value) |
| 64 | for _, field := range []string{"capabilities" , "git_sync" , "storage" , "jobs" , "delivery" , "backups" } { |
| 65 | if !strings.Contains(string(raw), `"` +field+`"` ) { |
| 66 | t.Fatalf("missing %s: %s" , field, raw) |
| 67 | } |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestFileSearchPaginationPreservesEveryUploaderMatch(t *testing.T) { |
| 72 | _, tenant := testTenant(t) |
| 73 | ctx := context.Background() |
| 74 | uploader := strings.Repeat("b" , 64) |
| 75 | var want []string |
| 76 | for i, typ := range []string{"text/plain" , "image/png" , "text/plain" , "text/plain" , "text/plain" } { |
| 77 | entry, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader(fmt.Sprintf("file %d" , i)), Type: typ, Uploader: uploader}) |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | if _, err := tenant.store.DB().ExecContext(ctx, "UPDATE blobs SET uploaded=? WHERE sha256=?" , 100-i, entry.SHA256); err != nil { |
| 82 | t.Fatal(err) |
| 83 | } |
| 84 | if typ == "text/plain" { |
| 85 | want = append(want, entry.SHA256) |
| 86 | } |
| 87 | } |
| 88 | if _, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("another user's matching file" ), Type: "text/plain" , Uploader: tenant.Policy().Owner}); err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | var got []string |
| 92 | cursor := "" |
| 93 | for page := 0; page < 5; page++ { |
| 94 | params, err := json.Marshal(map[string]any{"q" : "text/plain" , "limit" : 2, "cursor" : cursor}) |
| 95 | if err != nil { |
| 96 | t.Fatal(err) |
| 97 | } |
| 98 | result, err := tenant.Execute(ctx, uploader, "browsefiles" , []json.RawMessage{params}) |
| 99 | if err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | row := result.(map[string]any) |
| 103 | for _, item := range row["items" ].([]map[string]any) { |
| 104 | got = append(got, item["sha256" ].(string)) |
| 105 | } |
| 106 | cursor = row["next_cursor" ].(string) |
| 107 | if cursor == "" { |
| 108 | break |
| 109 | } |
| 110 | } |
| 111 | if fmt.Sprint(got) != fmt.Sprint(want) { |
| 112 | t.Fatalf("matching files = %v, want %v" , got, want) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | func TestOwnerFileSearchUsesLiteralTextBeforePagination(t *testing.T) { |
| 117 | _, tenant := testTenant(t) |
| 118 | ctx := context.Background() |
| 119 | for i, typ := range []string{"text/plain" , "application/x_foo" , "image/png" } { |
| 120 | if _, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader(fmt.Sprintf("owner file %d" , i)), Type: typ, Uploader: tenant.Policy().Owner}); err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | } |
| 124 | result, err := tenant.Execute(ctx, tenant.Policy().Owner, "browsefiles" , []json.RawMessage{json.RawMessage(`{"q":"_","limit":1}` )}) |
| 125 | if err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | row := result.(map[string]any) |
| 129 | items := row["items" ].([]map[string]any) |
| 130 | if len(items) != 1 || items[0]["type" ] != "application/x_foo" || row["next_cursor" ] != "" { |
| 131 | t.Fatalf("literal search = %#v" , row) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | func TestLibraryRowsGroupsLegacyUnnamedBinaryAndHidesMarkedChunks(t *testing.T) { |
| 136 | rows := []map[string]any{ |
| 137 | {"sha256" : strings.Repeat("a" , 64), "name" : "" , "path" : "" , "type" : "application/octet-stream" }, |
| 138 | {"sha256" : strings.Repeat("b" , 64), "name" : "" , "path" : "" , "type" : "application/octet-stream" , "purpose" : "chunk" }, |
| 139 | {"sha256" : strings.Repeat("c" , 64), "name" : "notes.txt" , "path" : "notes.txt" , "type" : "text/plain" }, |
| 140 | } |
| 141 | got := libraryRows(rows, nil) |
| 142 | if len(got) != 2 { |
| 143 | t.Fatalf("library rows = %d, want 2" , len(got)) |
| 144 | } |
| 145 | if got[0]["path" ] != "Unorganized uploads/aaaaaaaaaaaa" || got[0]["name" ] != "aaaaaaaaaaaa" { |
| 146 | t.Fatalf("legacy row = %#v" , got[0]) |
| 147 | } |
| 148 | if got[1]["path" ] != "notes.txt" { |
| 149 | t.Fatalf("named row changed = %#v" , got[1]) |
| 150 | } |
| 151 | } |