tinyrelayfile
tinyrelay/internal/sites/sites_test.go/

internal/sites/sites_test.go raw
1package sites
2
3import (
4 "context"
5 "database/sql"
6 "io"
7 "net"
8 "net/http"
9 "net/http/httptest"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/FelineStateMachine/tinyrelay/internal/event"
15 "github.com/FelineStateMachine/tinyrelay/internal/policy"
16 "github.com/FelineStateMachine/tinyrelay/internal/storage"
17)
18
19func TestSiteLabelsAndManifestValidation(t *testing.T) {
20 key := "0000000000000000000000000000000000000000000000000000000000000001"
21 hash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
22 paths := [][]string{{"path", "/index.html", hash}, {"path", "/404.html", hash}}
23 root := event.Event{Kind: KindSite, PubKey: key, Tags: paths}
24 if got := SiteLabel(root); got == "" {
25 t.Fatal("root site label is empty")
26 }
27 if err := ValidateManifest(root); err != nil {
28 t.Fatalf("root manifest rejected: %v", err)
29 }
30 named := root
31 named.Kind = KindNamedSite
32 named.Tags = append(append([][]string(nil), paths...), []string{"d", "demo-site"})
33 if err := ValidateManifest(named); err != nil {
34 t.Fatalf("named manifest rejected: %v", err)
35 }
36 bad := root
37 bad.Tags = append(append([][]string(nil), paths...), []string{"path", "/../x.js", hash})
38 if err := ValidateManifest(bad); err == nil {
39 t.Fatal("path traversal manifest accepted")
40 }
41}
42
43func TestHandlerServesVerifiedBlobAndFallback(t *testing.T) {
44 const hash = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
45 store, err := storage.Open(context.Background(), filepath.Join(t.TempDir(), "relay.db"))
46 if err != nil {
47 t.Fatal(err)
48 }
49 t.Cleanup(func() { _ = store.Close() })
50 service, err := New(Config{Store: store, BaseDomain: "example.test", Policy: func() policy.Policy { return policy.Defaults("owner") }, GetBlob: func(context.Context, string) (Blob, error) {
51 return Blob{Body: io.NopCloser(strings.NewReader("hello")), Type: "text/plain"}, nil
52 }})
53 if err != nil {
54 t.Fatal(err)
55 }
56 key := "0000000000000000000000000000000000000000000000000000000000000001"
57 e := event.Event{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Kind: KindSite, PubKey: key, Tags: [][]string{{"path", "/index.html", hash}, {"path", "/blog/index.html", hash}, {"path", "/404.html", hash}}}
58 if _, err := store.Save(context.Background(), e, service.SaveOptions(e, 1)); err != nil {
59 t.Fatal(err)
60 }
61 host := SiteLabel(e) + ".example.test"
62 req := httptest.NewRequest(http.MethodGet, "https://"+host+"/", nil)
63 response := httptest.NewRecorder()
64 service.Handler().ServeHTTP(response, req)
65 if response.Code != http.StatusOK || response.Body.String() != "hello" {
66 t.Fatalf("root = %d %q", response.Code, response.Body.String())
67 }
68 req = httptest.NewRequest(http.MethodGet, "https://"+host+"/index.html", nil)
69 req.Header.Set("Range", "bytes=1-3")
70 response = httptest.NewRecorder()
71 service.Handler().ServeHTTP(response, req)
72 if response.Code != http.StatusPartialContent || response.Body.String() != "ell" {
73 t.Fatalf("range = %d %q", response.Code, response.Body.String())
74 }
75 req = httptest.NewRequest(http.MethodGet, "https://"+host+"/blog?x=1", nil)
76 req.Header.Set("Accept", "text/html")
77 response = httptest.NewRecorder()
78 service.Handler().ServeHTTP(response, req)
79 if response.Code != http.StatusPermanentRedirect {
80 t.Fatalf("directory redirect = %d", response.Code)
81 }
82 location, err := response.Result().Location()
83 if err != nil {
84 t.Fatal(err)
85 }
86 if location.Path != "/blog/" || location.RawQuery != "x=1" {
87 t.Fatalf("directory location = %s", location.String())
88 }
89 req = httptest.NewRequest(http.MethodGet, "https://"+host+"/.well-known/nostr.json?path=%2F", nil)
90 response = httptest.NewRecorder()
91 service.Handler().ServeHTTP(response, req)
92 if response.Code != http.StatusOK || response.Header().Get("Access-Control-Allow-Origin") != "*" || !strings.Contains(response.Body.String(), `"authors"`) {
93 t.Fatalf("discovery = %d %q", response.Code, response.Body.String())
94 }
95 req.Method = http.MethodHead
96 response = httptest.NewRecorder()
97 service.Handler().ServeHTTP(response, req)
98 if response.Code != http.StatusOK || response.Body.Len() != 0 {
99 t.Fatalf("discovery HEAD = %d %q", response.Code, response.Body.String())
100 }
101 req = httptest.NewRequest(http.MethodGet, "https://"+host+"/missing.js", nil)
102 response = httptest.NewRecorder()
103 service.Handler().ServeHTTP(response, req)
104 if response.Code != http.StatusNotFound || response.Body.String() != "hello" {
105 t.Fatalf("fallback = %d %q", response.Code, response.Body.String())
106 }
107}
108
109func TestMirrorUsesVerifiedRemoteAndBlobWriter(t *testing.T) {
110 const hash = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
111 store, err := storage.Open(context.Background(), filepath.Join(t.TempDir(), "relay.db"))
112 if err != nil {
113 t.Fatal(err)
114 }
115 t.Cleanup(func() { _ = store.Close() })
116 var saved []byte
117 service, err := New(Config{Store: store, Policy: func() policy.Policy { return policy.Defaults("owner") }, Fetch: func(context.Context, *http.Request) (*http.Response, error) {
118 return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("hello")), Header: make(http.Header)}, nil
119 }, ResolveIP: func(context.Context, string) ([]net.IP, error) { return []net.IP{{8, 8, 8, 8}}, nil }, PutBlob: func(_ context.Context, _ string, _ string, body io.Reader) error {
120 var err error
121 saved, err = io.ReadAll(body)
122 return err
123 }})
124 if err != nil {
125 t.Fatal(err)
126 }
127 manifest := event.Event{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Kind: KindSite, PubKey: "0000000000000000000000000000000000000000000000000000000000000001", Tags: [][]string{{"path", "/index.html", hash}, {"server", "https://origin.example"}}}
128 if err := service.Mirror(context.Background(), manifest); err != nil {
129 t.Fatal(err)
130 }
131 if string(saved) != "hello" {
132 t.Fatalf("saved = %q", saved)
133 }
134}
135
136func TestManifestIndexFollowsEventDeletion(t *testing.T) {
137 store, err := storage.Open(context.Background(), filepath.Join(t.TempDir(), "relay.db"))
138 if err != nil {
139 t.Fatal(err)
140 }
141 t.Cleanup(func() { _ = store.Close() })
142 service, err := New(Config{Store: store, Policy: func() policy.Policy { return policy.Defaults("owner") }})
143 if err != nil {
144 t.Fatal(err)
145 }
146 e := event.Event{ID: strings.Repeat("b", 64), Kind: KindSite, PubKey: strings.Repeat("0", 63) + "1", Tags: [][]string{{"path", "/index.html", strings.Repeat("a", 64)}}}
147 if _, err := store.Save(context.Background(), e, service.SaveOptions(e, 1)); err != nil {
148 t.Fatal(err)
149 }
150 if err := store.WithTx(context.Background(), func(tx *sql.Tx) error { _, err := tx.Exec("DELETE FROM events WHERE id=?", e.ID); return err }); err != nil {
151 t.Fatal(err)
152 }
153 var count int
154 if err := store.DB().QueryRow("SELECT count(*) FROM site_manifests WHERE event_id=?", e.ID).Scan(&count); err != nil {
155 t.Fatal(err)
156 }
157 if count != 0 {
158 t.Fatalf("manifest index count = %d", count)
159 }
160}
161
162func TestSnapshotAggregateAndParseLabel(t *testing.T) {
163 key := "0000000000000000000000000000000000000000000000000000000000000001"
164 paths := [][]string{{"path", "/index.html", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
165 aggregate := Aggregate(paths)
166 if len(aggregate) != 64 {
167 t.Fatalf("aggregate length = %d", len(aggregate))
168 }
169 snapshot := event.Event{Kind: KindSiteSnapshot, PubKey: key, ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Tags: append(paths, []string{"x", aggregate, "aggregate"}, []string{"a", "15128:" + key + ":"})}
170 if err := ValidateManifest(snapshot); err != nil {
171 t.Fatalf("snapshot rejected: %v", err)
172 }
173 parsed, ok := ParseSite(SiteLabel(snapshot))
174 if !ok || parsed.Kind != KindSiteSnapshot || parsed.ID != snapshot.ID {
175 t.Fatalf("snapshot label did not round trip: %#v", parsed)
176 }
177}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs