1package storage23import (4 "context"5 "database/sql"6 "errors"7 "path/filepath"8 "testing"910 "github.com/FelineStateMachine/tinyrelay/internal/event"11)1213func openTestStore(t *testing.T) *Store {14 t.Helper()15 s, err := Open(context.Background(), filepath.Join(t.TempDir(), "relay.db"))16 if err != nil {17 t.Fatal(err)18 }19 t.Cleanup(func() {20 if err := s.Close(); err != nil {21 t.Error(err)22 }23 })24 return s25}2627func sampleEvent(t *testing.T, kind int, at int64, tags [][]string) event.Event {28 t.Helper()29 e := event.Event{Kind: kind, CreatedAt: at, Tags: tags, Content: "hello storage"}30 if e.Tags == nil {31 e.Tags = [][]string{}32 }33 if err := event.Sign(&e, "0000000000000000000000000000000000000000000000000000000000000001"); err != nil {34 t.Fatal(err)35 }36 return e37}3839func TestDurableAcceptanceIncludesIntents(t *testing.T) {40 s := openTestStore(t)41 ctx := context.Background()42 e := sampleEvent(t, 1, 100, nil)43 opts := SaveOptions{Now: 101, Intents: []Intent{{Kind: "delivery", EventID: e.ID, Target: "wss://example.com", Payload: `{}`}}}44 if _, err := s.Save(ctx, e, opts); err != nil {45 t.Fatal(err)46 }47 if _, err := s.Save(ctx, e, opts); !errors.Is(err, ErrDuplicate) {48 t.Fatalf("duplicate: %v", err)49 }50 var n int51 if err := s.DB().QueryRowContext(ctx, "SELECT count(*) FROM work_intents").Scan(&n); err != nil {52 t.Fatal(err)53 }54 if n != 1 {55 t.Fatalf("got %d intents", n)56 }57 got, err := s.Query(ctx, event.Filter{}, QueryOptions{Now: 101})58 if err != nil || len(got.Events) != 1 {59 t.Fatalf("query: %+v %v", got, err)60 }61}6263func TestDeletionArchivesAgentGrant(t *testing.T) {64 s := openTestStore(t)65 ctx := context.Background()66 agent := "0000000000000000000000000000000000000000000000000000000000000002"67 grant := sampleEvent(t, event.KIND_AGENT_GRANT, 100, [][]string{{"d", agent}, {"p", agent}, {"expiration", "200"}})68 if _, err := s.Save(ctx, grant, SaveOptions{Now: 100}); err != nil {69 t.Fatal(err)70 }71 del := sampleEvent(t, event.KIND_DELETION, 110, [][]string{{"e", grant.ID}})72 del.PubKey = grant.PubKey73 if err := event.Sign(&del, "0000000000000000000000000000000000000000000000000000000000000001"); err != nil {74 t.Fatal(err)75 }76 if _, err := s.Save(ctx, del, SaveOptions{Now: 110}); err != nil {77 t.Fatal(err)78 }79 var raw string80 if err := s.DB().QueryRowContext(ctx, "SELECT raw FROM agent_grant_revisions WHERE event_id=?", grant.ID).Scan(&raw); err != nil {81 t.Fatal(err)82 }83 if raw == "" {84 t.Fatal("deleted grant revision is empty")85 }86}8788func TestEventAndIntentRollbackTogether(t *testing.T) {89 s := openTestStore(t)90 e := sampleEvent(t, 1, 100, nil)91 opts := SaveOptions{Now: 101, BeforeCommit: func(context.Context, *sql.Tx) error { return errors.New("simulated disk failure") }}92 if _, err := s.Save(context.Background(), e, opts); err == nil {93 t.Fatal("expected failure")94 }95 got, err := s.Query(context.Background(), event.Filter{}, QueryOptions{Now: 101})96 if err != nil || len(got.Events) != 0 {97 t.Fatalf("uncommitted event visible: %+v %v", got, err)98 }99}100101func TestReplaceDeleteExpiryAndPrivacy(t *testing.T) {102 ctx := context.Background()103 t.Run("replacement and history", func(t *testing.T) {104 s := openTestStore(t)105 old := sampleEvent(t, 3, 100, [][]string{{"p", "old"}})106 fresh := sampleEvent(t, 3, 102, [][]string{{"p", "new"}})107 for _, e := range []event.Event{old, fresh} {108 if _, err := s.Save(ctx, e, SaveOptions{Now: 103}); err != nil {109 t.Fatal(err)110 }111 }112 if _, err := s.Save(ctx, old, SaveOptions{Now: 103}); !errors.Is(err, ErrReplaced) {113 t.Fatalf("got %v", err)114 }115 got, err := s.Query(ctx, event.Filter{}, QueryOptions{Now: 103})116 if err != nil || len(got.Events) != 1 || got.Events[0].ID != fresh.ID {117 t.Fatalf("replace: %+v %v", got, err)118 }119 rows, err := s.ListHistory(ctx, old.PubKey, 103)120 if err != nil || len(rows) != 1 {121 t.Fatalf("history: %+v %v", rows, err)122 }123 })124 t.Run("deletion arrives first", func(t *testing.T) {125 s := openTestStore(t)126 e := sampleEvent(t, 1, 100, nil)127 d := sampleEvent(t, 5, 102, [][]string{{"e", e.ID}})128 if _, err := s.Save(ctx, d, SaveOptions{Now: 103}); err != nil {129 t.Fatal(err)130 }131 if _, err := s.Save(ctx, e, SaveOptions{Now: 103}); !errors.Is(err, ErrDeleted) {132 t.Fatalf("got %v", err)133 }134 })135 t.Run("private count and query agree", func(t *testing.T) {136 s := openTestStore(t)137 e := sampleEvent(t, 1059, 100, [][]string{{"p", "recipient"}})138 if _, err := s.Save(ctx, e, SaveOptions{Now: 101}); err != nil {139 t.Fatal(err)140 }141 for _, tc := range []struct {142 who Access143 want int144 }{{Access{}, 0}, {Access{PubKeys: []string{"recipient"}}, 1}, {Access{PubKeys: []string{"stranger"}}, 0}} {145 opts := QueryOptions{Now: 101, Access: tc.who}146 got, err := s.Query(ctx, event.Filter{}, opts)147 if err != nil || len(got.Events) != tc.want {148 t.Fatalf("privacy: %+v %v", got, err)149 }150 n, err := s.Count(ctx, []event.Filter{{}}, opts)151 if err != nil || n != int64(tc.want) {152 t.Fatalf("count: %d %v", n, err)153 }154 }155 })156 t.Run("nip59 seals are visible only to their author", func(t *testing.T) {157 s := openTestStore(t)158 e := sampleEvent(t, 13, 100, [][]string{})159 if _, err := s.Save(ctx, e, SaveOptions{Now: 101}); err != nil {160 t.Fatal(err)161 }162 for _, tc := range []struct {163 who Access164 want int165 }{{Access{}, 0}, {Access{PubKeys: []string{e.PubKey}}, 1}, {Access{PubKeys: []string{"stranger"}}, 0}} {166 got, err := s.Query(ctx, event.Filter{Kinds: []int{13}}, QueryOptions{Now: 101, Access: tc.who})167 if err != nil || len(got.Events) != tc.want {168 t.Fatalf("seal query: %+v %v", got, err)169 }170 n, err := s.Count(ctx, []event.Filter{{Kinds: []int{13}}}, QueryOptions{Now: 101, Access: tc.who})171 if err != nil || n != int64(tc.want) {172 t.Fatalf("seal count: %d %v", n, err)173 }174 }175 })176 t.Run("expired and ephemeral", func(t *testing.T) {177 s := openTestStore(t)178 for _, e := range []event.Event{sampleEvent(t, 1, 100, [][]string{{"expiration", "102"}}), sampleEvent(t, 20001, 100, nil)} {179 if _, err := s.Save(ctx, e, SaveOptions{Now: 101}); err != nil {180 t.Fatal(err)181 }182 }183 got, err := s.Query(ctx, event.Filter{}, QueryOptions{Now: 103})184 if err != nil || len(got.Events) != 0 {185 t.Fatalf("expiration: %+v %v", got, err)186 }187 })188}189190func TestEmptyFilterArraysMatchNothing(t *testing.T) {191 s := openTestStore(t)192 if _, err := s.Save(context.Background(), sampleEvent(t, 1, 100, nil), SaveOptions{Now: 101}); err != nil {193 t.Fatal(err)194 }195 for _, f := range []event.Filter{{IDs: []string{}}, {Authors: []string{}}, {Kinds: []int{}}, {Tags: map[string][]string{"p": {}}}} {196 got, err := s.Query(context.Background(), f, QueryOptions{Now: 101})197 if err != nil || len(got.Events) != 0 {198 t.Fatalf("filter: %+v %v", got, err)199 }200 }201}