clone

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

feat/standalone-relay
tinyrelay / internal / daemon / social_regression_test.go
social_regression_test.go 5.9K
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 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}2122func socialRegressionBrowse(t *testing.T, tenant *Tenant, params map[string]any) map[string]any {23 return socialRegressionBrowseAs(t, tenant, tenant.Policy().Owner, params)24}2526func 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}3435func 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 updated := tenant.Policy()65 updated.Reads = "members"66 if err := tenant.applyPolicy(context.Background(), updated); err != nil {67 t.Fatal(err)68 }69 _, err := tenant.Execute(context.Background(), "", "browsesocial", []json.RawMessage{rawJSON(map[string]any{"limit": 10})})70 if err == nil || !strings.Contains(err.Error(), "members-only") {71 t.Fatalf("members-only social feed guest error = %v", err)72 }73}7475func TestSocialRegressionSearchCursorCrossesSparsePages(t *testing.T) {76 _, tenant := testTenant(t)77 secret := strings.Repeat("b", 63) + "c"78 newest := signedEvent(t, secret, 1, 1000, nil, "needle newest")79 rows := []event.Event{newest}80 for at := int64(999); at >= 850; at-- {81 rows = append(rows, signedEvent(t, secret, 1, at, nil, "filler"))82 }83 oldest := signedEvent(t, secret, 1, 1, nil, "needle oldest")84 rows = append(rows, oldest)85 saveSocialRegression(t, tenant, rows...)86 first := socialRegressionBrowse(t, tenant, map[string]any{"q": "needle", "limit": 1})87 items := first["items"].([]socialItem)88 if len(items) != 1 || items[0].ID != newest.ID || first["next_cursor"] == "" {89 t.Fatalf("first search page = %#v", first)90 }91 second := socialRegressionBrowse(t, tenant, map[string]any{"q": "needle", "limit": 1, "cursor": first["next_cursor"]})92 items = second["items"].([]socialItem)93 if len(items) != 1 || items[0].ID != oldest.ID {94 t.Fatalf("second search page = %#v", second)95 }96}9798func TestSocialRegressionAddressRevisionKeepsCommentsAndReactions(t *testing.T) {99 _, tenant := testTenant(t)100 authorSecret := strings.Repeat("c", 63) + "d"101 commenterSecret := strings.Repeat("e", 63) + "f"102 old := signedEvent(t, authorSecret, 30023, 100, [][]string{{"d", "release"}, {"title", "Old"}}, "old")103 current := signedEvent(t, authorSecret, 30023, 200, [][]string{{"d", "release"}, {"title", "Current"}}, "current")104 address := "30023:" + current.PubKey + ":release"105 comment := signedEvent(t, commenterSecret, 1111, 201, [][]string{{"A", address}, {"K", "30023"}, {"P", current.PubKey}, {"a", address}, {"k", "30023"}, {"p", current.PubKey}}, "comment survives revision")106 reaction := signedEvent(t, commenterSecret, 7, 202, [][]string{{"a", address}, {"e", old.ID}}, "+")107 saveSocialRegression(t, tenant, old, current, comment, reaction)108 result := socialRegressionBrowse(t, tenant, map[string]any{"kind": "articles", "limit": 10})109 items := result["items"].([]socialItem)110 if len(items) != 1 || items[0].Address != address || items[0].Title != "Current" {111 t.Fatalf("canonical article = %#v", items)112 }113 if items[0].ReplyCount != 1 || len(items[0].Reactions) != 1 || items[0].Reactions[0].Count != 1 {114 t.Fatalf("address activity lost: %#v", items[0])115 }116}117118func TestSocialRegressionNIP10MentionIsNotReply(t *testing.T) {119 _, tenant := testTenant(t)120 secret := strings.Repeat("d", 63) + "e"121 mentioned := signedEvent(t, secret, 1, 100, nil, "mentioned")122 note := signedEvent(t, secret, 1, 101, [][]string{{"e", mentioned.ID, "", "mention", mentioned.PubKey}, {"q", mentioned.ID}, {"p", mentioned.PubKey}}, "mention only")123 saveSocialRegression(t, tenant, mentioned, note)124 result := socialRegressionBrowse(t, tenant, map[string]any{"limit": 10})125 items := result["items"].([]socialItem)126 if len(items) != 2 {127 t.Fatalf("mention-only note omitted: %#v", items)128 }129 for _, item := range items {130 if item.ID == note.ID && (item.ParentID != "" || item.RootID != note.ID) {131 t.Fatalf("mention-only note treated as reply: %#v", item)132 }133 }134}