| 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 saveSocialRegression(t *testing.T, tenant *Tenant, rows ...event.Event) { |
| 14 | t.Helper() |
| 15 | for _, row := range rows { |
| 16 | if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil { |
| 17 | t.Fatal(err) |
| 18 | } |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | func socialRegressionBrowse(t *testing.T, tenant *Tenant, params map[string]any) map[string]any { |
| 23 | return socialRegressionBrowseAs(t, tenant, tenant.Policy().Owner, params) |
| 24 | } |
| 25 | |
| 26 | func socialRegressionBrowseAs(t *testing.T, tenant *Tenant, actor string, params map[string]any) map[string]any { |
| 27 | t.Helper() |
| 28 | value, err := tenant.Execute(context.Background(), actor, "browsesocial" , []json.RawMessage{rawJSON(params)}) |
| 29 | if err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | return value.(map[string]any) |
| 33 | } |
| 34 | |
| 35 | func TestSocialRegressionHiddenActivityAndProfilesAreNotFolded(t *testing.T) { |
| 36 | _, tenant := testTenant(t) |
| 37 | secret := strings.Repeat("a" , 63) + "b" |
| 38 | note := signedEvent(t, secret, 1, 100, nil, "visible note" ) |
| 39 | comment := signedEvent(t, secret, 1, 101, [][]string{{"e" , note.ID, "" , "root" , note.PubKey}}, "hidden comment" ) |
| 40 | reaction := signedEvent(t, secret, 7, 102, [][]string{{"e" , note.ID}, {"p" , note.PubKey}}, "+" ) |
| 41 | profile := signedEvent(t, secret, 0, 103, nil, `{"name":"leaked name","picture":"https://example.test/avatar.png"}` ) |
| 42 | saveSocialRegression(t, tenant, note, comment, reaction, profile) |
| 43 | visible := socialRegressionBrowse(t, tenant, map[string]any{"limit" : 10})["items" ].([]socialItem) |
| 44 | if len(visible) != 1 || visible[0].ReplyCount != 1 || len(visible[0].Reactions) != 1 || visible[0].AuthorName != "leaked name" { |
| 45 | t.Fatalf("visible activity was not folded before hiding: %#v" , visible) |
| 46 | } |
| 47 | ctx := context.Background() |
| 48 | for _, row := range []event.Event{comment, reaction, profile} { |
| 49 | if _, err := tenant.store.DB().ExecContext(ctx, `INSERT INTO hidden_events(id,reason) VALUES(?,'regression')` , row.ID); err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | } |
| 53 | result := socialRegressionBrowse(t, tenant, map[string]any{"limit" : 10}) |
| 54 | items := result["items" ].([]socialItem) |
| 55 | if len(items) != 1 || items[0].ID != note.ID { |
| 56 | t.Fatalf("items = %#v" , items) |
| 57 | } |
| 58 | if items[0].ReplyCount != 0 || len(items[0].Reactions) != 0 { |
| 59 | t.Fatalf("hidden activity folded into note: %#v" , items[0]) |
| 60 | } |
| 61 | if items[0].AuthorName == "leaked name" || items[0].AuthorPicture != "" { |
| 62 | t.Fatalf("hidden profile leaked: %#v" , items[0]) |
| 63 | } |
| 64 | tenant.policy.Reads = "members" |
| 65 | _, err := tenant.Execute(context.Background(), "" , "browsesocial" , []json.RawMessage{rawJSON(map[string]any{"limit" : 10})}) |
| 66 | if err == nil || !strings.Contains(err.Error(), "members-only" ) { |
| 67 | t.Fatalf("members-only social feed guest error = %v" , err) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestSocialRegressionSearchCursorCrossesSparsePages(t *testing.T) { |
| 72 | _, tenant := testTenant(t) |
| 73 | secret := strings.Repeat("b" , 63) + "c" |
| 74 | newest := signedEvent(t, secret, 1, 1000, nil, "needle newest" ) |
| 75 | rows := []event.Event{newest} |
| 76 | for at := int64(999); at >= 850; at-- { |
| 77 | rows = append(rows, signedEvent(t, secret, 1, at, nil, "filler" )) |
| 78 | } |
| 79 | oldest := signedEvent(t, secret, 1, 1, nil, "needle oldest" ) |
| 80 | rows = append(rows, oldest) |
| 81 | saveSocialRegression(t, tenant, rows...) |
| 82 | first := socialRegressionBrowse(t, tenant, map[string]any{"q" : "needle" , "limit" : 1}) |
| 83 | items := first["items" ].([]socialItem) |
| 84 | if len(items) != 1 || items[0].ID != newest.ID || first["next_cursor" ] == "" { |
| 85 | t.Fatalf("first search page = %#v" , first) |
| 86 | } |
| 87 | second := socialRegressionBrowse(t, tenant, map[string]any{"q" : "needle" , "limit" : 1, "cursor" : first["next_cursor" ]}) |
| 88 | items = second["items" ].([]socialItem) |
| 89 | if len(items) != 1 || items[0].ID != oldest.ID { |
| 90 | t.Fatalf("second search page = %#v" , second) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestSocialRegressionAddressRevisionKeepsCommentsAndReactions(t *testing.T) { |
| 95 | _, tenant := testTenant(t) |
| 96 | authorSecret := strings.Repeat("c" , 63) + "d" |
| 97 | commenterSecret := strings.Repeat("e" , 63) + "f" |
| 98 | old := signedEvent(t, authorSecret, 30023, 100, [][]string{{"d" , "release" }, {"title" , "Old" }}, "old" ) |
| 99 | current := signedEvent(t, authorSecret, 30023, 200, [][]string{{"d" , "release" }, {"title" , "Current" }}, "current" ) |
| 100 | address := "30023:" + current.PubKey + ":release" |
| 101 | comment := signedEvent(t, commenterSecret, 1111, 201, [][]string{{"A" , address}, {"K" , "30023" }, {"P" , current.PubKey}, {"a" , address}, {"k" , "30023" }, {"p" , current.PubKey}}, "comment survives revision" ) |
| 102 | reaction := signedEvent(t, commenterSecret, 7, 202, [][]string{{"a" , address}, {"e" , old.ID}}, "+" ) |
| 103 | saveSocialRegression(t, tenant, old, current, comment, reaction) |
| 104 | result := socialRegressionBrowse(t, tenant, map[string]any{"kind" : "articles" , "limit" : 10}) |
| 105 | items := result["items" ].([]socialItem) |
| 106 | if len(items) != 1 || items[0].Address != address || items[0].Title != "Current" { |
| 107 | t.Fatalf("canonical article = %#v" , items) |
| 108 | } |
| 109 | if items[0].ReplyCount != 1 || len(items[0].Reactions) != 1 || items[0].Reactions[0].Count != 1 { |
| 110 | t.Fatalf("address activity lost: %#v" , items[0]) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func TestSocialRegressionNIP10MentionIsNotReply(t *testing.T) { |
| 115 | _, tenant := testTenant(t) |
| 116 | secret := strings.Repeat("d" , 63) + "e" |
| 117 | mentioned := signedEvent(t, secret, 1, 100, nil, "mentioned" ) |
| 118 | note := signedEvent(t, secret, 1, 101, [][]string{{"e" , mentioned.ID, "" , "mention" , mentioned.PubKey}, {"q" , mentioned.ID}, {"p" , mentioned.PubKey}}, "mention only" ) |
| 119 | saveSocialRegression(t, tenant, mentioned, note) |
| 120 | result := socialRegressionBrowse(t, tenant, map[string]any{"limit" : 10}) |
| 121 | items := result["items" ].([]socialItem) |
| 122 | if len(items) != 2 { |
| 123 | t.Fatalf("mention-only note omitted: %#v" , items) |
| 124 | } |
| 125 | for _, item := range items { |
| 126 | if item.ID == note.ID && (item.ParentID != "" || item.RootID != note.ID) { |
| 127 | t.Fatalf("mention-only note treated as reply: %#v" , item) |
| 128 | } |
| 129 | } |
| 130 | } |