| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 11 | ) |
| 12 | |
| 13 | func TestSocialBrowseChronologicalAndThread(t *testing.T) { |
| 14 | _, tenant := testTenant(t) |
| 15 | owner := tenant.Policy().Owner |
| 16 | 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 | } |
| 53 | |
| 54 | func TestSocialBrowseDoesNotExposeUnrelatedComments(t *testing.T) { |
| 55 | _, tenant := testTenant(t) |
| 56 | owner := tenant.Policy().Owner |
| 57 | 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 | } |
| 73 | |
| 74 | func TestSocialBrowseNIP10NIP22AndRevisionReactions(t *testing.T) { |
| 75 | _, tenant := testTenant(t) |
| 76 | owner := tenant.Policy().Owner |
| 77 | authorSecret := strings.Repeat("1" , 63) + "2" |
| 78 | otherSecret := strings.Repeat("3" , 63) + "4" |
| 79 | note := signedEvent(t, authorSecret, 1, 500, nil, "root note" ) |
| 80 | legacyReply := signedEvent(t, otherSecret, 1, 501, [][]string{{"e" , note.ID, "" }, {"e" , note.ID, "" }}, "legacy reply" ) |
| 81 | nestedReply := signedEvent(t, authorSecret, 1, 502, [][]string{{"e" , note.ID, "" , "root" , note.PubKey}, {"e" , legacyReply.ID, "" , "reply" , legacyReply.PubKey}}, "nested reply" ) |
| 82 | articleOld := signedEvent(t, authorSecret, 30023, 400, [][]string{{"d" , "essay:edition" }, {"title" , "Old" }}, "old article" ) |
| 83 | article := signedEvent(t, authorSecret, 30023, 600, [][]string{{"d" , "essay:edition" }, {"title" , "Current" }}, "current article" ) |
| 84 | articleAddress := "30023:" + article.PubKey + ":essay:edition" |
| 85 | articleComment := signedEvent(t, otherSecret, 1111, 601, [][]string{ |
| 86 | {"A" , articleAddress}, {"E" , articleOld.ID, "" , articleOld.PubKey}, {"K" , "30023" }, {"P" , article.PubKey}, |
| 87 | {"a" , articleAddress}, {"e" , articleOld.ID, "" , articleOld.PubKey}, {"k" , "30023" }, {"p" , articleOld.PubKey}, |
| 88 | }, "article comment" ) |
| 89 | reactionOld := signedEvent(t, otherSecret, 7, 602, [][]string{{"e" , articleOld.ID}}, "" ) |
| 90 | reactionNew := signedEvent(t, otherSecret, 7, 603, [][]string{{"e" , article.ID}, {"a" , articleAddress}}, "" ) |
| 91 | strangerComment := signedEvent(t, otherSecret, 1111, 604, [][]string{{"E" , strings.Repeat("f" , 64)}, {"K" , "30023" }}, "unrelated" ) |
| 92 | for _, row := range []event.Event{note, legacyReply, nestedReply, articleOld, article, articleComment, reactionOld, reactionNew, strangerComment} { |
| 93 | if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | } |
| 97 | 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 { |
| 98 | t.Fatalf("article comment query = %d, %v" , len(check.Events), queryErr) |
| 99 | } |
| 100 | value, err := tenant.Execute(context.Background(), owner, "browsesocial" , []json.RawMessage{rawJSON(map[string]any{"limit" : 20})}) |
| 101 | if err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | feed := value.(map[string]any) |
| 105 | items := feed["items" ].([]socialItem) |
| 106 | if len(items) != 2 || items[0].Kind != 30023 || items[1].Kind != 1 { |
| 107 | t.Fatalf("feed roots = %#v" , items) |
| 108 | } |
| 109 | articleItem := items[0] |
| 110 | if articleItem.Title != "Current" || articleItem.Address != articleAddress || articleItem.ReplyCount != 1 { |
| 111 | t.Fatalf("article item = %#v" , articleItem) |
| 112 | } |
| 113 | if len(articleItem.Reactions) != 1 || articleItem.Reactions[0].Content != "+" || articleItem.Reactions[0].Count != 1 { |
| 114 | t.Fatalf("article reactions = %#v" , articleItem.Reactions) |
| 115 | } |
| 116 | if items[1].ReplyCount != 2 { |
| 117 | t.Fatalf("note replies = %#v" , items[1]) |
| 118 | } |
| 119 | if items[1].RootID != items[1].ID || items[1].ParentID != "" || items[1].RootKind != 1 { |
| 120 | t.Fatalf("root metadata = %#v" , items[1]) |
| 121 | } |
| 122 | detail, err := tenant.Execute(context.Background(), owner, "browsesocialthread" , []json.RawMessage{rawJSON(map[string]any{"id" : nestedReply.ID, "limit" : 20})}) |
| 123 | if err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | selected := detail.(map[string]any)["post" ].(socialItem) |
| 127 | if selected.ParentID != legacyReply.ID || selected.RootID != note.ID || selected.ParentKind != 1 { |
| 128 | t.Fatalf("nested metadata = %#v" , selected) |
| 129 | } |
| 130 | articleDetail, err := tenant.Execute(context.Background(), owner, "browsesocialthread" , []json.RawMessage{rawJSON(map[string]any{"address" : articleAddress, "limit" : 20})}) |
| 131 | if err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | if len(articleDetail.(map[string]any)["comments" ].([]socialItem)) != 1 { |
| 135 | t.Fatal("article comment missing" ) |
| 136 | } |
| 137 | } |