tinyrelayfile
tinyrelay/internal/daemon/data_http_test.go/

internal/daemon/data_http_test.go raw
1package daemon
2
3import (
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"
14
15 "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)
20
21func 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}
31
32func 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}
42
43func 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}
75
76func 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}
84
85func 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 string
120 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 int
124 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}
152
153func 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.Git
165 }
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}
192
193func 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 form
200 // is available. That bootstrap row is the only 30078 row the freshness
201 // 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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs