clone

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

feat/ui-updates
tinyrelay / internal / daemon / tinyclient_integration_test.go
tinyclient_integration_test.go 4.2K
1package daemon23import (4 "context"5 "encoding/json"6 "io"7 "net/http"8 "net/http/httptest"9 "net/url"10 "strings"11 "testing"1213 "github.com/FelineStateMachine/tinyrelay/tinyclient"14)1516func TestStandaloneTinyclientAuthenticatesAgainstRealRelay(t *testing.T) {17 _, tenant := testTenant(t)18 upstream := httptest.NewServer(tenant)19 defer upstream.Close()20 handler, err := tinyclient.NewRemote(tinyclient.RemoteOptions{BackendURL: upstream.URL, PublicURL: "http://relay.test"})21 if err != nil {22 t.Fatal(err)23 }24 frontend := httptest.NewServer(handler)25 defer frontend.Close()26 frontendURL, _ := url.Parse(frontend.URL)27 client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}28 issue := func(method, path, body string, cookie *http.Cookie, signed bool) (*http.Response, string) {29 t.Helper()30 request := httptest.NewRequest(method, "http://relay.test"+path, strings.NewReader(body))31 request.Header.Set("Content-Type", "application/json")32 request.Header.Set("Origin", "http://relay.test")33 if cookie != nil {34 request.AddCookie(cookie)35 }36 if signed {37 signRequest(t, request, body)38 }39 request.URL.Scheme, request.URL.Host = frontendURL.Scheme, frontendURL.Host40 request.RequestURI = ""41 response, err := client.Do(request)42 if err != nil {43 t.Fatal(err)44 }45 raw, err := io.ReadAll(response.Body)46 response.Body.Close()47 if err != nil {48 t.Fatal(err)49 }50 return response, string(raw)51 }52 login, body := issue("POST", "/session", "", nil, true)53 if login.StatusCode != 200 || len(login.Cookies()) != 1 {54 t.Fatalf("signed login: %d %s", login.StatusCode, body)55 }56 cookie := login.Cookies()[0]57 if !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode {58 t.Fatalf("unsafe cookie: %+v", cookie)59 }60 for _, path := range []string{"/", "/account", "/manage/rules", "/chat", "/scripts/bridge.js", "/manifest.webmanifest"} {61 response, body := issue("GET", path, "", cookie, false)62 integrated := httptest.NewRecorder()63 request := httptest.NewRequest("GET", "http://relay.test"+path, nil)64 request.AddCookie(cookie)65 tenant.ServeHTTP(integrated, request)66 if response.StatusCode != integrated.Code || body != integrated.Body.String() {67 t.Fatalf("standalone parity %s: status=%d want=%d body lengths=%d/%d", path, response.StatusCode, integrated.Code, len(body), integrated.Body.Len())68 }69 }70 denied, _ := issue("POST", "/manage/rpc", `{"method":"stats"}`, cookie, false)71 if denied.StatusCode != 401 {72 t.Fatalf("cookie-only write accepted: %d", denied.StatusCode)73 }74 signed, body := issue("POST", "/manage/rpc", `{"method":"stats"}`, cookie, true)75 if signed.StatusCode != 200 {76 t.Fatalf("signed management request lost URL binding: %d %s", signed.StatusCode, body)77 }78 // The new read bridge ignores claimed pubkeys and still requires a real session.79 forged, body := issue("GET", tinyclient.BackendPath+"?actor="+tenant.Policy().Owner, "", nil, false)80 var snapshot struct {81 Actor string `json:"actor"`82 }83 if forged.StatusCode != 200 || json.Unmarshal([]byte(body), &snapshot) != nil || snapshot.Actor != "" {84 t.Fatalf("forged actor accepted: %d %s", forged.StatusCode, body)85 }86 logout, body := issue("POST", "/session/logout", "", cookie, false)87 if logout.StatusCode != 200 {88 t.Fatalf("logout: %d %s", logout.StatusCode, body)89 }90 response, body := issue("GET", "/account", "", cookie, false)91 if response.StatusCode != 200 || !strings.Contains(body, "Sign in to open your account.") {92 t.Fatalf("revoked session survived: %d", response.StatusCode)93 }94 p := tenant.Policy()95 p.Features.Grasp, p.Features.Grasp08, p.Reads, p.Name = true, true, "members", "private secret project"96 if err := tenant.applyPolicy(context.Background(), p); err != nil {97 t.Fatal(err)98 }99 for _, path := range []string{"/", tinyclient.BackendPath} {100 response, body := issue("GET", path, "", cookie, false)101 if response.StatusCode != 200 || strings.Contains(body, p.Name) || strings.Contains(body, p.Owner) {102 t.Fatalf("private metadata leaked through %s: %d", path, response.StatusCode)103 }104 }105 response, _ = issue("GET", "/social", "", cookie, false)106 if response.StatusCode != 401 {107 t.Fatalf("private read without membership: %d", response.StatusCode)108 }109}