Decompose handle-event.go into DDD domain services (v0.36.15)
Some checks failed
Go / build-and-release (push) Has been cancelled

Major refactoring of event handling into clean, testable domain services:

- Add pkg/event/validation: JSON hex validation, signature verification,
  timestamp bounds, NIP-70 protected tag validation
- Add pkg/event/authorization: Policy and ACL authorization decisions,
  auth challenge handling, access level determination
- Add pkg/event/routing: Event router registry with ephemeral and delete
  handlers, kind-based dispatch
- Add pkg/event/processing: Event persistence, delivery to subscribers,
  and post-save hooks (ACL reconfig, sync, relay groups)
- Reduce handle-event.go from 783 to 296 lines (62% reduction)
- Add comprehensive unit tests for all new domain services
- Refactor database tests to use shared TestMain setup
- Fix blossom URL test expectations (missing "/" separator)
- Add go-memory-optimization skill and analysis documentation
- Update DDD_ANALYSIS.md to reflect completed decomposition

Files modified:
- app/handle-event.go: Slim orchestrator using domain services
- app/server.go: Service initialization and interface wrappers
- app/handle-event-types.go: Shared types (OkHelper, result types)
- pkg/event/validation/*: New validation service package
- pkg/event/authorization/*: New authorization service package
- pkg/event/routing/*: New routing service package
- pkg/event/processing/*: New processing service package
- pkg/database/*_test.go: Refactored to shared TestMain
- pkg/blossom/http_test.go: Fixed URL format expectations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-25 05:30:07 +01:00
parent 3e0a94a053
commit 24383ef1f4
42 changed files with 4791 additions and 2118 deletions

View File

@@ -1,16 +1,12 @@
package database
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"sort"
"testing"
"git.mleku.dev/mleku/nostr/encoders/event"
"git.mleku.dev/mleku/nostr/encoders/event/examples"
"git.mleku.dev/mleku/nostr/encoders/filter"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/encoders/kind"
@@ -21,87 +17,44 @@ import (
"next.orly.dev/pkg/utils"
)
// setupTestDB creates a new test database and loads example events
func setupTestDB(t *testing.T) (
*D, []*event.E, context.Context, context.CancelFunc, string,
) {
// Create a temporary directory for the database
// setupFreshTestDB creates a new isolated test database for tests that modify data.
// Use this for tests that need to write/delete events.
func setupFreshTestDB(t *testing.T) (*D, context.Context, func()) {
if testing.Short() {
t.Skip("skipping test that requires fresh database in short mode")
}
tempDir, err := os.MkdirTemp("", "test-db-*")
if err != nil {
t.Fatalf("Failed to create temporary directory: %v", err)
}
// Create a context and cancel function for the database
ctx, cancel := context.WithCancel(context.Background())
// Initialize the database
db, err := New(ctx, cancel, tempDir, "info")
if err != nil {
os.RemoveAll(tempDir)
t.Fatalf("Failed to create database: %v", err)
}
// Create a scanner to read events from examples.Cache
scanner := bufio.NewScanner(bytes.NewBuffer(examples.Cache))
scanner.Buffer(make([]byte, 0, 1_000_000_000), 1_000_000_000)
var events []*event.E
// First, collect all events from examples.Cache
for scanner.Scan() {
chk.E(scanner.Err())
b := scanner.Bytes()
ev := event.New()
// Unmarshal the event
if _, err = ev.Unmarshal(b); chk.E(err) {
ev.Free()
t.Fatal(err)
}
events = append(events, ev)
cleanup := func() {
db.Close()
cancel()
os.RemoveAll(tempDir)
}
// Check for scanner errors
if err = scanner.Err(); err != nil {
t.Fatalf("Scanner error: %v", err)
}
// Sort events by CreatedAt to ensure addressable events are processed in chronological order
sort.Slice(events, func(i, j int) bool {
return events[i].CreatedAt < events[j].CreatedAt
})
// Count the number of events processed
eventCount := 0
skippedCount := 0
var savedEvents []*event.E
// Now process each event in chronological order
for _, ev := range events {
// Save the event to the database
if _, err = db.SaveEvent(ctx, ev); err != nil {
// Skip events that fail validation (e.g., kind 3 without p tags)
skippedCount++
continue
}
savedEvents = append(savedEvents, ev)
eventCount++
}
t.Logf("Successfully saved %d events to the database (skipped %d invalid events)", eventCount, skippedCount)
return db, savedEvents, ctx, cancel, tempDir
return db, ctx, cleanup
}
func TestQueryEventsByID(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Use shared database (read-only test)
db, ctx := GetSharedDB(t)
events := GetSharedEvents(t)
// Test QueryEvents with an ID filter
testEvent := events[3] // Using the same event as in other tests
if len(events) < 4 {
t.Fatalf("Need at least 4 saved events, got %d", len(events))
}
testEvent := events[3]
evs, err := db.QueryEvents(
ctx, &filter.F{
@@ -112,12 +65,10 @@ func TestQueryEventsByID(t *testing.T) {
t.Fatalf("Failed to query events by ID: %v", err)
}
// Verify we got exactly one event
if len(evs) != 1 {
t.Fatalf("Expected 1 event, got %d", len(evs))
}
// Verify it's the correct event
if !utils.FastEqual(evs[0].ID, testEvent.ID) {
t.Fatalf(
"Event ID doesn't match. Got %x, expected %x", evs[0].ID,
@@ -127,12 +78,9 @@ func TestQueryEventsByID(t *testing.T) {
}
func TestQueryEventsByKind(t *testing.T) {
db, _, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Use shared database (read-only test)
db, ctx := GetSharedDB(t)
// Test querying by kind
testKind := kind.New(1) // Kind 1 is typically text notes
kindFilter := kind.NewS(testKind)
@@ -146,12 +94,10 @@ func TestQueryEventsByKind(t *testing.T) {
t.Fatalf("Failed to query events by kind: %v", err)
}
// Verify we got results
if len(evs) == 0 {
t.Fatal("Expected events with kind 1, but got none")
}
// Verify all events have the correct kind
for i, ev := range evs {
if ev.Kind != testKind.K {
t.Fatalf(
@@ -163,12 +109,14 @@ func TestQueryEventsByKind(t *testing.T) {
}
func TestQueryEventsByAuthor(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Use shared database (read-only test)
db, ctx := GetSharedDB(t)
events := GetSharedEvents(t)
if len(events) < 2 {
t.Fatalf("Need at least 2 saved events, got %d", len(events))
}
// Test querying by author
authorFilter := tag.NewFromBytesSlice(events[1].Pubkey)
evs, err := db.QueryEvents(
@@ -180,12 +128,10 @@ func TestQueryEventsByAuthor(t *testing.T) {
t.Fatalf("Failed to query events by author: %v", err)
}
// Verify we got results
if len(evs) == 0 {
t.Fatal("Expected events from author, but got none")
}
// Verify all events have the correct author
for i, ev := range evs {
if !utils.FastEqual(ev.Pubkey, events[1].Pubkey) {
t.Fatalf(
@@ -197,12 +143,16 @@ func TestQueryEventsByAuthor(t *testing.T) {
}
func TestReplaceableEventsAndDeletion(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Needs fresh database (modifies data)
db, ctx, cleanup := setupFreshTestDB(t)
defer cleanup()
// Seed with a few events for pubkey reference
events := GetSharedEvents(t)
if len(events) == 0 {
t.Fatal("Need at least 1 event for pubkey reference")
}
// Test querying for replaced events by ID
sign := p8k.MustNew()
if err := sign.Generate(); chk.E(err) {
t.Fatal(err)
@@ -210,26 +160,26 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
// Create a replaceable event
replaceableEvent := event.New()
replaceableEvent.Kind = kind.ProfileMetadata.K // Kind 0 is replaceable
replaceableEvent.Pubkey = events[0].Pubkey // Use the same pubkey as an existing event
replaceableEvent.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
replaceableEvent.Kind = kind.ProfileMetadata.K
replaceableEvent.Pubkey = events[0].Pubkey
replaceableEvent.CreatedAt = timestamp.Now().V - 7200
replaceableEvent.Content = []byte("Original profile")
replaceableEvent.Tags = tag.NewS()
replaceableEvent.Sign(sign)
// Save the replaceable event
if _, err := db.SaveEvent(ctx, replaceableEvent); err != nil {
t.Errorf("Failed to save replaceable event: %v", err)
}
// Create a newer version of the replaceable event
// Create a newer version
newerEvent := event.New()
newerEvent.Kind = kind.ProfileMetadata.K // Same kind
newerEvent.Pubkey = replaceableEvent.Pubkey // Same pubkey
newerEvent.CreatedAt = timestamp.Now().V - 3600 // 1 hour ago (newer than the original)
newerEvent.Kind = kind.ProfileMetadata.K
newerEvent.Pubkey = replaceableEvent.Pubkey
newerEvent.CreatedAt = timestamp.Now().V - 3600
newerEvent.Content = []byte("Updated profile")
newerEvent.Tags = tag.NewS()
newerEvent.Sign(sign)
// Save the newer event
if _, err := db.SaveEvent(ctx, newerEvent); err != nil {
t.Errorf("Failed to save newer event: %v", err)
}
@@ -244,12 +194,10 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
t.Errorf("Failed to query for replaced event by ID: %v", err)
}
// Verify the original event is still found (it's kept but not returned in general queries)
if len(evs) != 1 {
t.Errorf("Expected 1 event when querying for replaced event by ID, got %d", len(evs))
}
// Verify it's the original event
if !utils.FastEqual(evs[0].ID, replaceableEvent.ID) {
t.Errorf(
"Event ID doesn't match when querying for replaced event. Got %x, expected %x",
@@ -271,7 +219,6 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
t.Errorf("Failed to query for replaceable events: %v", err)
}
// Verify we got only one event (the latest one)
if len(evs) != 1 {
t.Errorf(
"Expected 1 event when querying for replaceable events, got %d",
@@ -279,7 +226,6 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
)
}
// Verify it's the newer event
if !utils.FastEqual(evs[0].ID, newerEvent.ID) {
t.Fatalf(
"Event ID doesn't match when querying for replaceable events. Got %x, expected %x",
@@ -288,36 +234,23 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
}
// Test deletion events
// Create a deletion event that references the replaceable event
deletionEvent := event.New()
deletionEvent.Kind = kind.Deletion.K // Kind 5 is deletion
deletionEvent.Pubkey = replaceableEvent.Pubkey // Same pubkey as the event being deleted
deletionEvent.CreatedAt = timestamp.Now().V // Current time
deletionEvent.Kind = kind.Deletion.K
deletionEvent.Pubkey = replaceableEvent.Pubkey
deletionEvent.CreatedAt = timestamp.Now().V
deletionEvent.Content = []byte("Deleting the replaceable event")
deletionEvent.Tags = tag.NewS()
deletionEvent.Sign(sign)
// Add an e-tag referencing the replaceable event
t.Logf("Replaceable event ID: %x", replaceableEvent.ID)
*deletionEvent.Tags = append(
*deletionEvent.Tags,
tag.NewFromAny("e", hex.Enc(replaceableEvent.ID)),
)
// Save the deletion event
if _, err = db.SaveEvent(ctx, deletionEvent); err != nil {
t.Fatalf("Failed to save deletion event: %v", err)
}
// Debug: Check if the deletion event was saved
t.Logf("Deletion event ID: %x", deletionEvent.ID)
t.Logf("Deletion event pubkey: %x", deletionEvent.Pubkey)
t.Logf("Deletion event kind: %d", deletionEvent.Kind)
t.Logf("Deletion event tags count: %d", deletionEvent.Tags.Len())
for i, tag := range *deletionEvent.Tags {
t.Logf("Deletion event tag[%d]: %v", i, tag.T)
}
// Query for all events of this kind and pubkey again
evs, err = db.QueryEvents(
ctx, &filter.F{
@@ -331,7 +264,6 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
)
}
// Verify we still get the newer event (deletion should only affect the original event)
if len(evs) != 1 {
t.Fatalf(
"Expected 1 event when querying for replaceable events after deletion, got %d",
@@ -339,7 +271,6 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
)
}
// Verify it's still the newer event
if !utils.FastEqual(evs[0].ID, newerEvent.ID) {
t.Fatalf(
"Event ID doesn't match after deletion. Got %x, expected %x",
@@ -357,33 +288,20 @@ func TestReplaceableEventsAndDeletion(t *testing.T) {
t.Errorf("Failed to query for deleted event by ID: %v", err)
}
// Verify the original event is not found (it was deleted)
if len(evs) != 0 {
t.Errorf("Expected 0 events when querying for deleted event by ID, got %d", len(evs))
}
// // Verify we still get the original event when querying by ID
// if len(evs) != 1 {
// t.Errorf(
// "Expected 1 event when querying for deleted event by ID, got %d",
// len(evs),
// )
// }
// // Verify it's the original event
// if !utils.FastEqual(evs[0].ID, replaceableEvent.ID) {
// t.Errorf(
// "Event ID doesn't match when querying for deleted event by ID. Got %x, expected %x",
// evs[0].ID, replaceableEvent.ID,
// )
// }
}
func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Needs fresh database (modifies data)
db, ctx, cleanup := setupFreshTestDB(t)
defer cleanup()
events := GetSharedEvents(t)
if len(events) == 0 {
t.Fatal("Need at least 1 event for pubkey reference")
}
sign := p8k.MustNew()
if err := sign.Generate(); chk.E(err) {
@@ -392,31 +310,27 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
// Create a parameterized replaceable event
paramEvent := event.New()
paramEvent.Kind = 30000 // Kind 30000+ is parameterized replaceable
paramEvent.Pubkey = events[0].Pubkey // Use the same pubkey as an existing event
paramEvent.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
paramEvent.Kind = 30000
paramEvent.Pubkey = events[0].Pubkey
paramEvent.CreatedAt = timestamp.Now().V - 7200
paramEvent.Content = []byte("Original parameterized event")
paramEvent.Tags = tag.NewS()
// Add a d-tag
*paramEvent.Tags = append(
*paramEvent.Tags, tag.NewFromAny([]byte{'d'}, []byte("test-d-tag")),
)
paramEvent.Sign(sign)
// Save the parameterized replaceable event
if _, err := db.SaveEvent(ctx, paramEvent); err != nil {
t.Fatalf("Failed to save parameterized replaceable event: %v", err)
}
// Create a deletion event that references the parameterized replaceable event using an a-tag
// Create a deletion event
paramDeletionEvent := event.New()
paramDeletionEvent.Kind = kind.Deletion.K // Kind 5 is deletion
paramDeletionEvent.Pubkey = paramEvent.Pubkey // Same pubkey as the event being deleted
paramDeletionEvent.CreatedAt = timestamp.Now().V // Current time
paramDeletionEvent.Kind = kind.Deletion.K
paramDeletionEvent.Pubkey = paramEvent.Pubkey
paramDeletionEvent.CreatedAt = timestamp.Now().V
paramDeletionEvent.Content = []byte("Deleting the parameterized replaceable event")
paramDeletionEvent.Tags = tag.NewS()
// Add an a-tag referencing the parameterized replaceable event
// Format: kind:pubkey:d-tag
aTagValue := fmt.Sprintf(
"%d:%s:%s",
paramEvent.Kind,
@@ -429,47 +343,30 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
)
paramDeletionEvent.Sign(sign)
// Save the parameterized deletion event
if _, err := db.SaveEvent(ctx, paramDeletionEvent); err != nil {
t.Fatalf("Failed to save parameterized deletion event: %v", err)
}
// Query for all events of this kind and pubkey
paramKindFilter := kind.NewS(kind.New(paramEvent.Kind))
paramAuthorFilter := tag.NewFromBytesSlice(paramEvent.Pubkey)
// Print debug info about the a-tag
fmt.Printf("Debug: a-tag value: %s\n", aTagValue)
fmt.Printf(
"Debug: kind: %d, pubkey: %s, d-tag: %s\n",
paramEvent.Kind,
hex.Enc(paramEvent.Pubkey),
"test-d-tag",
)
// Let's try a different approach - use an e-tag instead of an a-tag
// Create another deletion event that references the parameterized replaceable event using an e-tag
// Create deletion with e-tag too
paramDeletionEvent2 := event.New()
paramDeletionEvent2.Kind = kind.Deletion.K // Kind 5 is deletion
paramDeletionEvent2.Pubkey = paramEvent.Pubkey // Same pubkey as the event being deleted
paramDeletionEvent2.CreatedAt = timestamp.Now().V // Current time
paramDeletionEvent2.Content = []byte("Deleting the parameterized replaceable event with e-tag")
paramDeletionEvent2.Kind = kind.Deletion.K
paramDeletionEvent2.Pubkey = paramEvent.Pubkey
paramDeletionEvent2.CreatedAt = timestamp.Now().V
paramDeletionEvent2.Content = []byte("Deleting with e-tag")
paramDeletionEvent2.Tags = tag.NewS()
// Add an e-tag referencing the parameterized replaceable event
*paramDeletionEvent2.Tags = append(
*paramDeletionEvent2.Tags,
tag.NewFromAny("e", []byte(hex.Enc(paramEvent.ID))),
)
paramDeletionEvent2.Sign(sign)
// Save the parameterized deletion event with e-tag
if _, err := db.SaveEvent(ctx, paramDeletionEvent2); err != nil {
t.Fatalf(
"Failed to save parameterized deletion event with e-tag: %v", err,
)
t.Fatalf("Failed to save deletion event with e-tag: %v", err)
}
fmt.Printf("Debug: Added a second deletion event with e-tag referencing the event ID\n")
// Query for all events of this kind and pubkey
paramKindFilter := kind.NewS(kind.New(paramEvent.Kind))
paramAuthorFilter := tag.NewFromBytesSlice(paramEvent.Pubkey)
evs, err := db.QueryEvents(
ctx, &filter.F{
@@ -478,71 +375,45 @@ func TestParameterizedReplaceableEventsAndDeletion(t *testing.T) {
},
)
if err != nil {
t.Fatalf(
"Failed to query for parameterized replaceable events after deletion: %v",
err,
)
t.Fatalf("Failed to query for parameterized events: %v", err)
}
// Print debug info about the returned events
fmt.Printf("Debug: Got %d events\n", len(evs))
for i, ev := range evs {
fmt.Printf(
"Debug: Event %d: kind=%d, pubkey=%s\n",
i, ev.Kind, hex.Enc(ev.Pubkey),
)
dTag := ev.Tags.GetFirst([]byte("d"))
if dTag != nil && dTag.Len() > 1 {
fmt.Printf("Debug: Event %d: d-tag=%s\n", i, dTag.Value())
}
}
// Verify we get no events (since the only one was deleted)
if len(evs) != 0 {
t.Fatalf(
"Expected 0 events when querying for deleted parameterized replaceable events, got %d",
len(evs),
)
t.Fatalf("Expected 0 events after deletion, got %d", len(evs))
}
// Query for the parameterized event by ID
// Query by ID
evs, err = db.QueryEvents(
ctx, &filter.F{
Ids: tag.NewFromBytesSlice(paramEvent.ID),
},
)
if err != nil {
t.Fatalf(
"Failed to query for deleted parameterized event by ID: %v", err,
)
t.Fatalf("Failed to query for deleted event by ID: %v", err)
}
// Verify the deleted event is not found when querying by ID
if len(evs) != 0 {
t.Fatalf(
"Expected 0 events when querying for deleted parameterized event by ID, got %d",
len(evs),
)
t.Fatalf("Expected 0 events when querying deleted event by ID, got %d", len(evs))
}
}
func TestQueryEventsByTimeRange(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Use shared database (read-only test)
db, ctx := GetSharedDB(t)
events := GetSharedEvents(t)
if len(events) < 10 {
t.Fatalf("Need at least 10 saved events, got %d", len(events))
}
// Test querying by time range
// Use the timestamp from the middle event as a reference
middleIndex := len(events) / 2
middleEvent := events[middleIndex]
// Create a timestamp range that includes events before and after the middle event
sinceTime := new(timestamp.T)
sinceTime.V = middleEvent.CreatedAt - 3600 // 1 hour before middle event
sinceTime.V = middleEvent.CreatedAt - 3600
untilTime := new(timestamp.T)
untilTime.V = middleEvent.CreatedAt + 3600 // 1 hour after middle event
untilTime.V = middleEvent.CreatedAt + 3600
evs, err := db.QueryEvents(
ctx, &filter.F{
@@ -554,12 +425,10 @@ func TestQueryEventsByTimeRange(t *testing.T) {
t.Fatalf("Failed to query events by time range: %v", err)
}
// Verify we got results
if len(evs) == 0 {
t.Fatal("Expected events in time range, but got none")
}
// Verify all events are within the time range
for i, ev := range evs {
if ev.CreatedAt < sinceTime.V || ev.CreatedAt > untilTime.V {
t.Fatalf(
@@ -571,16 +440,14 @@ func TestQueryEventsByTimeRange(t *testing.T) {
}
func TestQueryEventsByTag(t *testing.T) {
db, events, ctx, cancel, tempDir := setupTestDB(t)
defer os.RemoveAll(tempDir) // Clean up after the test
defer cancel()
defer db.Close()
// Use shared database (read-only test)
db, ctx := GetSharedDB(t)
events := GetSharedEvents(t)
// Find an event with tags to use for testing
// Find an event with tags
var testTagEvent *event.E
for _, ev := range events {
if ev.Tags != nil && ev.Tags.Len() > 0 {
// Find a tag with at least 2 elements and first element of length 1
for _, tg := range *ev.Tags {
if tg.Len() >= 2 && len(tg.Key()) == 1 {
testTagEvent = ev
@@ -598,7 +465,6 @@ func TestQueryEventsByTag(t *testing.T) {
return
}
// Get the first tag with at least 2 elements and first element of length 1
var testTag *tag.T
for _, tg := range *testTagEvent.Tags {
if tg.Len() >= 2 && len(tg.Key()) == 1 {
@@ -607,7 +473,6 @@ func TestQueryEventsByTag(t *testing.T) {
}
}
// Create a tags filter with the test tag
tagsFilter := tag.NewS(testTag)
evs, err := db.QueryEvents(
@@ -619,12 +484,10 @@ func TestQueryEventsByTag(t *testing.T) {
t.Fatalf("Failed to query events by tag: %v", err)
}
// Verify we got results
if len(evs) == 0 {
t.Fatal("Expected events with tag, but got none")
}
// Verify all events have the tag
for i, ev := range evs {
var hasTag bool
for _, tg := range *ev.Tags {