clone

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

feat/standalone-relay
tinyrelay / internal / daemon / file_metadata_test.go
file_metadata_test.go 4.7K
1package daemon23import (4 "encoding/json"5 "net/http"6 "net/http/httptest"7 "strings"8 "testing"910 "github.com/FelineStateMachine/tinyrelay/internal/blob"11)1213func TestEncryptedFileMetadataEndpointBindsBodyAndClaim(t *testing.T) {14 _, tenant := testTenant(t)15 ctx := t.Context()16 entry, err := tenant.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("ciphertext"), Type: "application/octet-stream", Uploader: tenant.Policy().Owner})17 if err != nil {18 t.Fatal(err)19 }20 body := `{"hash":"` + entry.SHA256 + `","name":"Encrypted file ` + entry.SHA256[:12] + `","path":"Encrypted file ` + entry.SHA256[:12] + `","purpose":"file","size":104857600,"type":"application/octet-stream"}`21 r := httptest.NewRequest(http.MethodPost, "http://relay.test/files/metadata", strings.NewReader(body))22 r.Header.Set("Content-Type", "application/json")23 signRequest(t, r, body)24 w := httptest.NewRecorder()25 tenant.ServeHTTP(w, r)26 if w.Code != http.StatusOK {27 t.Fatalf("metadata status=%d body=%s", w.Code, w.Body.String())28 }29 var purpose, typ string30 var size int6431 if err := tenant.store.DB().QueryRow("SELECT purpose,logical_size,logical_type FROM blob_claim_metadata WHERE sha256=? AND uploader=?", entry.SHA256, tenant.Policy().Owner).Scan(&purpose, &size, &typ); err != nil {32 t.Fatal(err)33 }34 if purpose != "file" || size != 104857600 || typ != "application/octet-stream" {35 t.Fatalf("metadata=%q %d %q", purpose, size, typ)36 }37 bad := strings.Replace(body, `"size":104857600`, `"size":104857601`, 1)38 r = httptest.NewRequest(http.MethodPost, "http://relay.test/files/metadata", strings.NewReader(bad))39 r.Header.Set("Content-Type", "application/json")40 signRequest(t, r, body)41 w = httptest.NewRecorder()42 tenant.ServeHTTP(w, r)43 if w.Code != http.StatusUnauthorized {44 t.Fatalf("tampered metadata status=%d body=%s", w.Code, w.Body.String())45 }46 unknown := body[:len(body)-1] + `,"key":"must-not-store"}`47 r = httptest.NewRequest(http.MethodPost, "http://relay.test/files/metadata", strings.NewReader(unknown))48 r.Header.Set("Content-Type", "application/json")49 signRequest(t, r, unknown)50 w = httptest.NewRecorder()51 tenant.ServeHTTP(w, r)52 if w.Code != http.StatusBadRequest {53 t.Fatalf("unknown metadata status=%d body=%s", w.Code, w.Body.String())54 }55}5657func TestEncryptedUploadFinalizesOneLogicalLibraryFile(t *testing.T) {58 _, tenant := testTenant(t)59 body := "an encrypted upload fixture"60 r := httptest.NewRequest(http.MethodPut, "http://relay.test/upload?purpose=chunk", strings.NewReader(body))61 r.Header.Set("Content-Type", "application/octet-stream")62 signRequest(t, r, body)63 w := httptest.NewRecorder()64 tenant.ServeHTTP(w, r)65 if w.Code != http.StatusOK {66 t.Fatalf("upload: %d %s", w.Code, w.Body.String())67 }68 var uploaded struct {69 Hash string `json:"sha256"`70 }71 if err := json.Unmarshal(w.Body.Bytes(), &uploaded); err != nil {72 t.Fatal(err)73 }74 query := []json.RawMessage{json.RawMessage(`{"view":"library"}`)}75 library, err := tenant.Execute(t.Context(), tenant.Policy().Owner, "browsefiles", query)76 if err != nil {77 t.Fatal(err)78 }79 if len(fileItems(t, library)) != 0 {80 t.Fatalf("internal chunk in library: %v", library)81 }82 metadata := `{"hash":"` + uploaded.Hash + `","name":"Encrypted file","purpose":"file","size":104857600,"type":"application/octet-stream"}`83 r = httptest.NewRequest(http.MethodPost, "http://relay.test/files/metadata", strings.NewReader(metadata))84 signRequest(t, r, metadata)85 w = httptest.NewRecorder()86 tenant.ServeHTTP(w, r)87 if w.Code != http.StatusOK {88 t.Fatalf("finalize: %d %s", w.Code, w.Body.String())89 }90 library, err = tenant.Execute(t.Context(), tenant.Policy().Owner, "browsefiles", query)91 if err != nil {92 t.Fatal(err)93 }94 items := fileItems(t, library)95 if len(items) != 1 || items[0]["name"] != "Encrypted file" || items[0]["size"] != int64(104857600) {96 t.Fatalf("logical file: %v", items)97 }98}99100func TestFileMetadataCannotChangeAnotherUploadersClaim(t *testing.T) {101 _, tenant := testTenant(t)102 entry, err := tenant.blobs.Put(t.Context(), blob.PutOptions{Reader: strings.NewReader("someone else's file"), Uploader: strings.Repeat("b", 64)})103 if err != nil {104 t.Fatal(err)105 }106 body := `{"hash":"` + entry.SHA256 + `","name":"Changed","purpose":"file"}`107 r := httptest.NewRequest(http.MethodPost, "http://relay.test/files/metadata", strings.NewReader(body))108 signRequest(t, r, body)109 w := httptest.NewRecorder()110 tenant.ServeHTTP(w, r)111 if w.Code == http.StatusOK {112 t.Fatal("owner must not edit another uploader's claim metadata")113 }114 var count int115 if err := tenant.store.DB().QueryRow("SELECT count(*) FROM blob_claim_metadata WHERE sha256=?", entry.SHA256).Scan(&count); err != nil {116 t.Fatal(err)117 }118 if count != 0 {119 t.Fatal("metadata changed without claim ownership")120 }121}