clone

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

main
tinyrelay / internal / daemon / data_http_test.go
data_http_test.go 8.3K
1package daemon23import (4 "bytes"5 "context"6 "encoding/json"7 "net/http"8 "net/http/httptest"9 "os"10 "path/filepath"11 "strings"12 "testing"13 "time"1415 "github.com/FelineStateMachine/tinyrelay/internal/blob"16 "github.com/FelineStateMachine/tinyrelay/internal/event"17 "github.com/FelineStateMachine/tinyrelay/internal/replication"18 "github.com/FelineStateMachine/tinyrelay/internal/storage"19)2021func TestParseJSONLEventsValidatesEveryLine(t *testing.T) {22 good := map[string]any{"id": strings.Repeat("a", 64), "pubkey": strings.Repeat("b", 64), "created_at": 1, "kind": 1, "tags": [][]string{}, "content": "", "sig": strings.Repeat("c", 128)}23 raw, _ := json.Marshal(good)24 if _, err := parseJSONLEvents(append(raw, '\n')); err == nil {25 t.Fatal("unsigned event accepted")26 }27 if _, err := parseJSONLEvents([]byte("{}\n{")); err == nil {28 t.Fatal("malformed later line accepted")29 }30}3132func TestDataHTTPRequiresSignedOwnerForMutations(t *testing.T) {33 _, tenant := testTenant(t)34 body := []byte("{}\n")35 req := httptest.NewRequest(http.MethodPut, "http://relay.test/import", bytes.NewReader(body))36 res := httptest.NewRecorder()37 tenant.tryDataHTTP(res, req)38 if res.Code != http.StatusForbidden {39 t.Fatalf("cookie-less import status = %d, body=%s", res.Code, res.Body.String())40 }41}4243func TestBackupPreviewSignedAndRestoreRequiresFreshMatchingOwner(t *testing.T) {44 _, tenant := testTenant(t)45 archive, err := replication.CreateBackupWithProvider(context.Background(), tenant.store, tenant.ReplicationBackupProvider(), time.Now().Unix())46 if err != nil {47 t.Fatal(err)48 }49 body, err := json.Marshal(archive)50 if err != nil {51 t.Fatal(err)52 }53 req := httptest.NewRequest(http.MethodPost, "http://relay.test/backups/preview", bytes.NewReader(body))54 signRequest(t, req, string(body))55 res := httptest.NewRecorder()56 tenant.tryDataHTTP(res, req)57 if res.Code != http.StatusOK {58 t.Fatalf("preview status = %d, body=%s", res.Code, res.Body.String())59 }60 item := event.Event{Kind: 1, CreatedAt: time.Now().Unix(), Tags: [][]string{}, Content: "already here"}61 if err := event.Sign(&item, strings.Repeat("0", 63)+"1"); err != nil {62 t.Fatal(err)63 }64 if _, err := tenant.store.Save(context.Background(), item, storage.SaveOptions{Now: time.Now().Unix()}); err != nil {65 t.Fatal(err)66 }67 req = httptest.NewRequest(http.MethodPost, "http://relay.test/backups/restore", bytes.NewReader(body))68 signRequest(t, req, string(body))69 res = httptest.NewRecorder()70 tenant.tryDataHTTP(res, req)71 if res.Code != http.StatusConflict {72 t.Fatalf("non-fresh restore status = %d, body=%s", res.Code, res.Body.String())73 }74}7576func TestWithinPathRejectsEscape(t *testing.T) {77 if withinPath("/srv/relay", "/srv/relay/backup.json") == false {78 t.Fatal("in-root path rejected")79 }80 if withinPath("/srv/relay", "/srv/relay-other/backup.json") {81 t.Fatal("prefix escape accepted")82 }83}8485func TestNativeBackupHTTPRoundTripPreservesPrivateStateAndFiles(t *testing.T) {86 ctx := context.Background()87 _, source := testTenant(t)88 app, target := testTenant(t)89 now := time.Now().Unix()90 item := event.Event{Kind: 1, CreatedAt: now, Tags: [][]string{}, Content: "hidden source history"}91 if err := event.Sign(&item, strings.Repeat("0", 63)+"1"); err != nil {92 t.Fatal(err)93 }94 if _, err := source.store.Save(ctx, item, storage.SaveOptions{Now: now}); err != nil {95 t.Fatal(err)96 }97 if _, err := source.store.DB().ExecContext(ctx, `INSERT INTO hidden_events(id,reason) VALUES(?,?)`, item.ID, "test"); err != nil {98 t.Fatal(err)99 }100 entry, err := source.blobs.Put(ctx, blob.PutOptions{Reader: strings.NewReader("portable file"), Type: "text/plain", Uploader: source.Policy().Owner})101 if err != nil {102 t.Fatal(err)103 }104 archive, err := replication.CreateBackupWithProvider(ctx, source.store, source.ReplicationBackupProvider(), now)105 if err != nil {106 t.Fatal(err)107 }108 body, err := json.Marshal(archive)109 if err != nil {110 t.Fatal(err)111 }112 req := httptest.NewRequest(http.MethodPost, "http://relay.test/r/main/backups/restore", bytes.NewReader(body))113 signRequest(t, req, string(body))114 res := httptest.NewRecorder()115 app.ServeHTTP(res, req)116 if res.Code != http.StatusOK {117 t.Fatalf("restore %d: %s", res.Code, res.Body.String())118 }119 var raw string120 if err := target.store.DB().QueryRowContext(ctx, `SELECT raw FROM events WHERE id=?`, item.ID).Scan(&raw); err != nil {121 t.Fatal(err)122 }123 var hidden int124 if err := target.store.DB().QueryRowContext(ctx, `SELECT count(*) FROM hidden_events WHERE id=?`, item.ID).Scan(&hidden); err != nil || hidden != 1 {125 t.Fatalf("hidden state %d: %v", hidden, err)126 }127 if target.records.PublicKey() != source.records.PublicKey() {128 t.Fatal("native relay signing identity was not restored")129 }130 restored, reader, err := target.blobs.Get(ctx, entry.SHA256)131 if err != nil {132 t.Fatal(err)133 }134 contents, err := readUnbounded(reader)135 _ = reader.Close()136 if err != nil || string(contents) != "portable file" || restored.Type != entry.Type || restored.Uploader != entry.Uploader {137 t.Fatalf("restored blob %#v %q: %v", restored, contents, err)138 }139 // Owner-only downloads must remain scoped to the same tenant and uncacheable.140 name := "migration.backup.json"141 if err := os.WriteFile(filepath.Join(target.meta.Paths.Root, name), body, 0600); err != nil {142 t.Fatal(err)143 }144 req = httptest.NewRequest(http.MethodGet, "http://relay.test/r/main/backups/"+name, nil)145 signRequest(t, req, "")146 res = httptest.NewRecorder()147 app.ServeHTTP(res, req)148 if res.Code != http.StatusOK || !bytes.Equal(res.Body.Bytes(), body) || res.Header().Get("Cache-Control") != "private, no-store" {149 t.Fatalf("download %d: %s", res.Code, res.Body.String())150 }151}152153func TestRestoreFreshnessIncludesInvisibleEventsAndUnindexedFiles(t *testing.T) {154 for _, fixture := range []string{"hidden", "pending", "expired", "blob", "git"} {155 t.Run(fixture, func(t *testing.T) {156 _, tenant := testTenant(t)157 ctx := context.Background()158 if fresh, err := tenant.restoreTargetFresh(ctx); err != nil || !fresh {159 t.Fatalf("new relay fresh=%v err=%v", fresh, err)160 }161 if fixture == "blob" || fixture == "git" {162 root := filepath.Join(tenant.meta.Paths.Root, "blobs")163 if fixture == "git" {164 root = tenant.meta.Paths.Git165 }166 if err := os.WriteFile(filepath.Join(root, "existing"), []byte("data"), 0600); err != nil {167 t.Fatal(err)168 }169 } else {170 e := event.Event{Kind: 1, CreatedAt: time.Now().Unix(), Tags: [][]string{}, Content: fixture}171 if err := event.Sign(&e, strings.Repeat("0", 63)+"1"); err != nil {172 t.Fatal(err)173 }174 if _, err := tenant.store.Save(ctx, e, storage.SaveOptions{Now: time.Now().Unix()}); err != nil {175 t.Fatal(err)176 }177 switch fixture {178 case "hidden":179 _, _ = tenant.store.DB().ExecContext(ctx, `INSERT INTO hidden_events(id,reason) VALUES(?,'test')`, e.ID)180 case "pending":181 _, _ = tenant.store.DB().ExecContext(ctx, `INSERT INTO pending_events(id,reason) VALUES(?,'git')`, e.ID)182 case "expired":183 _, _ = tenant.store.DB().ExecContext(ctx, `UPDATE events SET expires=1 WHERE id=?`, e.ID)184 }185 }186 if fresh, err := tenant.restoreTargetFresh(ctx); err != nil || fresh {187 t.Fatalf("used relay fresh=%v err=%v", fresh, err)188 }189 })190 }191}192193func TestRestoreFreshnessOnlyExemptsRelayBootstrap30078(t *testing.T) {194 _, tenant := testTenant(t)195 ctx := context.Background()196 if fresh, err := tenant.restoreTargetFresh(ctx); err != nil || !fresh {197 t.Fatalf("new relay fresh=%v err=%v", fresh, err)198 }199 // Permanent ownership emits a relay-authored 30078 before the restore form200 // is available. That bootstrap row is the only 30078 row the freshness201 // check may ignore.202 if _, err := tenant.store.DB().ExecContext(ctx, `INSERT INTO events(id,pubkey,created_at,kind,d,raw) VALUES(?,?,?,?,?,?)`,203 "bootstrap-30078", tenant.records.PublicKey(), time.Now().Unix(), 30078, "", `{"id":"bootstrap-30078","pubkey":"relay","kind":30078}`); err != nil {204 t.Fatal(err)205 }206 if fresh, err := tenant.restoreTargetFresh(ctx); err != nil || !fresh {207 t.Fatalf("relay bootstrap made target stale: fresh=%v err=%v", fresh, err)208 }209 if _, err := tenant.store.DB().ExecContext(ctx, `INSERT INTO events(id,pubkey,created_at,kind,d,raw) VALUES(?,?,?,?,?,?)`,210 "user-30078", tenant.Policy().Owner, time.Now().Unix(), 30078, "", `{"id":"user-30078","pubkey":"owner","kind":30078}`); err != nil {211 t.Fatal(err)212 }213 if fresh, err := tenant.restoreTargetFresh(ctx); err != nil || fresh {214 t.Fatalf("user 30078 was incorrectly exempted: fresh=%v err=%v", fresh, err)215 }216}