clone

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

feat/tinyagent
tinyrelay / tinyclient / remote_scope_test.go
remote_scope_test.go 3.0K
1package tinyclient23import (4 "net/http"5 "net/http/httptest"6 "strings"7 "sync/atomic"8 "testing"9)1011func TestRemoteRejectsCrossTenantRoutesBeforeRenderingOrProxying(t *testing.T) {12 app, err := New(&fakeBackend{policy: DefaultPolicy(strings.Repeat("a", 64))}, Options{})13 if err != nil {14 t.Fatal(err)15 }16 var calls atomic.Int3217 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {18 calls.Add(1)19 if strings.HasSuffix(r.URL.Path, BackendPath) {20 app.BackendHandler().ServeHTTP(w, r)21 return22 }23 w.WriteHeader(http.StatusAccepted)24 }))25 defer upstream.Close()26 for _, mount := range []string{"", "/r/team"} {27 t.Run("mount="+mount, func(t *testing.T) {28 remote, err := NewRemote(RemoteOptions{BackendURL: upstream.URL + mount, PublicURL: "http://relay.example" + mount})29 if err != nil {30 t.Fatal(err)31 }32 for _, tc := range []struct {33 name, method, path, header, value string34 }{35 {"page", "GET", "/r/other/account", "", ""},36 {"head", "HEAD", "/r/other/account", "", ""},37 {"feed", "GET", "/r/other/articles.json", "", ""},38 {"asset", "GET", "/r/other/scripts/bridge.js", "", ""},39 {"tenant root", "GET", "/r/other", "", ""},40 {"tenant home", "GET", "/r/other/", "", ""},41 {"events", "POST", "/r/other/events", "", ""},42 {"session", "POST", "/r/other/session", "", ""},43 {"delete", "DELETE", "/r/other/events", "", ""},44 {"options", "OPTIONS", "/r/other/events", "", ""},45 {"signed page", "GET", "/r/other/account", "Authorization", "Nostr exact-proof"},46 {"websocket", "GET", "/r/other/", "Upgrade", "websocket"},47 {"nostr info", "GET", "/r/other/", "Accept", "application/nostr+json"},48 {"stream", "GET", "/r/other/rooms/build/stream", "", ""},49 {"adapter", "GET", "/r/other" + BackendPath, "", ""},50 {"encoded tenant prefix", "GET", "/%72/other/account", "", ""},51 } {52 t.Run(tc.name, func(t *testing.T) {53 req := httptest.NewRequest(tc.method, "http://relay.example"+tc.path, nil)54 req.AddCookie(&http.Cookie{Name: "tiny_session", Value: "root-session"})55 if tc.header != "" {56 req.Header.Set(tc.header, tc.value)57 }58 if tc.header == "Upgrade" {59 req.Header.Set("Connection", "Upgrade")60 }61 before := calls.Load()62 got := httptest.NewRecorder()63 remote.ServeHTTP(got, req)64 if got.Code != http.StatusNotFound || calls.Load() != before {65 t.Fatalf("cross-tenant request reached renderer or proxy: status=%d upstream calls=%d", got.Code, calls.Load()-before)66 }67 })68 }69 // The scope guard must not disable the configured tenant's routes.70 for _, tc := range []struct {71 method, path string72 status int73 }{74 {"GET", "/account", 200},75 {"POST", "/events", 202},76 {"GET", "/rooms/build/stream", 202},77 } {78 got := httptest.NewRecorder()79 remote.ServeHTTP(got, httptest.NewRequest(tc.method, "http://relay.example"+mount+tc.path, nil))80 if got.Code != tc.status {81 t.Errorf("valid tenant %s %s: status=%d want=%d", tc.method, tc.path, got.Code, tc.status)82 }83 }84 })85 }86}