clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / daemon / client_browse_test.go
client_browse_test.go 5.1K
1package daemon23import (4 "context"5 "encoding/json"6 "fmt"7 "net/http"8 "net/http/httptest"9 "strings"10 "testing"1112 "github.com/FelineStateMachine/tinyrelay/internal/blob"13)1415func 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}5354func 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}7071func TestFileSearchPaginationPreservesEveryUploaderMatch(t *testing.T) {72 _, tenant := testTenant(t)73 ctx := context.Background()74 uploader := strings.Repeat("b", 64)75 var want []string76 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 []string92 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 break109 }110 }111 if fmt.Sprint(got) != fmt.Sprint(want) {112 t.Fatalf("matching files = %v, want %v", got, want)113 }114}115116func 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}134135func 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}