| 1 | package tinyclient |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "net/http" |
| 7 | "net/http/httptest" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 12 | ) |
| 13 | |
| 14 | type socialBackend struct { |
| 15 | fakeBackend |
| 16 | method string |
| 17 | query map[string]any |
| 18 | } |
| 19 | |
| 20 | func (b *socialBackend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) { |
| 21 | if method != "browsesocial" && method != "browsesocialthread" { |
| 22 | return b.fakeBackend.Query(ctx, method, params, actor) |
| 23 | } |
| 24 | b.method = method |
| 25 | if len(params) > 0 { |
| 26 | if err := json.Unmarshal(params[0], &b.query); err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | } |
| 30 | post := map[string]any{"id" : strings.Repeat("b" , 64), "kind" : 1, "pubkey" : strings.Repeat("a" , 64), "created_at" : 1789000000, "content" : "A small moment worth sharing." , "author_name" : "River" , "author_picture" : "https://example.com/avatar.png" , "reply_count" : 2, "reactions" : []any{map[string]any{"content" : "+" , "count" : 3, "reacted" : false}, map[string]any{"content" : ":party:" , "image" : "https://example.com/party.png" , "count" : 1}}, "read_path" : "/social/" + strings.Repeat("b" , 64)} |
| 31 | if method == "browsesocialthread" { |
| 32 | return map[string]any{"post" : post, "comments" : []any{}, "next_cursor" : "" }, nil |
| 33 | } |
| 34 | return map[string]any{"items" : []any{post}, "next_cursor" : "older-page" , "stats" : map[string]any{"notes" : 1, "articles" : 0, "comments" : 2}}, nil |
| 35 | } |
| 36 | |
| 37 | func TestSocialPageRendersFeedWithoutJavaScript(t *testing.T) { |
| 38 | b := &socialBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a" , 64))}} |
| 39 | a, err := New(b, Options{}) |
| 40 | if err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | r := httptest.NewRecorder() |
| 44 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/social?kind=notes&author=" +strings.Repeat("a" , 64)+"&cursor=older&q=moment" , nil)) |
| 45 | for _, want := range []string{"Social" , "A small moment worth sharing." , "River" , "/social" , "older-page" , `data-reaction-emoji` , `alt=":party:"` } { |
| 46 | if r.Code != http.StatusOK || !strings.Contains(r.Body.String(), want) { |
| 47 | t.Fatalf("missing %q: status=%d body=%s" , want, r.Code, r.Body.String()) |
| 48 | } |
| 49 | } |
| 50 | if b.method != "browsesocial" || b.query["kind" ] != "notes" || b.query["cursor" ] != "older" || b.query["q" ] != "moment" { |
| 51 | t.Fatalf("query: %s %#v" , b.method, b.query) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func TestSocialThreadAndLegacyArticleRoute(t *testing.T) { |
| 56 | b := &socialBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a" , 64))}} |
| 57 | a, err := New(b, Options{}) |
| 58 | if err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | r := httptest.NewRecorder() |
| 62 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/social/" +strings.Repeat("b" , 64), nil)) |
| 63 | if r.Code != http.StatusOK || b.method != "browsesocialthread" || b.query["id" ] != strings.Repeat("b" , 64) || !strings.Contains(r.Body.String(), "A small moment worth sharing." ) { |
| 64 | t.Fatalf("thread: %d %s" , r.Code, r.Body.String()) |
| 65 | } |
| 66 | r = httptest.NewRecorder() |
| 67 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/articles?author=alice" , nil)) |
| 68 | if r.Code != http.StatusMovedPermanently || r.Header().Get("Location" ) != "/social?author=alice" { |
| 69 | t.Fatalf("legacy: %d %s" , r.Code, r.Header().Get("Location" )) |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | func TestNostrReferenceRoutesUseSocialThread(t *testing.T) { |
| 74 | owner := strings.Repeat("a" , 64) |
| 75 | b := &socialBackend{fakeBackend: fakeBackend{policy: policy.Defaults(owner)}} |
| 76 | a, err := New(b, Options{}) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | id := strings.Repeat("b" , 64) |
| 81 | r := httptest.NewRecorder() |
| 82 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/e/" +id, nil)) |
| 83 | if r.Code != http.StatusOK || b.method != "browsesocialthread" || b.query["id" ] != id || !strings.Contains(r.Body.String(), "A small moment worth sharing." ) { |
| 84 | t.Fatalf("event reference: status=%d method=%s query=%#v" , r.Code, b.method, b.query) |
| 85 | } |
| 86 | |
| 87 | b.method, b.query = "" , nil |
| 88 | address := "30023:" + owner + ":hello" |
| 89 | r = httptest.NewRecorder() |
| 90 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/a/" +address, nil)) |
| 91 | if r.Code != http.StatusOK || b.method != "browsesocialthread" || b.query["address" ] != address { |
| 92 | t.Fatalf("article reference: status=%d method=%s query=%#v" , r.Code, b.method, b.query) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func TestNostrReferenceRoutesKeepLegacyFallback(t *testing.T) { |
| 97 | owner := strings.Repeat("a" , 64) |
| 98 | b := &fakeBackend{policy: policy.Defaults(owner)} |
| 99 | a, err := New(b, Options{}) |
| 100 | if err != nil { |
| 101 | t.Fatal(err) |
| 102 | } |
| 103 | r := httptest.NewRecorder() |
| 104 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, "/a/legacy-name" , nil)) |
| 105 | if r.Code != http.StatusOK || b.call != "queryevents:" { |
| 106 | t.Fatalf("legacy article fallback: status=%d call=%q" , r.Code, b.call) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | func TestSocialRespectsPagesFeature(t *testing.T) { |
| 111 | b := &socialBackend{fakeBackend: fakeBackend{policy: policy.Defaults(strings.Repeat("a" , 64))}} |
| 112 | b.policy.Features.Pages = false |
| 113 | a, err := New(b, Options{}) |
| 114 | if err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | for _, path := range []string{"/social" , "/social/" + strings.Repeat("b" , 64), "/social.json" , "/articles" } { |
| 118 | r := httptest.NewRecorder() |
| 119 | a.ServeHTTP(r, httptest.NewRequest(http.MethodGet, path, nil)) |
| 120 | if r.Code != http.StatusNotFound { |
| 121 | t.Fatalf("%s status %d" , path, r.Code) |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | func TestSocialContextLinksToParentAndRoot(t *testing.T) { |
| 127 | root, parent := strings.Repeat("a" , 64), strings.Repeat("b" , 64) |
| 128 | data := PageData{Tab: "social-thread" , Event: map[string]any{"post" : map[string]any{"id" : root}}} |
| 129 | row := socialContext(map[string]any{"id" : strings.Repeat("c" , 64), "parent_id" : parent, "root_id" : root}, data) |
| 130 | if row["parent_url" ] != "/social/" +parent || row["root_url" ] != "/social/" +root || row["nested" ] != true { |
| 131 | t.Fatalf("missing nested context: %#v" , row) |
| 132 | } |
| 133 | address := "30023:" + root + ":essay:part-one" |
| 134 | row = socialContext(map[string]any{"id" : parent, "parent_id" : root, "parent_address" : address, "root_address" : address}, data) |
| 135 | if row["nested" ] != false || !strings.Contains(plainString(row["parent_url" ]), "address=30023%3A" ) { |
| 136 | t.Fatalf("missing article parent: %#v" , row) |
| 137 | } |
| 138 | } |