| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | func mountedRequest(t *testing.T, app *App, method, path string, body []byte, accept string, signed bool) *httptest.ResponseRecorder { |
| 13 | req := httptest.NewRequest(method, "http://relay.test/r/main" +path, bytes.NewReader(body)) |
| 14 | if accept != "" { |
| 15 | req.Header.Set("Accept" , accept) |
| 16 | } |
| 17 | if signed { |
| 18 | signRequest(t, req, string(body)) |
| 19 | } |
| 20 | res := httptest.NewRecorder() |
| 21 | app.ServeHTTP(res, req) |
| 22 | return res |
| 23 | } |
| 24 | |
| 25 | func TestMountedRouteNIP11AndBridgeAcceptance(t *testing.T) { |
| 26 | app, _ := testTenant(t) |
| 27 | info := mountedRequest(t, app, http.MethodGet, "/" , nil, "application/nostr+json" , false) |
| 28 | if info.Code != http.StatusOK || info.Header().Get("Content-Type" ) != "application/nostr+json" { |
| 29 | t.Fatalf("NIP-11 status/content-type=%d/%q" , info.Code, info.Header().Get("Content-Type" )) |
| 30 | } |
| 31 | var document map[string]any |
| 32 | if err := json.Unmarshal(info.Body.Bytes(), &document); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | if _, ok := document["fuel" ]; ok { |
| 36 | t.Fatal("NIP-11 exposes removed fuel capability" ) |
| 37 | } |
| 38 | |
| 39 | query := []byte(`[{"kinds":[1]}]` ) |
| 40 | unauth := mountedRequest(t, app, http.MethodPost, "/query" , query, "" , false) |
| 41 | if unauth.Code != http.StatusUnauthorized { |
| 42 | t.Fatalf("unsigned bridge query=%d" , unauth.Code) |
| 43 | } |
| 44 | auth := mountedRequest(t, app, http.MethodPost, "/query" , query, "" , true) |
| 45 | if auth.Code != http.StatusOK { |
| 46 | t.Fatalf("signed bridge query=%d body=%s" , auth.Code, auth.Body.String()) |
| 47 | } |
| 48 | bad := mountedRequest(t, app, http.MethodPost, "/query" , []byte(`[{` ), "" , true) |
| 49 | if bad.Code != http.StatusBadRequest { |
| 50 | t.Fatalf("malformed bridge query=%d" , bad.Code) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestMountedRouteNIP86CORSAndPublicPages(t *testing.T) { |
| 55 | app, _ := testTenant(t) |
| 56 | body := []byte(`{"method":"listviews","params":[]}` ) |
| 57 | unauthReq := httptest.NewRequest(http.MethodPost, "http://relay.test/r/main/manage" , bytes.NewReader(body)) |
| 58 | unauthReq.Header.Set("Content-Type" , "application/nostr+json+rpc" ) |
| 59 | unauthRes := httptest.NewRecorder() |
| 60 | app.ServeHTTP(unauthRes, unauthReq) |
| 61 | unauth := unauthRes |
| 62 | if unauth.Code != http.StatusUnauthorized { |
| 63 | t.Fatalf("unsigned NIP-86=%d" , unauth.Code) |
| 64 | } |
| 65 | authReq := httptest.NewRequest(http.MethodPost, "http://relay.test/r/main/manage" , bytes.NewReader(body)) |
| 66 | authReq.Header.Set("Content-Type" , "application/nostr+json+rpc" ) |
| 67 | signRequest(t, authReq, string(body)) |
| 68 | authRes := httptest.NewRecorder() |
| 69 | app.ServeHTTP(authRes, authReq) |
| 70 | auth := authRes |
| 71 | if auth.Code != http.StatusOK { |
| 72 | t.Fatalf("signed NIP-86=%d body=%s" , auth.Code, auth.Body.String()) |
| 73 | } |
| 74 | for _, path := range []string{"/" , "/people" , "/card.json" , "/connect.json" , "/terms" , "/invite/missing" } { |
| 75 | res := mountedRequest(t, app, http.MethodGet, path, nil, "" , path == "/people" || path == "/connect.json" ) |
| 76 | if res.Code != http.StatusOK && !(path == "/invite/missing" && res.Code == http.StatusNotFound) { |
| 77 | t.Fatalf("public route %s=%d body=%s" , path, res.Code, res.Body.String()) |
| 78 | } |
| 79 | } |
| 80 | preflight := mountedRequest(t, app, http.MethodOptions, "/query" , nil, "" , false) |
| 81 | if preflight.Code != http.StatusNoContent || preflight.Header().Get("Access-Control-Allow-Origin" ) != "*" { |
| 82 | t.Fatalf("CORS preflight=%d origin=%q" , preflight.Code, preflight.Header().Get("Access-Control-Allow-Origin" )) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestMountedRouteNIP05WebAddressAndDataAuth(t *testing.T) { |
| 87 | app, tenant := testTenant(t) |
| 88 | p := tenant.Policy() |
| 89 | p.Features.Names = true |
| 90 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | unknown := mountedRequest(t, app, http.MethodGet, "/.well-known/nostr.json?name=missing" , nil, "" , false) |
| 94 | if unknown.Code != http.StatusOK { |
| 95 | t.Fatalf("unknown NIP-05=%d" , unknown.Code) |
| 96 | } |
| 97 | var emptyNIP05 struct { |
| 98 | Names map[string]string `json:"names"` |
| 99 | } |
| 100 | if err := json.Unmarshal(unknown.Body.Bytes(), &emptyNIP05); err != nil || len(emptyNIP05.Names) != 0 { |
| 101 | t.Fatalf("unknown NIP-05 document=%s" , unknown.Body.String()) |
| 102 | } |
| 103 | web := mountedRequest(t, app, http.MethodGet, "/.well-known/nostr.json?path=/npub1missing" , nil, "" , false) |
| 104 | if web.Code != http.StatusUnauthorized { |
| 105 | t.Fatalf("unsigned NIP-AD=%d" , web.Code) |
| 106 | } |
| 107 | importRes := mountedRequest(t, app, http.MethodPut, "/import" , []byte("{}\n" ), "" , false) |
| 108 | if importRes.Code != http.StatusForbidden { |
| 109 | t.Fatalf("unsigned import=%d" , importRes.Code) |
| 110 | } |
| 111 | backup := mountedRequest(t, app, http.MethodGet, "/backups/missing.json" , nil, "" , false) |
| 112 | if backup.Code != http.StatusUnauthorized && backup.Code != http.StatusForbidden && backup.Code != http.StatusNotFound { |
| 113 | t.Fatalf("backup visibility=%d" , backup.Code) |
| 114 | } |
| 115 | if tenant.Policy().Features.Files { |
| 116 | // The dedicated NIP-96 test covers upload/delete; this assertion keeps |
| 117 | // the mounted route's disabled-file behavior explicit for this matrix. |
| 118 | p := tenant.Policy() |
| 119 | p.Features.Files = false |
| 120 | if err := tenant.applyPolicy(context.Background(), p); err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | blob := mountedRequest(t, app, http.MethodGet, "/nip96/missing" , nil, "" , false) |
| 124 | if blob.Code != http.StatusNotFound { |
| 125 | t.Fatalf("disabled blob route=%d" , blob.Code) |
| 126 | } |
| 127 | } |
| 128 | } |