clone

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

feat/standalone-relay
tinyrelay / internal / daemon / daemon_test.go
daemon_test.go 5.0K
1package daemon23import (4 "context"5 "crypto/sha256"6 "encoding/base64"7 "encoding/hex"8 "encoding/json"9 "net/http"10 "net/http/httptest"11 "strings"12 "testing"13 "time"1415 "github.com/FelineStateMachine/tinyrelay/internal/blob"16 "github.com/FelineStateMachine/tinyrelay/internal/event"17)1819func TestTenantIsolationAndDurableHTTPPublish(t *testing.T) {20 ctx := context.Background()21 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "alice"})22 if err != nil {23 t.Fatal(err)24 }25 t.Cleanup(func() {26 if err := app.Close(ctx); err != nil {27 t.Error(err)28 }29 })30 owner, err := event.PublicKey(strings.Repeat("0", 63) + "1")31 if err != nil {32 t.Fatal(err)33 }34 for _, name := range []string{"alice", "bob"} {35 if _, err := app.Create(ctx, CreateOptions{Name: name, Owner: owner, Template: "default"}); err != nil {36 t.Fatal(err)37 }38 }39 e := event.Event{Kind: 1, CreatedAt: time.Now().Unix(), Tags: [][]string{}, Content: "hello tiny"}40 if err := event.Sign(&e, strings.Repeat("0", 63)+"1"); err != nil {41 t.Fatal(err)42 }43 raw, err := json.Marshal(e)44 if err != nil {45 t.Fatal(err)46 }47 request := httptest.NewRequest(http.MethodPost, "http://relay.test/r/alice/events", strings.NewReader(string(raw)))48 signRequest(t, request, string(raw))49 response := httptest.NewRecorder()50 app.ServeHTTP(response, request)51 if response.Code != 200 {52 t.Fatalf("publish %d %s", response.Code, response.Body.String())53 }54 for _, tc := range []struct {55 name string56 count int57 }{{"alice", 1}, {"bob", 0}} {58 request := httptest.NewRequest(http.MethodPost, "http://relay.test/r/"+tc.name+"/query", strings.NewReader(`[{"kinds":[1]}]`))59 signRequest(t, request, `[{"kinds":[1]}]`)60 response := httptest.NewRecorder()61 app.ServeHTTP(response, request)62 var events []event.Event63 if err := json.Unmarshal(response.Body.Bytes(), &events); err != nil {64 t.Fatalf("query %s: %s %v", tc.name, response.Body.String(), err)65 }66 if len(events) != tc.count {67 t.Fatalf("%s: got%d want%d", tc.name, len(events), tc.count)68 }69 }70}7172func signRequest(t *testing.T, r *http.Request, body string) {73 t.Helper()74 hash := sha256.Sum256([]byte(body))75 tags := [][]string{{"u", r.URL.String()}, {"method", r.Method}, {"payload", hex.EncodeToString(hash[:])}}76 if i := strings.Index(r.URL.Path, ".git/"); i >= 0 {77 tags = [][]string{{"u", r.URL.Scheme + "://" + r.URL.Host + r.URL.Path[:i+4]}, {"method", http.MethodGet}}78 }79 e := event.Event{Kind: 27235, CreatedAt: time.Now().Unix(), Content: "", Tags: tags}80 if err := event.Sign(&e, strings.Repeat("0", 63)+"1"); err != nil {81 t.Fatal(err)82 }83 raw, err := json.Marshal(e)84 if err != nil {85 t.Fatal(err)86 }87 r.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(raw))88}8990func TestBUD13RemoteNIP98AcceptsAbsentOrEmptyPayloadOnly(t *testing.T) {91 _, tenant := testTenant(t)92 sha := strings.Repeat("a", 64)93 empty := sha256.Sum256(nil)94 for name, payload := range map[string]string{"absent": "", "empty": hex.EncodeToString(empty[:]), "wrong": strings.Repeat("b", 64)} {95 t.Run(name, func(t *testing.T) {96 req := httptest.NewRequest(http.MethodPut, "http://relay.test/"+sha+"?url=https%3A%2F%2Forigin.example%2Fblob", http.NoBody)97 tags := [][]string{{"u", req.URL.String()}, {"method", "PUT"}}98 if payload != "" {99 tags = append(tags, []string{"payload", payload})100 }101 e := event.Event{Kind: 27235, CreatedAt: time.Now().Unix(), Tags: tags}102 if err := event.Sign(&e, strings.Repeat("0", 63)+"1"); err != nil {103 t.Fatal(err)104 }105 raw, _ := json.Marshal(e)106 req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(raw))107 _, err := tenant.authorizeBlob(req, blob.ActionUpload)108 if name == "wrong" && err == nil {109 t.Fatal("accepted wrong empty-body payload")110 }111 if name != "wrong" && err != nil {112 t.Fatalf("remote authorization: %v", err)113 }114 })115 }116}117118func TestCreationRequiresOwnerAndInformationHasNoEconomy(t *testing.T) {119 ctx := context.Background()120 app, err := New(ctx, Config{DataDir: t.TempDir(), DefaultTenant: "main"})121 if err != nil {122 t.Fatal(err)123 }124 t.Cleanup(func() {125 if err := app.Close(ctx); err != nil {126 t.Error(err)127 }128 })129 if _, err := app.Create(ctx, CreateOptions{Name: "unowned", Template: "default"}); err == nil {130 t.Fatal("unowned relay allowed")131 }132 owner, err := event.PublicKey(strings.Repeat("0", 63) + "1")133 if err != nil {134 t.Fatal(err)135 }136 if _, err := app.Create(ctx, CreateOptions{Name: "main", Owner: owner, Template: "default"}); err != nil {137 t.Fatal(err)138 }139 req := httptest.NewRequest(http.MethodGet, "http://relay.test/", nil)140 req.Header.Set("Accept", "application/nostr+json")141 res := httptest.NewRecorder()142 app.ServeHTTP(res, req)143 if res.Code != 200 {144 t.Fatalf("info %d %s", res.Code, res.Body.String())145 }146 var info map[string]json.RawMessage147 if err := json.Unmarshal(res.Body.Bytes(), &info); err != nil {148 t.Fatal(err)149 }150 for _, key := range []string{"fees", "payments_url", "fuel", "lease"} {151 if _, ok := info[key]; ok {152 t.Fatalf("hosted economy leaked: %s", key)153 }154 }155}