| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/catalog" |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/work" |
| 12 | ) |
| 13 | |
| 14 | func TestDeliverNotificationQueuesPrivateLocalAndRecipientWorkAtomically(t *testing.T) { |
| 15 | ctx := context.Background() |
| 16 | store, err := storage.Open(ctx, filepath.Join(t.TempDir(), "tenant.db" )) |
| 17 | if err != nil { |
| 18 | t.Fatal(err) |
| 19 | } |
| 20 | defer store.Close() |
| 21 | secret, err := event.GenerateKey() |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | recipient, err := event.PublicKey(secret) |
| 26 | if err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | wrap := event.Event{CreatedAt: 10, Kind: event.KIND_WRAP, Tags: [][]string{{"p" , recipient}}, Content: "cipher" } |
| 30 | if err := event.Sign(&wrap, secret); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | tenant := &Tenant{store: store, meta: catalog.Tenant{ID: "tenant-1" }, publicURL: "http://relay.example" } |
| 34 | if err := tenant.deliverNotification(ctx, wrap, recipient); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | var count int |
| 38 | if err := store.DB().QueryRowContext(ctx, `SELECT count(*) FROM work_intents WHERE event_id=?` , wrap.ID).Scan(&count); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | if count != 2 { |
| 42 | t.Fatalf("queued intents = %d, want local broadcast and recipient delivery" , count) |
| 43 | } |
| 44 | var target string |
| 45 | if err := store.DB().QueryRowContext(ctx, `SELECT target FROM work_intents WHERE kind=?` , notificationDelivery).Scan(&target); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | if target != recipient { |
| 49 | t.Fatalf("delivery target = %q, want recipient %q" , target, recipient) |
| 50 | } |
| 51 | if err := store.DB().QueryRowContext(ctx, `SELECT count(*) FROM events WHERE kind=10050` ).Scan(&count); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if count != 0 { |
| 55 | t.Fatal("stored a recipient's kind-10050 list locally" ) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestNotificationRelayValidation(t *testing.T) { |
| 60 | if !validRelayURL("wss://inbox.example" ) || !validRelayURL("ws://localhost" ) { |
| 61 | t.Fatal("valid relay URL rejected" ) |
| 62 | } |
| 63 | if validRelayURL("https://inbox.example" ) || validRelayURL("relay.example" ) { |
| 64 | t.Fatal("non-relay URL accepted" ) |
| 65 | } |
| 66 | if !sameRelay("wss://relay.example/" , "wss://relay.example" ) { |
| 67 | t.Fatal("trailing slash not normalized" ) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func TestNotificationDeliveryRequiresRecipientInboxList(t *testing.T) { |
| 72 | ctx := context.Background() |
| 73 | store, err := storage.Open(ctx, filepath.Join(t.TempDir(), "tenant.db" )) |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | defer store.Close() |
| 78 | secret, err := event.GenerateKey() |
| 79 | if err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | recipient, err := event.PublicKey(secret) |
| 83 | if err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | wrap := event.Event{CreatedAt: 10, Kind: event.KIND_WRAP, Tags: [][]string{{"p" , recipient}}, Content: "cipher" } |
| 87 | if err := event.Sign(&wrap, secret); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | tenant := &Tenant{store: store} |
| 91 | if err := tenant.deliverNotification(ctx, wrap, recipient); err != nil { |
| 92 | t.Fatal(err) |
| 93 | } |
| 94 | handler := tenant.notificationHandlers()[notificationDelivery] |
| 95 | err = handler(ctx, work.Intent{Kind: notificationDelivery, EventID: wrap.ID, Target: recipient}) |
| 96 | if err == nil { |
| 97 | t.Fatal("delivery without kind-10050 inbox should remain retryable" ) |
| 98 | } |
| 99 | } |