11 "github.com/stretchr/testify/require"
17 func TestDriverPollSharesOneSnapshotAcrossThermostats(t *testing.T) {
18 synctest.Test(t, func(t *testing.T) {
21 second.MAC = "fc610e7f98ab"
22 client := &fakeGateway{states: []State{first, second}}
23 transport := &fakeTransport{}
24 integration, _ := testDriver(t, client, transport, Thermostat{
25 DeviceID: testDeviceID,
28 DeviceID: testSecondID,
32 err := integration.poll(t.Context())
33 require.NoError(t, err)
34 require.Len(t, client.starts, 1)
35 require.Len(t, transport.messages(t, protocol.Availability), 2)
36 require.Len(t, transport.messages(t, protocol.Reported), 2)
40 func TestDriverMarksOnlyMissingThermostatOfflineAfterThreePollsAndRecovers(t *testing.T) {
41 synctest.Test(t, func(t *testing.T) {
43 second.MAC = "fc610e7f98ab"
44 client := &fakeGateway{states: []State{testState(), second}}
45 transport := &fakeTransport{}
46 integration, _ := testDriver(t, client, transport, Thermostat{
47 DeviceID: testDeviceID,
50 DeviceID: testSecondID,
54 require.NoError(t, integration.poll(t.Context()))
56 client.states = []State{second}
59 require.NoError(t, integration.poll(t.Context()))
60 require.Len(t, transport.messages(t, protocol.Availability), 2)
63 require.NoError(t, integration.poll(t.Context()))
65 messages := transport.messages(t, protocol.Availability)
66 require.Len(t, messages, 3)
67 require.Equal(t, testDeviceID, messages[2].Address.ID)
68 require.Equal(t, protocol.AvailabilityPayload{Status: "offline"}, messages[2].Payload)
69 require.NoError(t, integration.poll(t.Context()))
70 require.Len(t, transport.messages(t, protocol.Availability), 3)
72 client.states = []State{testState(), second}
74 require.NoError(t, integration.poll(t.Context()))
76 messages = transport.messages(t, protocol.Availability)
77 require.Len(t, messages, 4)
78 require.Equal(t, protocol.AvailabilityPayload{Status: "online"}, messages[3].Payload)
82 func TestDriverFailedPollsHaveNoPrematureStartupAvailability(t *testing.T) {
83 synctest.Test(t, func(t *testing.T) {
84 client := &fakeGateway{err: errors.New("gateway unavailable")}
85 transport := &fakeTransport{}
86 integration, _ := testDriver(t, client, transport)
89 require.NoError(t, integration.poll(t.Context()))
90 require.Empty(t, transport.messages(t, protocol.Availability))
93 require.NoError(t, integration.poll(t.Context()))
95 messages := transport.messages(t, protocol.Availability)
96 require.Len(t, messages, 1)
97 require.Equal(t, protocol.AvailabilityPayload{Status: "offline"}, messages[0].Payload)
100 client.states = []State{testState()}
102 require.NoError(t, integration.poll(t.Context()))
104 client.err = errors.New("gateway unavailable")
106 require.NoError(t, integration.poll(t.Context()))
107 require.Len(t, transport.messages(t, protocol.Availability), 2)
111 func TestDriverRunForwardsCommandsRefreshesAndStopsWithoutOutage(t *testing.T) {
112 synctest.Test(t, func(t *testing.T) {
113 client := &fakeGateway{states: []State{testState()}}
114 transport := &fakeTransport{}
115 integration, router := testDriver(t, client, transport)
116 ctx, cancel := context.WithCancel(t.Context())
119 integration.Refresh()
120 integration.Refresh()
122 done := make(chan error, 1)
125 done <- integration.Run(ctx)
129 require.Len(t, transport.messages(t, protocol.Descriptor), 1)
130 require.Equal(t, 1, client.startCount())
131 time.Sleep(6 * time.Second)
133 require.Equal(t, 2, client.startCount())
135 command := testCommand()
136 publication, err := protocol.BuildCommand(command.DeviceID, command.Capability, command.Metadata, command.Payload)
137 require.NoError(t, err)
138 require.NoError(t, router.Handle(ctx, publication.Topic, publication.Data, false))
139 require.NoError(t, router.Handle(ctx, publication.Topic, publication.Data, false))
141 time.Sleep(40 * time.Second)
143 require.Len(t, transport.messages(t, protocol.Result), 2)
145 require.ErrorIs(t, <-done, context.Canceled)
146 require.Len(t, client.actions, 3)
148 messages := transport.messages(t, protocol.Availability)
150 for _, message := range messages {
151 require.Equal(t, protocol.AvailabilityPayload{Status: "online"}, message.Payload)
156 func TestDriverConstructorValidatesAndCopiesSettings(t *testing.T) {
157 router, err := driver.NewRouter([]string{testDeviceID, testSecondID})
158 require.NoError(t, err)
160 publisher := driver.NewPublisher(&fakeTransport{})
162 DeviceID: testDeviceID,
165 testCases := []struct {
170 name: "empty gateway is rejected",
173 name: "invalid ID is rejected",
174 settings: Settings{Thermostats: []Thermostat{{
180 name: "invalid MAC is rejected",
181 settings: Settings{Thermostats: []Thermostat{{
182 DeviceID: testDeviceID,
187 name: "duplicate ID is rejected",
188 settings: Settings{Thermostats: []Thermostat{valid, valid}},
191 name: "MAC uniqueness ignores letter case",
192 settings: Settings{Thermostats: []Thermostat{valid, {
193 DeviceID: testSecondID,
194 MAC: strings.ToUpper(testMAC),
198 name: "poll interval cannot exceed five minutes",
200 PollInterval: time.Hour,
201 Thermostats: []Thermostat{valid},
205 name: "poll interval cannot be below ten seconds",
207 PollInterval: time.Second,
208 Thermostats: []Thermostat{valid},
213 for _, tc := range testCases {
214 t.Run(tc.name, func(t *testing.T) {
215 _, err = New(tc.settings, publisher, router, &fakeGateway{})
216 require.Error(t, err)
220 emptyRouter, err := driver.NewRouter(nil)
221 require.NoError(t, err)
223 _, err = New(Settings{Thermostats: []Thermostat{valid}}, publisher, emptyRouter, &fakeGateway{})
224 require.ErrorContains(t, err, "queue")
226 settings := Settings{
227 PollInterval: 10 * time.Second,
228 Thermostats: []Thermostat{valid},
230 integration, err := New(settings, publisher, router, &fakeGateway{})
231 require.NoError(t, err)
233 settings.Thermostats[0].MAC = "bad"
234 require.Equal(t, testMAC, integration.thermostats[0].settings.MAC)
235 require.Equal(t, 10*time.Second, integration.pollInterval)
238 func TestDriverReturnsPublicationFailuresAndCallerCancellation(t *testing.T) {
239 synctest.Test(t, func(t *testing.T) {
240 failure := errors.New("publication unavailable")
241 transport := &fakeTransport{
242 failKind: protocol.Descriptor,
245 integration, _ := testDriver(t, &fakeGateway{states: []State{testState()}}, transport)
246 require.ErrorIs(t, integration.Run(t.Context()), failure)
248 transport.failKind = protocol.Reported
250 require.ErrorIs(t, integration.Run(t.Context()), failure)
252 transport.failKind = protocol.Availability
253 integration.thermostats[0].missed = 2
254 integration.gateway.client = &fakeGateway{}
255 require.ErrorIs(t, integration.poll(t.Context()), failure)
257 ctx, cancel := context.WithCancel(t.Context())
260 require.ErrorIs(t, integration.poll(ctx), context.Canceled)
264 func TestDriverReturnsRefreshAndPeriodicPublicationFailures(t *testing.T) {
265 for _, refresh := range []bool{false, true} {
266 t.Run(publicationFailureName(refresh), func(t *testing.T) {
267 synctest.Test(t, func(t *testing.T) {
268 client := &fakeGateway{states: []State{testState()}}
269 transport := &fakeTransport{}
270 integration, _ := testDriver(t, client, transport)
271 ctx, cancel := context.WithCancel(t.Context())
274 done := make(chan error, 1)
277 done <- integration.Run(ctx)
282 failure := errors.New("publication unavailable")
286 transport.failKind = protocol.Reported
287 transport.failure = failure
288 transport.mu.Unlock()
291 client.states[0].TargetTemperature = 25
295 integration.Refresh()
298 require.ErrorIs(t, <-done, failure)
304 func TestDriverReturnsCommandPublicationFailuresAndJoinsForwarders(t *testing.T) {
305 synctest.Test(t, func(t *testing.T) {
306 client := &fakeGateway{states: []State{testState()}}
307 transport := &fakeTransport{
308 failKind: protocol.Result,
309 failure: errors.New("result unavailable"),
311 integration, router := testDriver(t, client, transport)
312 command := testCommand()
313 publication, err := protocol.BuildCommand(command.DeviceID, command.Capability, command.Metadata, command.Payload)
314 require.NoError(t, err)
315 require.NoError(t, router.Handle(t.Context(), publication.Topic, publication.Data, false))
316 require.ErrorIs(t, integration.Run(t.Context()), transport.failure)
320 func TestDriverPollKeepsIndependentFailureDuringCancellation(t *testing.T) {
321 failure := errors.New("independent gateway failure")
322 ctx, cancel := context.WithCancel(t.Context())
325 client := &cancelingGateway{
329 integration, _ := testDriver(t, client, &fakeTransport{})
330 err := integration.poll(ctx)
331 require.ErrorIs(t, err, context.Canceled)
332 require.ErrorIs(t, err, failure)
335 func publicationFailureName(refresh bool) string {
337 return "refresh failure returns to runner"
340 return "periodic failure returns to runner"