Igor Sirotin 43ed60b641
feat: add Messaging API (pkg/messaging)
Implement the high-level, idiomatic Go Messaging API mirroring the Nim
MessagingClient, on top of the internal/ffi/liblogosdelivery bridge.

- MessagingClient: New/Start/Stop/Close, Subscribe/Unsubscribe,
  Send -> RequestID. (Named to match the Nim MessagingClient.)
- Unified Events() <-chan Event with a sealed Event interface
  (MessageReceived/Sent/Propagated/Error, ConnectionStatus). Events are dropped
  (never block) if a consumer falls behind.
- Event decoding handles liblogosdelivery's std/json wire format: received
  payload/meta arrive as JSON byte-int arrays (not base64), with base64 + null
  fallbacks; connectionStatus as an enum-name string. Unit-tested.
- Config aliases the kernel WakuNodeConf.
- examples/messaging: runnable demo.

Part of #106 (Store + kernel accessors follow after logos-delivery#3851).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 22:56:08 +03:00

108 lines
3.4 KiB
Go

package messaging
import (
"bytes"
"testing"
)
func TestDecodeEvent(t *testing.T) {
t.Run("message_received with int-array payload", func(t *testing.T) {
// liblogosdelivery serialises WakuMessage via std/json: payload/meta are
// arrays of byte integers, not base64. "hi" = [104,105].
const raw = `{"eventType":"message_received","messageHash":"0xabc",` +
`"message":{"contentTopic":"/app/1/c/proto","payload":[104,105],` +
`"meta":[1,2],"version":1,"timestamp":1717000000000000000,"ephemeral":true}}`
ev, err := decodeEvent(raw)
if err != nil {
t.Fatalf("decodeEvent: %v", err)
}
got, ok := ev.(MessageReceivedEvent)
if !ok {
t.Fatalf("got %T, want MessageReceivedEvent", ev)
}
if got.MessageHash != "0xabc" {
t.Errorf("messageHash = %q", got.MessageHash)
}
if !bytes.Equal(got.Message.Payload, []byte("hi")) {
t.Errorf("payload = %v, want %v", got.Message.Payload, []byte("hi"))
}
if !bytes.Equal(got.Message.Meta, []byte{1, 2}) {
t.Errorf("meta = %v", got.Message.Meta)
}
if got.Message.ContentTopic != "/app/1/c/proto" {
t.Errorf("contentTopic = %q", got.Message.ContentTopic)
}
if got.Message.Version != 1 || got.Message.Timestamp != 1717000000000000000 || !got.Message.Ephemeral {
t.Errorf("scalar fields wrong: %+v", got.Message)
}
})
t.Run("message_received with base64 payload (robustness)", func(t *testing.T) {
const raw = `{"eventType":"message_received","messageHash":"0x1",` +
`"message":{"contentTopic":"/a/1/b/proto","payload":"aGk=","ephemeral":false}}`
ev, err := decodeEvent(raw)
if err != nil {
t.Fatalf("decodeEvent: %v", err)
}
if got := ev.(MessageReceivedEvent); !bytes.Equal(got.Message.Payload, []byte("hi")) {
t.Errorf("payload = %v", got.Message.Payload)
}
})
t.Run("message_sent", func(t *testing.T) {
ev, err := decodeEvent(`{"eventType":"message_sent","requestId":"req-1","messageHash":"0x9"}`)
if err != nil {
t.Fatal(err)
}
got, ok := ev.(MessageSentEvent)
if !ok || got.RequestID != "req-1" || got.MessageHash != "0x9" {
t.Fatalf("got %#v", ev)
}
})
t.Run("message_propagated", func(t *testing.T) {
ev, err := decodeEvent(`{"eventType":"message_propagated","requestId":"req-2","messageHash":"0x8"}`)
if err != nil {
t.Fatal(err)
}
if got, ok := ev.(MessagePropagatedEvent); !ok || got.RequestID != "req-2" {
t.Fatalf("got %#v", ev)
}
})
t.Run("message_error", func(t *testing.T) {
ev, err := decodeEvent(`{"eventType":"message_error","requestId":"req-3","messageHash":"0x7","error":"boom"}`)
if err != nil {
t.Fatal(err)
}
got, ok := ev.(MessageErrorEvent)
if !ok || got.RequestID != "req-3" || got.Err != "boom" {
t.Fatalf("got %#v", ev)
}
})
t.Run("connection_status_change", func(t *testing.T) {
for in, want := range map[string]ConnectionStatus{
"Connected": Connected,
"PartiallyConnected": PartiallyConnected,
"Disconnected": Disconnected,
} {
raw := `{"eventType":"connection_status_change","connectionStatus":"` + in + `"}`
ev, err := decodeEvent(raw)
if err != nil {
t.Fatalf("%s: %v", in, err)
}
if got := ev.(ConnectionStatusEvent); got.Status != want {
t.Errorf("%s -> %v, want %v", in, got.Status, want)
}
}
})
t.Run("unknown event type is ignored", func(t *testing.T) {
ev, err := decodeEvent(`{"eventType":"something_new","foo":1}`)
if err != nil || ev != nil {
t.Fatalf("got ev=%v err=%v, want nil,nil", ev, err)
}
})
}