From a7dbf7c4910a5daa4ad54423f3386e8edc7eec7b Mon Sep 17 00:00:00 2001 From: bndw Date: Wed, 3 May 2023 17:18:47 -0700 Subject: [PATCH] refactor(postgres): Unit test SaveEvent Refactors SaveEvent so it's unit testable and adds tests to assert the current behavior. --- storage/postgresql/save.go | 71 +++++++++---- storage/postgresql/save_test.go | 177 ++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 22 deletions(-) create mode 100644 storage/postgresql/save_test.go diff --git a/storage/postgresql/save.go b/storage/postgresql/save.go index 6f57f60..5bf3bcd 100644 --- a/storage/postgresql/save.go +++ b/storage/postgresql/save.go @@ -9,30 +9,13 @@ import ( ) func (b *PostgresBackend) SaveEvent(ctx context.Context, evt *nostr.Event) error { - // react to different kinds of events - if evt.Kind == nostr.KindSetMetadata || evt.Kind == nostr.KindContactList || (10000 <= evt.Kind && evt.Kind < 20000) { - // delete past events from this user - b.DB.ExecContext(ctx, `DELETE FROM event WHERE pubkey = $1 AND kind = $2`, evt.PubKey, evt.Kind) - } else if evt.Kind == nostr.KindRecommendServer { - // delete past recommend_server events equal to this one - b.DB.ExecContext(ctx, `DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND content = $3`, - evt.PubKey, evt.Kind, evt.Content) - } else if evt.Kind >= 30000 && evt.Kind < 40000 { - // NIP-33 - d := evt.Tags.GetFirst([]string{"d"}) - if d != nil { - b.DB.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND tagvalues && ARRAY[$3]`, - evt.PubKey, evt.Kind, d.Value()) - } + deleteQuery, deleteParams, shouldDelete := deleteBeforeSaveSql(evt) + if shouldDelete { + _, _ = b.DB.ExecContext(ctx, deleteQuery, deleteParams...) } - // insert - tagsj, _ := json.Marshal(evt.Tags) - res, err := b.DB.ExecContext(ctx, ` - INSERT INTO event (id, pubkey, created_at, kind, tags, content, sig) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO NOTHING - `, evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsj, evt.Content, evt.Sig) + sql, params, _ := saveEventSql(evt) + res, err := b.DB.ExecContext(ctx, sql, params...) if err != nil { return err } @@ -60,3 +43,47 @@ func (b *PostgresBackend) AfterSave(evt *nostr.Event) { ORDER BY created_at DESC OFFSET 100 LIMIT 1 )`, evt.PubKey, evt.Kind) } + +func deleteBeforeSaveSql(evt *nostr.Event) (string, []any, bool) { + // react to different kinds of events + var ( + query = "" + params []any + shouldDelete bool + ) + if evt.Kind == nostr.KindSetMetadata || evt.Kind == nostr.KindContactList || (10000 <= evt.Kind && evt.Kind < 20000) { + // delete past events from this user + query = `DELETE FROM event WHERE pubkey = $1 AND kind = $2` + params = []any{evt.PubKey, evt.Kind} + shouldDelete = true + } else if evt.Kind == nostr.KindRecommendServer { + // delete past recommend_server events equal to this one + query = `DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND content = $3` + params = []any{evt.PubKey, evt.Kind, evt.Content} + shouldDelete = true + } else if evt.Kind >= 30000 && evt.Kind < 40000 { + // NIP-33 + d := evt.Tags.GetFirst([]string{"d"}) + if d != nil { + query = `DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND tagvalues && ARRAY[$3]` + params = []any{evt.PubKey, evt.Kind, d.Value()} + shouldDelete = true + } + } + + return query, params, shouldDelete +} + +func saveEventSql(evt *nostr.Event) (string, []any, error) { + const query = `INSERT INTO event ( + id, pubkey, created_at, kind, tags, content, sig) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO NOTHING` + + var ( + tagsj, _ = json.Marshal(evt.Tags) + params = []any{evt.ID, evt.PubKey, evt.CreatedAt, evt.Kind, tagsj, evt.Content, evt.Sig} + ) + + return query, params, nil +} diff --git a/storage/postgresql/save_test.go b/storage/postgresql/save_test.go new file mode 100644 index 0000000..74e7ae0 --- /dev/null +++ b/storage/postgresql/save_test.go @@ -0,0 +1,177 @@ +package postgresql + +import ( + "testing" + + "github.com/nbd-wtf/go-nostr" + "github.com/stretchr/testify/assert" +) + +func TestDeleteBeforeSave(t *testing.T) { + var tests = []struct { + name string + event *nostr.Event + query string + params []any + shouldDelete bool + }{ + { + name: "set metadata", + event: &nostr.Event{ + Kind: nostr.KindSetMetadata, + PubKey: "pk", + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2", + params: []any{"pk", nostr.KindSetMetadata}, + shouldDelete: true, + }, + { + name: "contact list", + event: &nostr.Event{ + Kind: nostr.KindContactList, + PubKey: "pk", + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2", + params: []any{"pk", nostr.KindContactList}, + shouldDelete: true, + }, + { + name: "recommend server", + event: &nostr.Event{ + Kind: nostr.KindRecommendServer, + PubKey: "pk", + Content: "test", + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND content = $3", + params: []any{"pk", nostr.KindRecommendServer, "test"}, + shouldDelete: true, + }, + { + name: "nip-33", + event: &nostr.Event{ + Kind: 31000, + PubKey: "pk", + Tags: nostr.Tags{nostr.Tag{"d", "value"}}, + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2 AND tagvalues && ARRAY[$3]", + params: []any{"pk", 31000, "value"}, + shouldDelete: true, + }, + { + name: "kind > 10000", + event: &nostr.Event{ + Kind: 10001, + PubKey: "pk", + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2", + params: []any{"pk", 10001}, + shouldDelete: true, + }, + { + name: "kind < 20000", + event: &nostr.Event{ + Kind: 19999, + PubKey: "pk", + }, + query: "DELETE FROM event WHERE pubkey = $1 AND kind = $2", + params: []any{"pk", 19999}, + shouldDelete: true, + }, + // Should not delete cases + { + name: "kind < 10000", + event: &nostr.Event{ + Kind: 9999, + PubKey: "pk", + }, + query: "", + params: nil, + shouldDelete: false, + }, + { + name: "kind > 21000", + event: &nostr.Event{ + Kind: 21000, + PubKey: "pk", + }, + query: "", + params: nil, + shouldDelete: false, + }, + { + name: "kind 1", + event: &nostr.Event{ + Kind: nostr.KindTextNote, + PubKey: "pk", + }, + query: "", + params: nil, + shouldDelete: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + query, params, shouldDelete := deleteBeforeSaveSql(tt.event) + assert.Equal(t, tt.query, query) + assert.Equal(t, tt.params, params) + assert.Equal(t, tt.shouldDelete, shouldDelete) + }) + } +} + +func TestSaveEventSql(t *testing.T) { + now := nostr.Now() + var tests = []struct { + name string + event *nostr.Event + query string + params []any + err error + }{ + { + name: "basic", + event: &nostr.Event{ + ID: "id", + PubKey: "pk", + CreatedAt: now, + Kind: nostr.KindTextNote, + Content: "test", + Sig: "sig", + }, + query: `INSERT INTO event ( + id, pubkey, created_at, kind, tags, content, sig) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO NOTHING`, + params: []any{"id", "pk", now, nostr.KindTextNote, []byte("null"), "test", "sig"}, + err: nil, + }, + { + name: "tags", + event: &nostr.Event{ + ID: "id", + PubKey: "pk", + CreatedAt: now, + Kind: nostr.KindTextNote, + Tags: nostr.Tags{nostr.Tag{"foo", "bar"}}, + Content: "test", + Sig: "sig", + }, + query: `INSERT INTO event ( + id, pubkey, created_at, kind, tags, content, sig) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO NOTHING`, + params: []any{"id", "pk", now, nostr.KindTextNote, []byte("[[\"foo\",\"bar\"]]"), "test", "sig"}, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + query, params, err := saveEventSql(tt.event) + assert.Equal(t, clean(tt.query), clean(query)) + assert.Equal(t, tt.params, params) + assert.Equal(t, tt.err, err) + }) + } +}