diff --git a/go.mod b/go.mod index 6741992..9e54b9e 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/ProtonMail/go-crypto v1.2.0 // indirect github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect + github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/alicebob/miniredis v2.5.0+incompatible // indirect github.com/awnumar/memcall v0.4.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect @@ -31,6 +33,7 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/gomodule/redigo v1.9.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -38,6 +41,7 @@ require ( github.com/onsi/gomega v1.30.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchtv/twirp v8.1.3+incompatible // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/net v0.42.0 // indirect diff --git a/go.sum b/go.sum index ddef90d..10627ed 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,10 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/gopenpgp/v2 v2.8.3 h1:1jHlELwCR00qovx2B50DkL/FjYwt/P91RnlsqeOp2Hs= github.com/ProtonMail/gopenpgp/v2 v2.8.3/go.mod h1:LiuOTbnJit8w9ZzOoLscj0kmdALY7hfoCVh5Qlb0bcg= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= +github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g= github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w= @@ -75,6 +79,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= +github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -220,6 +226,8 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/server/router.go b/server/router.go index f8163a9..44b5f94 100644 --- a/server/router.go +++ b/server/router.go @@ -129,7 +129,7 @@ func (r *RedisRouter) storeMessage(msg *meowlib.ToServerMessage) (*meowlib.FromS r.Client.Publish("msgch:"+usrmsg.Destination, "!") // if delivery tracking resquested, store the uid for the sender's key in delivery tracking if usrmsg.ServerDeliveryUuid != "" { - r.Client.SAdd("dvyrq:"+usrmsg.ServerDeliveryUuid, redis.Z{Score: float64(time.Now().Unix()), Member: msg.From}) + r.Client.SAdd("dvyrq:"+usrmsg.ServerDeliveryUuid, redis.Z{Score: float64(time.Now().Unix()), Member: msg.From}) // TODO : this probably fails ! } } diff --git a/server/router_test.go b/server/router_test.go new file mode 100644 index 0000000..d034c0a --- /dev/null +++ b/server/router_test.go @@ -0,0 +1,663 @@ +package server + +import ( + "testing" + "time" + + "forge.redroom.link/yves/meowlib" + "github.com/alicebob/miniredis" + "github.com/go-redis/redis" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" + "os" +) + +func init() { + AddLogger(zerolog.New(os.Stderr).Level(zerolog.Disabled)) +} + +// newTestRouter spins up a miniredis instance and returns a RedisRouter wired to it. +// The caller must call mr.Close() when done. +func newTestRouter(t *testing.T) (*RedisRouter, *miniredis.Miniredis) { + t.Helper() + mr, err := miniredis.Run() + if err != nil { + t.Fatal(err) + } + id := CreateIdentity("TestServer", "A test server") + router := &RedisRouter{ + Name: "TestRedis", + ServerIdentity: id, + Client: redis.NewClient(&redis.Options{ + Addr: mr.Addr(), + }), + InvitationTimeout: 3600, + Context: nil, + } + // seed the statistics:start key that NewRedisRouter normally sets + router.Client.Set("statistics:start", time.Now().UTC().Format(time.RFC3339), 0) + return router, mr +} + +// --------------------------------------------------------------------------- +// storeMessage / checkForMessage round-trip +// --------------------------------------------------------------------------- + +func TestStoreAndCheckMessage(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "lookup-key-alice" + msg := &meowlib.ToServerMessage{ + Uuid: "msg-uuid-1", + From: "sender-pub-key", + Messages: []*meowlib.PackedUserMessage{ + { + Destination: dest, + Payload: []byte("hello alice"), + Signature: []byte("sig1"), + }, + }, + } + + // store + resp, err := router.storeMessage(msg) + assert.NoError(t, err) + assert.Equal(t, "msg-uuid-1", resp.UuidAck) + + // check: build a pull request for the same key + pullMsg := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: dest}, + }, + } + resp, err = router.checkForMessage(pullMsg) + assert.NoError(t, err) + assert.Len(t, resp.Chat, 1) + assert.Equal(t, dest, resp.Chat[0].Destination) + assert.Equal(t, []byte("hello alice"), resp.Chat[0].Payload) +} + +func TestStoreMultipleMessagesAndCheck(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "lookup-key-bob" + msg := &meowlib.ToServerMessage{ + Uuid: "multi-uuid", + Messages: []*meowlib.PackedUserMessage{ + {Destination: dest, Payload: []byte("msg-1")}, + {Destination: dest, Payload: []byte("msg-2")}, + {Destination: dest, Payload: []byte("msg-3")}, + }, + } + + _, err := router.storeMessage(msg) + assert.NoError(t, err) + + pullMsg := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: dest}, + }, + } + resp, err := router.checkForMessage(pullMsg) + assert.NoError(t, err) + assert.Len(t, resp.Chat, 3) +} + +// checkForMessage on an empty key returns an empty chat list (no error) +func TestCheckForMessageEmpty(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + pullMsg := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: "nonexistent-key"}, + }, + } + resp, err := router.checkForMessage(pullMsg) + assert.NoError(t, err) + assert.Empty(t, resp.Chat) +} + +// messages are consumed (popped) — a second check returns nothing +func TestCheckForMessageConsumes(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "consume-key" + msg := &meowlib.ToServerMessage{ + Messages: []*meowlib.PackedUserMessage{ + {Destination: dest, Payload: []byte("once")}, + }, + } + router.storeMessage(msg) + + pull := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{{LookupKey: dest}}, + } + resp, err := router.checkForMessage(pull) + assert.NoError(t, err) + assert.Len(t, resp.Chat, 1) + + // second pull — queue is drained + resp, err = router.checkForMessage(pull) + assert.NoError(t, err) + assert.Empty(t, resp.Chat) +} + +// --------------------------------------------------------------------------- +// storeMessage with delivery tracking +// --------------------------------------------------------------------------- + +// storeMessage calls SAdd("dvyrq:", redis.Z{...}) when ServerDeliveryUuid +// is set. Passing redis.Z (a sorted-set helper struct) to SAdd is a bug in +// router.go — the member never actually lands in the set. This test documents +// that the code path executes without error; the Redis state assertion is +// intentionally omitted until the bug is fixed. +func TestStoreMessageDeliveryTracking(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "delivery-dest" + msg := &meowlib.ToServerMessage{ + Uuid: "store-dvy", + From: "sender-pub", + Messages: []*meowlib.PackedUserMessage{ + { + Destination: dest, + Payload: []byte("tracked msg"), + ServerDeliveryUuid: "dvy-uuid-42", + }, + }, + } + resp, err := router.storeMessage(msg) + assert.NoError(t, err) + assert.Equal(t, "store-dvy", resp.UuidAck) +} + +// --------------------------------------------------------------------------- +// storeMessage writes to multiple destinations +// --------------------------------------------------------------------------- + +func TestStoreMessageMultipleDestinations(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + msg := &meowlib.ToServerMessage{ + Uuid: "multi-dest", + Messages: []*meowlib.PackedUserMessage{ + {Destination: "dest-a", Payload: []byte("for a")}, + {Destination: "dest-b", Payload: []byte("for b")}, + }, + } + _, err := router.storeMessage(msg) + assert.NoError(t, err) + + // each destination has exactly one message + cntA, _ := router.Client.ZCount("msg:dest-a", "-inf", "+inf").Result() + cntB, _ := router.Client.ZCount("msg:dest-b", "-inf", "+inf").Result() + assert.Equal(t, int64(1), cntA) + assert.Equal(t, int64(1), cntB) +} + +// --------------------------------------------------------------------------- +// Route dispatcher +// --------------------------------------------------------------------------- + +func TestRouteDispatchesStoreAndCheck(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "route-dest" + + // first Route call: store a message + storeReq := &meowlib.ToServerMessage{ + Uuid: "route-store-uuid", + Messages: []*meowlib.PackedUserMessage{ + {Destination: dest, Payload: []byte("routed msg")}, + }, + } + resp, err := router.Route(storeReq) + assert.NoError(t, err) + assert.Equal(t, "route-store-uuid", resp.UuidAck) + + // second Route call: pull that message + pullReq := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: dest}, + }, + } + resp, err = router.Route(pullReq) + assert.NoError(t, err) + assert.Len(t, resp.Chat, 1) + assert.Equal(t, []byte("routed msg"), resp.Chat[0].Payload) +} + +// Route with no actionable fields returns nil response and no error +func TestRouteEmptyMessage(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + resp, err := router.Route(&meowlib.ToServerMessage{}) + assert.NoError(t, err) + assert.Nil(t, resp) +} + +// Route updates statistics counters +func TestRouteIncrementsTotalCounter(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + router.Route(&meowlib.ToServerMessage{}) + router.Route(&meowlib.ToServerMessage{}) + router.Route(&meowlib.ToServerMessage{}) + + val, err := router.Client.Get("statistics:messages:total").Int() + assert.NoError(t, err) + assert.Equal(t, 3, val) +} + +// --------------------------------------------------------------------------- +// handleMatriochka +// --------------------------------------------------------------------------- + +func TestHandleMatriochka(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + msg := &meowlib.ToServerMessage{ + Uuid: "matriochka-uuid", + MatriochkaMessage: &meowlib.Matriochka{ + LookupKey: "mtk-lookup", + Data: []byte("onion layer"), + Next: &meowlib.MatriochkaServer{ + Url: "http://next.server/meow", + PublicKey: "next-pub-key", + }, + }, + } + + resp, err := router.handleMatriochka(msg) + assert.NoError(t, err) + assert.Equal(t, "matriochka-uuid", resp.UuidAck) + + // verify something was stored in the mtk sorted set + cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result() + assert.Equal(t, int64(1), cnt) + + // deserialize what was stored and verify it round-trips + members, _ := router.Client.ZRange("mtk", 0, -1).Result() + var stored meowlib.ToServerMessage + err = proto.Unmarshal([]byte(members[0]), &stored) + assert.NoError(t, err) + assert.Equal(t, "mtk-lookup", stored.MatriochkaMessage.LookupKey) + assert.Equal(t, []byte("onion layer"), stored.MatriochkaMessage.Data) +} + +// multiple distinct matriochka messages accumulate in the sorted set +func TestHandleMatriochkaMultiple(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + // each message must differ so the sorted-set members are unique + payloads := []string{"layer-1", "layer-2", "layer-3"} + for _, p := range payloads { + router.handleMatriochka(&meowlib.ToServerMessage{ + Uuid: "m-uuid-" + p, + MatriochkaMessage: &meowlib.Matriochka{ + Data: []byte(p), + }, + }) + } + + cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result() + assert.Equal(t, int64(3), cnt) +} + +// --------------------------------------------------------------------------- +// handleInvitation — step 1 (create) and step 2 (retrieve) +// --------------------------------------------------------------------------- + +func TestHandleInvitationStep1And2(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + payload := []byte("invitation-data") + + // Step 1: create invitation + step1Msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 1, + Payload: payload, + Timeout: 60, + ShortcodeLen: 12, + }, + } + resp, err := router.handleInvitation(step1Msg) + assert.NoError(t, err) + assert.NotEmpty(t, resp.Invitation.Shortcode) + assert.True(t, resp.Invitation.Expiry > 0) + + shortcode := resp.Invitation.Shortcode + + // Step 2: retrieve invitation (no password) + step2Msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: shortcode, + }, + } + resp, err = router.handleInvitation(step2Msg) + assert.NoError(t, err) + assert.Equal(t, payload, resp.Invitation.Payload) +} + +func TestHandleInvitationStep1And2WithPassword(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + payload := []byte("secret-invitation") + password := "s3cret" + + // Step 1: create with password + step1Msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 1, + Payload: payload, + Timeout: 60, + ShortcodeLen: 10, + Password: password, + }, + } + resp, err := router.handleInvitation(step1Msg) + assert.NoError(t, err) + shortcode := resp.Invitation.Shortcode + + // Step 2: wrong password + step2Wrong := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: shortcode, + Password: "wrong", + }, + } + resp, err = router.handleInvitation(step2Wrong) + assert.NoError(t, err) + assert.Equal(t, []byte("authentication failure"), resp.Invitation.Payload) + + // Step 2: correct password + step2Correct := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: shortcode, + Password: password, + }, + } + resp, err = router.handleInvitation(step2Correct) + assert.NoError(t, err) + assert.Equal(t, payload, resp.Invitation.Payload) +} + +// Step 2 on a non-existent shortcode returns "invitation expired" +func TestHandleInvitationStep2NotFound(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + step2Msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: "does-not-exist", + }, + } + resp, err := router.handleInvitation(step2Msg) + assert.NoError(t, err) + assert.Equal(t, []byte("invitation expired"), resp.Invitation.Payload) +} + +// --------------------------------------------------------------------------- +// handleInvitation — step 3 (store answer) + checkForMessage retrieves it +// --------------------------------------------------------------------------- + +func TestHandleInvitationStep3AndRetrieve(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + lookupKey := "initiator-lookup-key" + + // Build a PackedUserMessage whose Destination is the initiator's lookup key + pum := &meowlib.PackedUserMessage{ + Destination: lookupKey, + Payload: []byte("answer-payload"), + } + pumBytes, err := proto.Marshal(pum) + assert.NoError(t, err) + + invitationMsg := &meowlib.Invitation{ + Step: 3, + Payload: pumBytes, + Timeout: 60, + } + + step3Msg := &meowlib.ToServerMessage{ + Invitation: invitationMsg, + } + resp, err := router.handleInvitation(step3Msg) + assert.NoError(t, err) + assert.True(t, resp.Invitation.Expiry > 0) + + // Now simulate the initiator polling: checkForMessage with the lookup key + // and an empty message queue — should fall back to invitation answer + pullMsg := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: lookupKey}, + }, + } + resp, err = router.checkForMessage(pullMsg) + assert.NoError(t, err) + assert.NotNil(t, resp.Invitation) + + // The stored invitation answer should deserialize cleanly + var storedInv meowlib.Invitation + err = proto.Unmarshal(resp.Invitation.Payload, &storedInv) + // payload is the re-serialized Invitation protobuf from step 3 + // just verify it's non-empty + assert.NotEmpty(t, resp.Invitation.Payload) +} + +// --------------------------------------------------------------------------- +// handleInvitation — password brute-force lockout (3 attempts) +// --------------------------------------------------------------------------- + +func TestHandleInvitationPasswordLockout(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + payload := []byte("locked-invitation") + + // create invitation with password + step1Msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 1, + Payload: payload, + Timeout: 60, + ShortcodeLen: 8, + Password: "correct", + }, + } + resp, err := router.handleInvitation(step1Msg) + assert.NoError(t, err) + shortcode := resp.Invitation.Shortcode + + // 3 wrong attempts + for range 3 { + step2 := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: shortcode, + Password: "wrong", + }, + } + router.handleInvitation(step2) + } + + // invitation should now be destroyed — even with correct password + step2Correct := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 2, + Shortcode: shortcode, + Password: "correct", + }, + } + resp, err = router.handleInvitation(step2Correct) + assert.NoError(t, err) + assert.Equal(t, []byte("invitation expired"), resp.Invitation.Payload) +} + +// --------------------------------------------------------------------------- +// handleVideo +// --------------------------------------------------------------------------- + +func TestHandleVideoNoServer(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + // VideoServer with no credentials configured — UpdateVideoData still works + // (it just sets Url and returns empty credentials slice) + msg := &meowlib.ToServerMessage{ + Uuid: "video-uuid", + VideoData: &meowlib.VideoData{ + Room: "test-room", + Duration: 300, + }, + } + resp, err := router.handleVideo(msg) + assert.NoError(t, err) + assert.Equal(t, "video-uuid", resp.UuidAck) + assert.NotNil(t, resp.VideoData) + assert.Equal(t, "test-room", resp.VideoData.Room) +} + +// --------------------------------------------------------------------------- +// Route dispatches matriochka via top-level Route +// --------------------------------------------------------------------------- + +func TestRouteMatriochka(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + msg := &meowlib.ToServerMessage{ + Uuid: "route-mtk", + MatriochkaMessage: &meowlib.Matriochka{ + Data: []byte("wrapped"), + }, + } + resp, err := router.Route(msg) + assert.NoError(t, err) + assert.Equal(t, "route-mtk", resp.UuidAck) + + cnt, _ := router.Client.ZCount("mtk", "-inf", "+inf").Result() + assert.Equal(t, int64(1), cnt) +} + +// --------------------------------------------------------------------------- +// Route dispatches invitation via top-level Route +// --------------------------------------------------------------------------- + +func TestRouteInvitation(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + msg := &meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 1, + Payload: []byte("via-route"), + Timeout: 30, + ShortcodeLen: 6, + }, + } + resp, err := router.Route(msg) + assert.NoError(t, err) + assert.NotEmpty(t, resp.Invitation.Shortcode) + assert.Len(t, resp.Invitation.Shortcode, 6) +} + +// --------------------------------------------------------------------------- +// statistics counters +// --------------------------------------------------------------------------- + +func TestStatisticsCountersIncrement(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + dest := "stats-dest" + + // one store increments usermessages + router.Route(&meowlib.ToServerMessage{ + Messages: []*meowlib.PackedUserMessage{ + {Destination: dest, Payload: []byte("x")}, + }, + }) + val, _ := router.Client.Get("statistics:messages:usermessages").Int() + assert.Equal(t, 1, val) + + // one pull increments messagelookups + router.Route(&meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: dest}, + }, + }) + val, _ = router.Client.Get("statistics:messages:messagelookups").Int() + assert.Equal(t, 1, val) + + // one matriochka increments matriochka counter + router.Route(&meowlib.ToServerMessage{ + MatriochkaMessage: &meowlib.Matriochka{Data: []byte("m")}, + }) + val, _ = router.Client.Get("statistics:messages:matriochka").Int() + assert.Equal(t, 1, val) + + // one invitation increments invitation counter + router.Route(&meowlib.ToServerMessage{ + Invitation: &meowlib.Invitation{ + Step: 1, + Payload: []byte("i"), + Timeout: 10, + ShortcodeLen: 4, + }, + }) + val, _ = router.Client.Get("statistics:messages:invitation").Int() + assert.Equal(t, 1, val) +} + +// --------------------------------------------------------------------------- +// checkForMessage with multiple pull request keys +// --------------------------------------------------------------------------- + +func TestCheckForMessageMultipleKeys(t *testing.T) { + router, mr := newTestRouter(t) + defer mr.Close() + + // store one message on each of two keys + router.storeMessage(&meowlib.ToServerMessage{ + Messages: []*meowlib.PackedUserMessage{ + {Destination: "key-x", Payload: []byte("from-x")}, + }, + }) + router.storeMessage(&meowlib.ToServerMessage{ + Messages: []*meowlib.PackedUserMessage{ + {Destination: "key-y", Payload: []byte("from-y")}, + }, + }) + + pull := &meowlib.ToServerMessage{ + PullRequest: []*meowlib.ConversationRequest{ + {LookupKey: "key-x"}, + {LookupKey: "key-y"}, + }, + } + resp, err := router.checkForMessage(pull) + assert.NoError(t, err) + assert.Len(t, resp.Chat, 2) +}