From 656748ea286ff7eac6cbe1b241ad31212892ba61 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 13 Feb 2026 19:12:28 -0800 Subject: feat: implement NIP-09 (deletions) and NIP-11 (relay info) NIP-11 (Relay Information Document): - Serves relay metadata at GET / with Accept: application/nostr+json - Returns name, description, supported NIPs, limitations - CORS headers for browser compatibility NIP-09 (Event Deletion): - Kind 5 events delete events referenced in 'e' tags - Only authors can delete their own events - Soft delete (marks deleted=1) - Records deletion in deletions table - Works across all protocols (gRPC, Connect, WebSocket) Fixed deletions schema: - deleted_event_id as PRIMARY KEY (not deletion_event_id) - Allows one deletion event to delete multiple events 3 new tests, 44 total tests passing Supported NIPs now: 1, 9, 11 --- internal/storage/deletions_test.go | 184 +++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 internal/storage/deletions_test.go (limited to 'internal/storage/deletions_test.go') diff --git a/internal/storage/deletions_test.go b/internal/storage/deletions_test.go new file mode 100644 index 0000000..47e6547 --- /dev/null +++ b/internal/storage/deletions_test.go @@ -0,0 +1,184 @@ +package storage + +import ( + "context" + "fmt" + "testing" + "time" + + pb "northwest.io/nostr-grpc/api/nostr/v1" +) + +func TestProcessDeletion(t *testing.T) { + store, err := New(":memory:") + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + event := &pb.Event{ + Id: "event123", + Pubkey: "alice", + CreatedAt: time.Now().Unix(), + Kind: 1, + Tags: []*pb.Tag{}, + Content: "to be deleted", + Sig: "sig1", + } + + err = store.StoreEvent(ctx, &EventData{ + Event: event, + CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"to be deleted"]`), + }) + if err != nil { + t.Fatalf("failed to store event: %v", err) + } + + retrieved, err := store.GetEvent(ctx, "event123") + if err != nil { + t.Fatalf("event should exist before deletion: %v", err) + } + if retrieved.Id != "event123" { + t.Errorf("expected event123, got %s", retrieved.Id) + } + + deletionEvent := &pb.Event{ + Id: "deletion123", + Pubkey: "alice", + CreatedAt: time.Now().Unix(), + Kind: KindDeletion, + Tags: []*pb.Tag{ + {Values: []string{"e", "event123"}}, + }, + Content: "deleting my event", + Sig: "sig2", + } + + err = store.ProcessDeletion(ctx, deletionEvent) + if err != nil { + t.Fatalf("failed to process deletion: %v", err) + } + + _, err = store.GetEvent(ctx, "event123") + if err != ErrEventNotFound { + t.Errorf("event should be deleted, got error: %v", err) + } +} + +func TestProcessDeletionWrongAuthor(t *testing.T) { + store, err := New(":memory:") + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + event := &pb.Event{ + Id: "event456", + Pubkey: "alice", + CreatedAt: time.Now().Unix(), + Kind: 1, + Tags: []*pb.Tag{}, + Content: "alice's event", + Sig: "sig1", + } + + err = store.StoreEvent(ctx, &EventData{ + Event: event, + CanonicalJSON: []byte(`[0,"alice",1234567890,1,[],"alice's event"]`), + }) + if err != nil { + t.Fatalf("failed to store event: %v", err) + } + + deletionEvent := &pb.Event{ + Id: "deletion456", + Pubkey: "bob", + CreatedAt: time.Now().Unix(), + Kind: KindDeletion, + Tags: []*pb.Tag{ + {Values: []string{"e", "event456"}}, + }, + Content: "trying to delete alice's event", + Sig: "sig2", + } + + err = store.ProcessDeletion(ctx, deletionEvent) + if err != nil { + t.Fatalf("process deletion should succeed but not delete: %v", err) + } + + retrieved, err := store.GetEvent(ctx, "event456") + if err != nil { + t.Fatalf("event should still exist: %v", err) + } + if retrieved.Id != "event456" { + t.Errorf("expected event456, got %s", retrieved.Id) + } +} + +func TestProcessDeletionMultipleEvents(t *testing.T) { + store, err := New(":memory:") + if err != nil { + t.Fatalf("failed to create storage: %v", err) + } + defer store.Close() + + ctx := context.Background() + + for i := 1; i <= 3; i++ { + event := &pb.Event{ + Id: fmt.Sprintf("event%d", i), + Pubkey: "alice", + CreatedAt: time.Now().Unix(), + Kind: 1, + Tags: []*pb.Tag{}, + Content: fmt.Sprintf("event %d", i), + Sig: fmt.Sprintf("sig%d", i), + } + + err = store.StoreEvent(ctx, &EventData{ + Event: event, + CanonicalJSON: []byte(fmt.Sprintf(`[0,"alice",1234567890,1,[],"event %d"]`, i)), + }) + if err != nil { + t.Fatalf("failed to store event: %v", err) + } + } + + deletionEvent := &pb.Event{ + Id: "deletion789", + Pubkey: "alice", + CreatedAt: time.Now().Unix(), + Kind: KindDeletion, + Tags: []*pb.Tag{ + {Values: []string{"e", "event1"}}, + {Values: []string{"e", "event2"}}, + }, + Content: "deleting multiple events", + Sig: "sig_del", + } + + err = store.ProcessDeletion(ctx, deletionEvent) + if err != nil { + t.Fatalf("failed to process deletion: %v", err) + } + + _, err = store.GetEvent(ctx, "event1") + if err != ErrEventNotFound { + t.Error("event1 should be deleted") + } + + _, err = store.GetEvent(ctx, "event2") + if err != ErrEventNotFound { + t.Error("event2 should be deleted") + } + + _, err = store.GetEvent(ctx, "event3") + if err != nil { + t.Error("event3 should still exist") + } +} -- cgit v1.2.3