clone

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

feat/ui-updates
tinyrelay / internal / records / records_test.go
records_test.go 10.3K
1package records23import (4 "context"5 "path/filepath"6 "testing"78 "github.com/FelineStateMachine/tinyrelay/internal/communityread"9 "github.com/FelineStateMachine/tinyrelay/internal/event"10 "github.com/FelineStateMachine/tinyrelay/internal/policy"11 "github.com/FelineStateMachine/tinyrelay/internal/storage"12)1314type emptyCommunityReader struct{}1516func (emptyCommunityReader) Members(context.Context) ([]communityread.Member, error) {17 return nil, nil18}1920func (emptyCommunityReader) ModerationCounts(context.Context, int64) (communityread.ModerationCounts, error) {21 return communityread.ModerationCounts{}, nil22}2324func (emptyCommunityReader) MemberStatus(context.Context, string) (bool, int, error) {25 return false, 0, nil26}2728func testStore(t *testing.T) (*storage.Store, context.Context) {29 t.Helper()30 ctx := context.Background()31 s, err := storage.Open(ctx, filepath.Join(t.TempDir(), "tenant.db"))32 if err != nil {33 t.Fatal(err)34 }35 t.Cleanup(func() { _ = s.Close() })36 return s, ctx37}3839func TestIdentitySurvivesRestartAndClockIsMonotonic(t *testing.T) {40 ctx := context.Background()41 path := filepath.Join(t.TempDir(), "tenant.db")42 s, err := storage.Open(ctx, path)43 if err != nil {44 t.Fatal(err)45 }46 p := policy.Defaults("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")47 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})48 if err != nil {49 t.Fatal(err)50 }51 first, err := r.Generate(ctx, event.KIND_APP_DATA, [][]string{{"d", "x"}}, "one", 100)52 if err != nil {53 t.Fatal(err)54 }55 if err := event.Validate(first); err != nil {56 t.Fatal(err)57 }58 key := r.PublicKey()59 if err := s.Close(); err != nil {60 t.Fatal(err)61 }62 s2, err := storage.Open(ctx, path)63 if err != nil {64 t.Fatal(err)65 }66 r2, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s2, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})67 if err != nil {68 t.Fatal(err)69 }70 if r2.PublicKey() != key {71 t.Fatalf("identity changed across restart: %s != %s", r2.PublicKey(), key)72 }73 second, err := r2.Generate(ctx, event.KIND_APP_DATA, [][]string{{"d", "x"}}, "two", 1)74 if err != nil {75 t.Fatal(err)76 }77 if second.CreatedAt <= first.CreatedAt {78 t.Fatalf("clock regressed: %d <= %d", second.CreatedAt, first.CreatedAt)79 }80 if err := event.Validate(second); err != nil {81 t.Fatal(err)82 }83 _ = s2.Close()84}8586func TestIdentityContinuityAndViewPrivacy(t *testing.T) {87 s, ctx := testStore(t)88 p := policy.Defaults("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")89 p.Views = map[string]string{"profiles": "daily"}90 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"91 p.Owner = owner92 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})93 if err != nil {94 t.Fatal(err)95 }96 key := r.PublicKey()97 p.Reads = "members"98 if _, err := r.Views(ctx, policy.Access{}, 100); err == nil {99 t.Fatal("private view leaked to guest")100 }101 if _, err := r.Views(ctx, policy.Access{Owner: true}, 100); err != nil {102 t.Fatal(err)103 }104 if r.PublicKey() != key {105 t.Fatal("public identity changed")106 }107}108109func TestDirectoryPrivateViewsRequireMembersAndDoNotTick(t *testing.T) {110 s, ctx := testStore(t)111 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"112 p := policy.Defaults(owner)113 p.DirectoryPublic = false114 p.Views = map[string]string{"profiles": "daily"}115 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example"})116 if err != nil {117 t.Fatal(err)118 }119 if _, err := r.Views(ctx, policy.Access{}, 100); err == nil {120 t.Fatal("private directory view leaked to guest")121 }122 if err := r.Tick(ctx, 100); err != nil {123 t.Fatal(err)124 }125 var count int126 if err := s.DB().QueryRow(`SELECT count(*) FROM events WHERE kind=?`, event.KIND_VIEW).Scan(&count); err != nil {127 t.Fatal(err)128 }129 if count != 0 {130 t.Fatalf("private tick stored %d views", count)131 }132}133134func TestViewAudienceAndMemberFoldAreNotStored(t *testing.T) {135 s, ctx := testStore(t)136 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"137 p := policy.Defaults(owner)138 p.DirectoryPublic = false139 p.Reads = "open"140 p.Views = map[string]string{"profiles": "daily", "relays": "off", "calendar": "daily", "moderation": "off", "articles": "off", "podcasts": "off", "zaps": "off", "presence": "off"}141 generated := []event.Event{}142 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, RelayURL: "wss://relay.example", OnGenerated: func(context.Context, event.Event) error {143 generated = append(generated, event.Event{})144 return nil145 }})146 if err != nil {147 t.Fatal(err)148 }149 // Calendar is read-public even when the directory is private, while the150 // profile fold is member-only and must be signed without persistence.151 views, err := r.Views(ctx, policy.Access{}, 100)152 if err == nil {153 t.Fatal("guest received a member-only profile view")154 }155 if err := r.Tick(ctx, 100); err != nil {156 t.Fatal(err)157 }158 if len(generated) != 1 {159 t.Fatalf("generated callbacks=%d, want public calendar only", len(generated))160 }161 _ = views162}163164func TestPresenceIsSignedWithoutGeneratedCallback(t *testing.T) {165 s, ctx := testStore(t)166 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"167 p := policy.Defaults(owner)168 called := 0169 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error {170 called++171 return nil172 }})173 if err != nil {174 t.Fatal(err)175 }176 if _, err := r.Presence(ctx, policy.Access{Owner: true}, 100); err != nil {177 t.Fatal(err)178 }179 if called != 0 {180 t.Fatalf("presence invoked generated callback %d times", called)181 }182}183184func TestProfileFoldUsesMembersAndNIP05(t *testing.T) {185 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"186 member := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"187 e := event.Event{PubKey: owner, Kind: event.KIND_PROFILE, CreatedAt: 1, Content: `{"name":"Owner"}`}188 tags, _ := profileFold([]event.Event{e}, "wss://relay.example", map[string]string{owner: "", member: "alice"}, []string{owner, member})189 if len(tags) != 2 || tags[1][1] != member || tags[1][4] != "alice@relay.example" {190 t.Fatalf("profile fold=%v", tags)191 }192}193194func TestHourlyViewFingerprintSkipsUnchangedRun(t *testing.T) {195 s, ctx := testStore(t)196 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"197 p := policy.Defaults(owner)198 p.Views = map[string]string{"profiles": "off", "relays": "off", "calendar": "hourly", "moderation": "off", "articles": "off", "podcasts": "off", "zaps": "off", "presence": "off"}199 generated := 0200 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error {201 generated++202 return nil203 }})204 if err != nil {205 t.Fatal(err)206 }207 if err := r.Tick(ctx, 100); err != nil {208 t.Fatal(err)209 }210 if err := r.Tick(ctx, 3700); err != nil {211 t.Fatal(err)212 }213 if generated != 1 {214 t.Fatalf("unchanged hourly view generated %d records", generated)215 }216 runs, err := r.ViewRuns(ctx, "calendar")217 if err != nil {218 t.Fatal(err)219 }220 if len(runs) != 1 {221 t.Fatalf("runs=%d, want only published run", len(runs))222 }223}224225func TestDefaultArticleWriteTriggerCoalescesDurably(t *testing.T) {226 s, ctx := testStore(t)227 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"228 p := policy.Defaults(owner)229 for _, name := range []string{"profiles", "relays", "calendar", "moderation", "podcasts", "zaps", "presence"} {230 p.Views[name] = "off"231 }232 generated := 0233 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, OnGenerated: func(context.Context, event.Event) error { generated++; return nil }})234 if err != nil {235 t.Fatal(err)236 }237 if err := r.MarkView(ctx, "articles", 100); err != nil {238 t.Fatal(err)239 }240 if err := r.MarkView(ctx, "articles", 101); err != nil {241 t.Fatal(err)242 }243 if got, err := r.NextDue(ctx, 105); err != nil || got != 110 {244 t.Fatalf("next due=%d err=%v", got, err)245 }246 if err := r.Tick(ctx, 105); err != nil {247 t.Fatal(err)248 }249 if generated != 0 {250 t.Fatal("published before coalescing window")251 }252 if err := r.Tick(ctx, 110); err != nil {253 t.Fatal(err)254 }255 if generated != 1 {256 t.Fatalf("generated=%d, want one coalesced article view", generated)257 }258 var dirty int64259 if err := s.GetSetting(ctx, "records.view.articles.dirty", &dirty); err != nil || dirty != 0 {260 t.Fatalf("dirty=%d err=%v", dirty, err)261 }262}263264func TestSuccessionWarningAndOwnerActivityAbort(t *testing.T) {265 s, ctx := testStore(t)266 owner := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"267 heir := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"268 p := policy.Defaults(owner)269 p.Notify.Succession = true270 p.Succession = &policy.Succession{Heir: heir, AfterDays: 90}271 transfers := 0272 r, err := New(ctx, Config{Community: emptyCommunityReader{}, Store: s, Policy: func() policy.Policy { return p }, SetPolicy: func(next policy.Policy) error { p = next; return nil }, RelayURL: "wss://relay.example", OnTransfer: func(context.Context, string, string) error { transfers++; return nil }})273 if err != nil {274 t.Fatal(err)275 }276 base := int64(1000)277 if err := r.Heartbeat(ctx, owner, base); err != nil {278 t.Fatal(err)279 }280 if err := r.Tick(ctx, base+90*86400+29*86400); err != nil {281 t.Fatal(err)282 }283 if transfers != 0 {284 t.Fatal("transferred during warning period")285 }286 if err := r.Heartbeat(ctx, owner, base+90*86400+29*86400+1); err != nil {287 t.Fatal(err)288 }289 if err := r.Tick(ctx, base+90*86400+31*86400); err != nil {290 t.Fatal(err)291 }292 if transfers != 0 {293 t.Fatal("owner heartbeat did not abort succession")294 }295 // Let the dead-man clock expire on a second relay instance: ownership must296 // be changed through the policy callback, not merely reported as a hook.297 p.Succession = &policy.Succession{Heir: heir, AfterDays: 90}298 if err := r.Heartbeat(ctx, owner, base); err != nil {299 t.Fatal(err)300 }301 if err := r.Tick(ctx, base+90*86400); err != nil {302 t.Fatal(err)303 }304 if err := r.Tick(ctx, base+90*86400+30*86400+1); err != nil {305 t.Fatal(err)306 }307 if p.Owner != heir || p.Succession != nil {308 t.Fatalf("succession did not transfer ownership: %+v", p)309 }310}