clone

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

main
tinyrelay / internal / daemon / social_browse_test.go
social_browse_test.go 9.4K
1package daemon23import (4 "context"5 "encoding/json"6 "strings"7 "testing"89 "github.com/FelineStateMachine/tinyrelay/internal/event"10 "github.com/FelineStateMachine/tinyrelay/internal/storage"11)1213func TestSocialBrowseChronologicalAndThread(t *testing.T) {14 _, tenant := testTenant(t)15 owner := tenant.Policy().Owner16 secret := strings.Repeat("7", 63) + "8"17 note := signedEvent(t, secret, 1, 200, [][]string{{"subject", "A note"}}, "hello markdown")18 article := signedEvent(t, secret, 30023, 100, [][]string{{"d", "essay"}, {"title", "An essay"}, {"summary", "reference"}}, "long form")19 comment := signedEvent(t, secret, 1, 300, [][]string{{"e", note.ID, "", "root", note.PubKey}}, "reply")20 for _, row := range []event.Event{note, article, comment} {21 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {22 t.Fatal(err)23 }24 }25 raw, err := json.Marshal(map[string]any{"limit": 2})26 if err != nil {27 t.Fatal(err)28 }29 value, err := tenant.Execute(context.Background(), owner, "browsesocial", []json.RawMessage{raw})30 if err != nil {31 t.Fatal(err)32 }33 feed := value.(map[string]any)34 items := feed["items"].([]socialItem)35 if len(items) != 2 || items[0].ID != note.ID || items[1].ID != article.ID {36 t.Fatalf("items = %#v", items)37 }38 check, _ := tenant.store.Query(context.Background(), event.Filter{Kinds: []int{1}, Tags: map[string][]string{"e": []string{note.ID}}}, storage.QueryOptions{Now: 999, Access: storage.Access{All: true}})39 if len(check.Events) != 1 {40 t.Fatalf("stored comments = %d", len(check.Events))41 }42 if items[0].ReplyCount != 1 {43 t.Fatalf("reply count = %d", items[0].ReplyCount)44 }45 detail, err := tenant.Execute(context.Background(), owner, "browsesocialthread", []json.RawMessage{json.RawMessage(`{"id":"` + note.ID + `"}`)})46 if err != nil {47 t.Fatal(err)48 }49 if len(detail.(map[string]any)["comments"].([]socialItem)) != 1 {50 t.Fatal("comment missing")51 }52}5354func TestSocialBrowseDoesNotExposeUnrelatedComments(t *testing.T) {55 _, tenant := testTenant(t)56 owner := tenant.Policy().Owner57 secret := strings.Repeat("8", 63) + "9"58 note := signedEvent(t, secret, 1, 100, nil, "note")59 comment := signedEvent(t, secret, 1111, 200, [][]string{{"e", strings.Repeat("a", 64)}, {"k", "1"}}, "private-looking")60 for _, row := range []event.Event{note, comment} {61 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {62 t.Fatal(err)63 }64 }65 value, err := tenant.Execute(context.Background(), owner, "browsesocial", nil)66 if err != nil {67 t.Fatal(err)68 }69 if len(value.(map[string]any)["items"].([]socialItem)) != 1 {70 t.Fatal("unrelated comment leaked into feed")71 }72}7374func TestSocialBrowseCategoriesFilterMediaAuthorsAndPages(t *testing.T) {75 _, tenant := testTenant(t)76 owner := tenant.Policy().Owner77 authorSecret := strings.Repeat("1", 63) + "2"78 otherSecret := strings.Repeat("3", 63) + "4"79 photoOld := signedEvent(t, authorSecret, 1, 100, [][]string{{"imeta", "url https://files.example/old.jpg", "m image/jpeg"}}, "old photo")80 note := signedEvent(t, authorSecret, 1, 200, nil, "plain note")81 video := signedEvent(t, otherSecret, 1, 300, [][]string{{"imeta", "url https://files.example/clip.mp4", "m video/mp4"}}, "video")82 photoNew := signedEvent(t, authorSecret, 30023, 400, [][]string{{"d", "photo"}, {"image", "https://files.example/new.png"}}, "new photo")83 podcast := signedEvent(t, authorSecret, 30023, 500, [][]string{{"d", "podcast"}, {"imeta", "url https://files.example/episode.mp3", "m audio/mpeg"}}, "episode")84 native := signedEvent(t, authorSecret, 54, 600, [][]string{{"title", "Native episode"}, {"audio", "https://files.example/native.mp3", "audio/mpeg"}}, "Show notes")85 for _, row := range []event.Event{photoOld, note, video, photoNew, podcast, native} {86 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {87 t.Fatal(err)88 }89 }90 for _, tc := range []struct {91 name, kind string92 want []string93 }{94 {"photos", "photos", []string{photoNew.ID, photoOld.ID}},95 {"videos", "videos", []string{video.ID}},96 {"podcasts", "podcasts", []string{native.ID}},97 {"posts", "posts", []string{podcast.ID, photoNew.ID}},98 } {99 t.Run(tc.name, func(t *testing.T) {100 value, err := tenant.Execute(context.Background(), owner, "browsesocial", []json.RawMessage{rawJSON(map[string]any{"kind": tc.kind, "limit": 20})})101 if err != nil {102 t.Fatal(err)103 }104 items := value.(map[string]any)["items"].([]socialItem)105 if len(items) != len(tc.want) {106 t.Fatalf("items = %#v", items)107 }108 for i, want := range tc.want {109 if items[i].ID != want {110 t.Fatalf("item %d = %s, want %s", i, items[i].ID, want)111 }112 }113 })114 }115 value, err := tenant.Execute(context.Background(), owner, "browsesocial", []json.RawMessage{rawJSON(map[string]any{"kind": "photos", "author": photoOld.PubKey, "limit": 1})})116 if err != nil {117 t.Fatal(err)118 }119 page := value.(map[string]any)120 items := page["items"].([]socialItem)121 if len(items) != 1 || items[0].ID != photoNew.ID || page["next_cursor"] == "" {122 t.Fatalf("first author page = %#v", page)123 }124 value, err = tenant.Execute(context.Background(), owner, "browsesocial", []json.RawMessage{rawJSON(map[string]any{"kind": "photos", "author": photoOld.PubKey, "limit": 1, "cursor": page["next_cursor"]})})125 if err != nil {126 t.Fatal(err)127 }128 items = value.(map[string]any)["items"].([]socialItem)129 if len(items) != 1 || items[0].ID != photoOld.ID {130 t.Fatalf("second author page = %#v", items)131 }132}133134func TestSocialBrowseNIP10NIP22AndRevisionReactions(t *testing.T) {135 _, tenant := testTenant(t)136 owner := tenant.Policy().Owner137 authorSecret := strings.Repeat("1", 63) + "2"138 otherSecret := strings.Repeat("3", 63) + "4"139 note := signedEvent(t, authorSecret, 1, 500, nil, "root note")140 legacyReply := signedEvent(t, otherSecret, 1, 501, [][]string{{"e", note.ID, ""}, {"e", note.ID, ""}}, "legacy reply")141 nestedReply := signedEvent(t, authorSecret, 1, 502, [][]string{{"e", note.ID, "", "root", note.PubKey}, {"e", legacyReply.ID, "", "reply", legacyReply.PubKey}}, "nested reply")142 articleOld := signedEvent(t, authorSecret, 30023, 400, [][]string{{"d", "essay:edition"}, {"title", "Old"}}, "old article")143 article := signedEvent(t, authorSecret, 30023, 600, [][]string{{"d", "essay:edition"}, {"title", "Current"}}, "current article")144 articleAddress := "30023:" + article.PubKey + ":essay:edition"145 articleComment := signedEvent(t, otherSecret, 1111, 601, [][]string{146 {"A", articleAddress}, {"E", articleOld.ID, "", articleOld.PubKey}, {"K", "30023"}, {"P", article.PubKey},147 {"a", articleAddress}, {"e", articleOld.ID, "", articleOld.PubKey}, {"k", "30023"}, {"p", articleOld.PubKey},148 }, "article comment")149 reactionOld := signedEvent(t, otherSecret, 7, 602, [][]string{{"e", articleOld.ID}}, "")150 reactionNew := signedEvent(t, otherSecret, 7, 603, [][]string{{"e", article.ID}, {"a", articleAddress}}, "")151 strangerComment := signedEvent(t, otherSecret, 1111, 604, [][]string{{"E", strings.Repeat("f", 64)}, {"K", "30023"}}, "unrelated")152 for _, row := range []event.Event{note, legacyReply, nestedReply, articleOld, article, articleComment, reactionOld, reactionNew, strangerComment} {153 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {154 t.Fatal(err)155 }156 }157 if check, queryErr := tenant.store.Query(context.Background(), event.Filter{Kinds: []int{1111}, Tags: map[string][]string{"A": {articleAddress}}}, storage.QueryOptions{Now: 999, Access: storage.Access{All: true}}); queryErr != nil || len(check.Events) != 1 {158 t.Fatalf("article comment query = %d, %v", len(check.Events), queryErr)159 }160 value, err := tenant.Execute(context.Background(), owner, "browsesocial", []json.RawMessage{rawJSON(map[string]any{"limit": 20})})161 if err != nil {162 t.Fatal(err)163 }164 feed := value.(map[string]any)165 items := feed["items"].([]socialItem)166 if len(items) != 2 || items[0].Kind != 30023 || items[1].Kind != 1 {167 t.Fatalf("feed roots = %#v", items)168 }169 articleItem := items[0]170 if articleItem.Title != "Current" || articleItem.Address != articleAddress || articleItem.ReplyCount != 1 {171 t.Fatalf("article item = %#v", articleItem)172 }173 if len(articleItem.Reactions) != 1 || articleItem.Reactions[0].Content != "+" || articleItem.Reactions[0].Count != 1 {174 t.Fatalf("article reactions = %#v", articleItem.Reactions)175 }176 if items[1].ReplyCount != 2 {177 t.Fatalf("note replies = %#v", items[1])178 }179 if items[1].RootID != items[1].ID || items[1].ParentID != "" || items[1].RootKind != 1 {180 t.Fatalf("root metadata = %#v", items[1])181 }182 detail, err := tenant.Execute(context.Background(), owner, "browsesocialthread", []json.RawMessage{rawJSON(map[string]any{"id": nestedReply.ID, "limit": 20})})183 if err != nil {184 t.Fatal(err)185 }186 selected := detail.(map[string]any)["post"].(socialItem)187 if selected.ParentID != legacyReply.ID || selected.RootID != note.ID || selected.ParentKind != 1 {188 t.Fatalf("nested metadata = %#v", selected)189 }190 articleDetail, err := tenant.Execute(context.Background(), owner, "browsesocialthread", []json.RawMessage{rawJSON(map[string]any{"address": articleAddress, "limit": 20})})191 if err != nil {192 t.Fatal(err)193 }194 if len(articleDetail.(map[string]any)["comments"].([]socialItem)) != 1 {195 t.Fatal("article comment missing")196 }197}