clone

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

feat/standalone-relay
tinyrelay / internal / sites / sites_test.go
sites_test.go 7.6K
1package sites23import (4 "context"5 "database/sql"6 "io"7 "net"8 "net/http"9 "net/http/httptest"10 "path/filepath"11 "strings"12 "testing"1314 "github.com/FelineStateMachine/tinyrelay/internal/event"15 "github.com/FelineStateMachine/tinyrelay/internal/policy"16 "github.com/FelineStateMachine/tinyrelay/internal/storage"17)1819func 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 := root31 named.Kind = KindNamedSite32 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 := root37 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}4243func 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"}, nil52 }})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.MethodHead96 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}108109func 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 []byte117 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)}, nil119 }, 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 error121 saved, err = io.ReadAll(body)122 return err123 }})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}135136func 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 int154 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}161162func 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}