10 "github.com/stretchr/testify/require"
15 func TestRouterKeepsSinglesAndBatchesInOneDeviceOrder(t *testing.T) {
16 router, err := NewRouter([]string{testDeviceID})
17 require.NoError(t, err)
19 queue, exists := router.Queue(testDeviceID)
20 require.True(t, exists)
22 first := routerCommand(t, testDeviceID, "relay", time.Minute)
23 second := routerCommand(t, testDeviceID, "output", time.Minute)
24 third := routerCommand(t, testDeviceID, "pulse", time.Minute)
25 last := routerCommand(t, testDeviceID, "batch", time.Minute)
26 batch, err := protocol.BuildCommandBatch(testDeviceID, second.Metadata, protocol.BatchPayload{
27 Commands: []protocol.BatchCommand{
29 Metadata: second.Metadata,
30 Capability: second.Capability,
31 Payload: second.Payload,
34 Metadata: third.Metadata,
35 Capability: third.Capability,
36 Payload: third.Payload,
40 require.NoError(t, err)
42 for _, publication := range []protocol.Publication{commandPublication(t, first), batch, commandPublication(t, last)} {
43 require.NoError(t, router.Handle(t.Context(), publication.Topic, publication.Data, publication.Retain))
46 require.Len(t, queue, 3)
47 require.Equal(t, []Command{first}, <-queue)
48 require.Equal(t, []Command{second, third}, <-queue)
49 require.Equal(t, []Command{last}, <-queue)
52 func TestRouterBuildsOnlyExplicitDeviceSubscriptions(t *testing.T) {
53 router, err := NewRouter([]string{testOtherDeviceID, testDeviceID})
54 require.NoError(t, err)
56 first, err := protocol.CommandSubscription(testDeviceID)
57 require.NoError(t, err)
59 second, err := protocol.CommandSubscription(testOtherDeviceID)
60 require.NoError(t, err)
62 subscriptions := router.Subscriptions()
63 require.Equal(t, []protocol.Subscription{first, second}, subscriptions)
65 subscriptions[0].Filter = "changed"
67 require.Equal(t, first, router.Subscriptions()[0])
69 queue, exists := router.Queue("unknown")
70 require.False(t, exists)
73 _, err = NewRouter([]string{"invalid"})
76 _, err = NewRouter([]string{testDeviceID, testDeviceID})
79 readOnly, err := NewRouter(nil)
80 require.NoError(t, err)
81 require.Empty(t, readOnly.Subscriptions())
84 func TestRouterRejectsInvalidUnownedAndExpiredDeliveries(t *testing.T) {
85 router, err := NewRouter([]string{testDeviceID})
86 require.NoError(t, err)
88 queue, exists := router.Queue(testDeviceID)
89 require.True(t, exists)
91 valid := commandPublication(t, routerCommand(t, testDeviceID, "relay", time.Minute))
92 unowned := commandPublication(t, routerCommand(t, testOtherDeviceID, "relay", time.Minute))
93 expired := commandPublication(t, routerCommand(t, testDeviceID, "relay", -time.Second))
94 value, err := protocol.BuildReported(testDeviceID, "relay", protocolMetadata(t), protocol.ValuePayload{Value: json.RawMessage("true")})
95 require.NoError(t, err)
97 availability, err := protocol.BuildAvailability(testDeviceID, protocolMetadata(t), protocol.AvailabilityPayload{Status: "online"})
98 require.NoError(t, err)
100 testCases := []struct {
102 publication protocol.Publication
105 name: "retained command rejects",
106 publication: protocol.Publication{
113 name: "malformed payload rejects",
114 publication: protocol.Publication{
120 name: "unknown topic rejects",
121 publication: protocol.Publication{
127 name: "unowned device rejects",
128 publication: unowned,
131 name: "expired command rejects",
132 publication: expired,
135 name: "reported value is not command intake",
139 name: "availability is not command intake",
140 publication: availability,
144 for _, tc := range testCases {
145 t.Run(tc.name, func(t *testing.T) {
146 require.Error(t, router.Handle(t.Context(), tc.publication.Topic, tc.publication.Data, tc.publication.Retain))
147 require.Empty(t, queue)
152 func TestRouterRejectsWholeBatchWhenOneMemberExpires(t *testing.T) {
153 router, err := NewRouter([]string{testDeviceID})
154 require.NoError(t, err)
156 first := routerCommand(t, testDeviceID, "relay", time.Minute)
157 second := routerCommand(t, testDeviceID, "pulse", -time.Second)
158 batch, err := protocol.BuildCommandBatch(testDeviceID, first.Metadata, protocol.BatchPayload{
159 Commands: []protocol.BatchCommand{
161 Metadata: first.Metadata,
162 Capability: first.Capability,
163 Payload: first.Payload,
166 Metadata: second.Metadata,
167 Capability: second.Capability,
168 Payload: second.Payload,
172 require.NoError(t, err)
173 require.Error(t, router.Handle(t.Context(), batch.Topic, batch.Data, false))
175 queue, exists := router.Queue(testDeviceID)
176 require.True(t, exists)
177 require.Empty(t, queue)
180 func TestRouterBackpressureHonorsCancellationAndEarliestExpiry(t *testing.T) {
181 synctest.Test(t, func(t *testing.T) {
182 router, err := NewRouter([]string{testDeviceID})
183 require.NoError(t, err)
185 valid := commandPublication(t, routerCommand(t, testDeviceID, "relay", time.Hour))
187 for range commandQueueCapacity {
188 require.NoError(t, router.Handle(t.Context(), valid.Topic, valid.Data, false))
191 ctx, cancel := context.WithCancel(t.Context())
194 finished := make(chan error, 1)
196 finished <- router.Handle(ctx, valid.Topic, valid.Data, false)
200 require.Empty(t, finished)
202 require.ErrorIs(t, <-finished, context.Canceled)
204 short := commandPublication(t, routerCommand(t, testDeviceID, "pulse", time.Second))
205 require.ErrorIs(t, router.Handle(t.Context(), short.Topic, short.Data, false), context.DeadlineExceeded)
207 first := routerCommand(t, testDeviceID, "relay", time.Minute)
208 second := routerCommand(t, testDeviceID, "pulse", time.Second)
209 batch, err := protocol.BuildCommandBatch(testDeviceID, first.Metadata, protocol.BatchPayload{Commands: []protocol.BatchCommand{
211 Metadata: first.Metadata,
212 Capability: first.Capability,
213 Payload: first.Payload,
216 Metadata: second.Metadata,
217 Capability: second.Capability,
218 Payload: second.Payload,
221 require.NoError(t, err)
225 require.ErrorIs(t, router.Handle(t.Context(), batch.Topic, batch.Data, false), context.DeadlineExceeded)
226 require.Equal(t, time.Second, time.Since(start))
228 queue, exists := router.Queue(testDeviceID)
229 require.True(t, exists)
230 require.Len(t, queue, commandQueueCapacity)
231 require.ErrorIs(t, router.Handle(ctx, valid.Topic, valid.Data, false), context.Canceled)
233 require.NoError(t, router.Handle(t.Context(), valid.Topic, valid.Data, false))
237 func routerCommand(t *testing.T, deviceID string, capability string, lifetime time.Duration) Command {
240 metadata := protocolMetadata(t)
241 expiry := time.Now().UTC().Add(lifetime)
242 metadata.ExpiresAt = &expiry
246 Capability: capability,
248 Payload: protocol.ValuePayload{Value: json.RawMessage("true")},
252 func commandPublication(t *testing.T, command Command) protocol.Publication {
255 publication, err := protocol.BuildCommand(command.DeviceID, command.Capability, command.Metadata, command.Payload)
256 require.NoError(t, err)
261 func protocolMetadata(t *testing.T) protocol.Metadata {
264 now := time.Now().UTC()
265 id, err := protocol.NewID(now)
266 require.NoError(t, err)
268 return protocol.Metadata{