@homebridge-plugins/homebridge-august 3.4.1 → 3.4.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,642 +0,0 @@
1
- /* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
- *
3
- * platform.test.ts: homebridge-august platform tests.
4
- */
5
- import type { API, Logging, PlatformAccessory } from 'homebridge'
6
-
7
- import type { AugustPlatformConfig } from '../src/settings.js'
8
-
9
- import { readFileSync, writeFileSync } from 'node:fs'
10
-
11
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
12
-
13
- import { AugustPlatform } from '../src/Platform.HAP.js'
14
-
15
- // Mock the august-yale module
16
- vi.mock('august-yale', () => {
17
- // eslint-disable-next-line prefer-arrow-callback
18
- const MockAugust = vi.fn().mockImplementation(function () {
19
- return { end: vi.fn() }
20
- })
21
-
22
- // Type assertion to add static methods
23
- const MockConstructor = MockAugust as any
24
- MockConstructor.details = vi.fn()
25
- MockConstructor.authorize = vi.fn()
26
- MockConstructor.validate = vi.fn()
27
-
28
- return {
29
- default: MockConstructor,
30
- }
31
- })
32
-
33
- // Mock file system
34
- vi.mock('node:fs', () => ({
35
- readFileSync: vi.fn(),
36
- writeFileSync: vi.fn(),
37
- }))
38
-
39
- describe('augustPlatform', () => {
40
- let platform: AugustPlatform
41
- let mockApi: API
42
- let mockLog: Logging
43
- let mockConfig: AugustPlatformConfig
44
-
45
- beforeEach(() => {
46
- // Reset all mocks
47
- vi.clearAllMocks()
48
-
49
- // Create mock API
50
- mockApi = {
51
- hap: {
52
- uuid: {
53
- generate: vi.fn(() => 'mock-uuid'),
54
- },
55
- },
56
- user: {
57
- configPath: vi.fn(() => '/mock/config.json'),
58
- },
59
- on: vi.fn(),
60
- registerPlatformAccessories: vi.fn(),
61
- updatePlatformAccessories: vi.fn(),
62
- unregisterPlatformAccessories: vi.fn(),
63
- publishExternalAccessories: vi.fn(),
64
- } as unknown as API
65
-
66
- // Create mock logger
67
- mockLog = {
68
- info: vi.fn(),
69
- warn: vi.fn(),
70
- error: vi.fn(),
71
- debug: vi.fn(),
72
- success: vi.fn(),
73
- } as any
74
-
75
- // Create mock config
76
- mockConfig = {
77
- platform: 'August',
78
- name: 'Test August',
79
- credentials: {
80
- augustId: 'test@example.com',
81
- password: 'testpassword',
82
- countryCode: 'US',
83
- installId: 'test-install-id',
84
- isValidated: true,
85
- },
86
- options: {
87
- logging: 'debug',
88
- },
89
- }
90
-
91
- // Mock readFileSync for config file
92
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
93
- platforms: [mockConfig],
94
- }))
95
- })
96
-
97
- it('should normalize Canadian country code to US for API compatibility', async () => {
98
- const config: AugustPlatformConfig = {
99
- platform: 'August',
100
- name: 'Test August',
101
- credentials: {
102
- installId: 'test-install-id',
103
- augustId: 'test@example.com',
104
- password: 'test-password',
105
- countryCode: 'CA',
106
- isValidated: true,
107
- },
108
- options: {
109
- logging: 'debug',
110
- },
111
- }
112
-
113
- platform = new AugustPlatform(mockLog, config, mockApi)
114
-
115
- // Access the private method for testing
116
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
117
- const normalizedCredentials = await normalizeMethod(config.credentials)
118
-
119
- expect(normalizedCredentials.countryCode).toBe('US')
120
- expect(normalizedCredentials.augustId).toBe('test@example.com')
121
- expect(normalizedCredentials.password).toBe('test-password')
122
- })
123
-
124
- it('should normalize Mexican country code to US for API compatibility', async () => {
125
- const config: AugustPlatformConfig = {
126
- platform: 'August',
127
- name: 'Test August',
128
- credentials: {
129
- installId: 'test-install-id',
130
- augustId: 'test@example.com',
131
- password: 'test-password',
132
- countryCode: 'MX',
133
- isValidated: true,
134
- },
135
- options: {
136
- logging: 'debug',
137
- },
138
- }
139
-
140
- platform = new AugustPlatform(mockLog, config, mockApi)
141
-
142
- // Access the private method for testing
143
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
144
- const normalizedCredentials = await normalizeMethod(config.credentials)
145
-
146
- expect(normalizedCredentials.countryCode).toBe('US')
147
- expect(normalizedCredentials.augustId).toBe('test@example.com')
148
- expect(normalizedCredentials.password).toBe('test-password')
149
- })
150
-
151
- it('should respect disableCountryCodeNormalization option', async () => {
152
- const config: AugustPlatformConfig = {
153
- platform: 'August',
154
- name: 'Test August',
155
- credentials: {
156
- installId: 'test-install-id',
157
- augustId: 'test@example.com',
158
- password: 'test-password',
159
- countryCode: 'CA',
160
- isValidated: true,
161
- },
162
- options: {
163
- logging: 'debug',
164
- disableCountryCodeNormalization: true,
165
- },
166
- }
167
-
168
- platform = new AugustPlatform(mockLog, config, mockApi)
169
-
170
- // Access the private method for testing
171
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
172
- const normalizedCredentials = await normalizeMethod(config.credentials)
173
-
174
- expect(normalizedCredentials.countryCode).toBe('CA') // Should remain unchanged
175
- expect(normalizedCredentials.augustId).toBe('test@example.com')
176
- expect(normalizedCredentials.password).toBe('test-password')
177
- })
178
-
179
- it('should handle case-insensitive country codes', async () => {
180
- const config: AugustPlatformConfig = {
181
- platform: 'August',
182
- name: 'Test August',
183
- credentials: {
184
- installId: 'test-install-id',
185
- augustId: 'test@example.com',
186
- password: 'test-password',
187
- countryCode: 'ca', // lowercase
188
- isValidated: true,
189
- },
190
- options: {
191
- logging: 'debug',
192
- },
193
- }
194
-
195
- platform = new AugustPlatform(mockLog, config, mockApi)
196
-
197
- // Access the private method for testing
198
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
199
- const normalizedCredentials = await normalizeMethod(config.credentials)
200
-
201
- expect(normalizedCredentials.countryCode).toBe('US')
202
- })
203
-
204
- it('should not change US country code', async () => {
205
- const config: AugustPlatformConfig = {
206
- platform: 'August',
207
- name: 'Test August',
208
- credentials: {
209
- installId: 'test-install-id',
210
- augustId: 'test@example.com',
211
- password: 'test-password',
212
- countryCode: 'US',
213
- isValidated: true,
214
- },
215
- options: {
216
- logging: 'debug',
217
- },
218
- }
219
-
220
- platform = new AugustPlatform(mockLog, config, mockApi)
221
-
222
- // Access the private method for testing
223
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
224
- const normalizedCredentials = await normalizeMethod(config.credentials)
225
-
226
- expect(normalizedCredentials.countryCode).toBe('US')
227
- })
228
-
229
- it('should preserve other credential properties when normalizing', async () => {
230
- const config: AugustPlatformConfig = {
231
- platform: 'August',
232
- name: 'Test August',
233
- credentials: {
234
- apiKey: 'test-api-key',
235
- pnSubKey: 'test-pn-sub-key',
236
- installId: 'test-install-id',
237
- augustId: 'test@example.com',
238
- password: 'test-password',
239
- countryCode: 'CA',
240
- isValidated: true,
241
- validateCode: 'test-code',
242
- },
243
- options: {
244
- logging: 'debug',
245
- },
246
- }
247
-
248
- platform = new AugustPlatform(mockLog, config, mockApi)
249
-
250
- // Access the private method for testing
251
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
252
- const normalizedCredentials = await normalizeMethod(config.credentials)
253
-
254
- expect(normalizedCredentials.apiKey).toBe('test-api-key')
255
- expect(normalizedCredentials.pnSubKey).toBe('test-pn-sub-key')
256
- expect(normalizedCredentials.installId).toBe('test-install-id')
257
- expect(normalizedCredentials.augustId).toBe('test@example.com')
258
- expect(normalizedCredentials.password).toBe('test-password')
259
- expect(normalizedCredentials.countryCode).toBe('US') // Should be normalized
260
- expect(normalizedCredentials.isValidated).toBe(true)
261
- expect(normalizedCredentials.validateCode).toBe('test-code')
262
- })
263
-
264
- it('should provide public method to get normalized credentials', async () => {
265
- const config: AugustPlatformConfig = {
266
- platform: 'August',
267
- name: 'Test August',
268
- credentials: {
269
- installId: 'test-install-id',
270
- augustId: 'test@example.com',
271
- password: 'test-password',
272
- countryCode: 'CA',
273
- isValidated: true,
274
- },
275
- options: {
276
- logging: 'debug',
277
- },
278
- }
279
-
280
- platform = new AugustPlatform(mockLog, config, mockApi)
281
-
282
- // Test the public method
283
- const normalizedCredentials = await platform.getNormalizedCredentials()
284
-
285
- expect(normalizedCredentials.countryCode).toBe('US')
286
- expect(normalizedCredentials.augustId).toBe('test@example.com')
287
- })
288
-
289
- it('should handle null credentials gracefully', async () => {
290
- const config: AugustPlatformConfig = {
291
- platform: 'August',
292
- name: 'Test August',
293
- credentials: {
294
- installId: 'test-install-id',
295
- augustId: 'test@example.com',
296
- password: 'test-password',
297
- countryCode: 'CA',
298
- isValidated: true,
299
- },
300
- options: {
301
- logging: 'debug',
302
- },
303
- }
304
-
305
- platform = new AugustPlatform(mockLog, config, mockApi)
306
-
307
- // Access the private method for testing
308
- const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
309
-
310
- await expect(normalizeMethod(null)).rejects.toThrow('Credentials cannot be null or undefined')
311
- await expect(normalizeMethod(undefined)).rejects.toThrow('Credentials cannot be null or undefined')
312
- })
313
-
314
- it('should use cached credentials for performance', async () => {
315
- const config: AugustPlatformConfig = {
316
- platform: 'August',
317
- name: 'Test August',
318
- credentials: {
319
- installId: 'test-install-id',
320
- augustId: 'test@example.com',
321
- password: 'test-password',
322
- countryCode: 'CA',
323
- isValidated: true,
324
- },
325
- options: {
326
- logging: 'debug',
327
- },
328
- }
329
-
330
- platform = new AugustPlatform(mockLog, config, mockApi)
331
-
332
- // First call should normalize and cache
333
- const firstCall = await platform.getNormalizedCredentials()
334
- expect(firstCall.countryCode).toBe('US')
335
-
336
- // Second call should use cache
337
- const secondCall = await platform.getNormalizedCredentials()
338
- expect(secondCall.countryCode).toBe('US')
339
- expect(secondCall).toBe(firstCall) // Should be the same object reference
340
- })
341
-
342
- afterEach(() => {
343
- vi.restoreAllMocks()
344
- })
345
-
346
- describe('discoverDevices', () => {
347
- it('should handle 401 authentication errors and trigger re-authentication', async () => {
348
- // Create platform instance
349
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
350
-
351
- // Mock August.details to throw 401 error
352
- const August = await import('august-yale')
353
- const detailsMock = vi.fn().mockRejectedValueOnce(new Error('GET failed with: 401'))
354
- August.default.details = detailsMock
355
-
356
- // Mock validated method to avoid actual re-authentication
357
- const validateSpy = vi.spyOn(platform, 'validated').mockImplementation(async () => {
358
- // Don't change the flag back to true, just simulate the method being called
359
-
360
- })
361
-
362
- // Call discoverDevices
363
- await expect(platform.discoverDevices()).resolves.toBeUndefined()
364
-
365
- // Verify that validation was reset and re-authentication was attempted
366
- expect(platform.config.credentials!.isValidated).toBe(false)
367
- expect(writeFileSync).toHaveBeenCalled()
368
- expect(validateSpy).toHaveBeenCalled()
369
- })
370
-
371
- it('should handle "unauthorized" errors and trigger re-authentication', async () => {
372
- // Create platform instance
373
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
374
-
375
- // Mock August.details to throw unauthorized error
376
- const August = await import('august-yale')
377
- const detailsMock = vi.fn().mockRejectedValueOnce(new Error('Request failed: Unauthorized'))
378
- August.default.details = detailsMock
379
-
380
- // Mock validated method
381
- const validateSpy = vi.spyOn(platform, 'validated').mockImplementation(async () => {
382
-
383
- })
384
-
385
- // Call discoverDevices
386
- await expect(platform.discoverDevices()).resolves.toBeUndefined()
387
-
388
- // Verify re-authentication was triggered
389
- expect(validateSpy).toHaveBeenCalled()
390
- })
391
-
392
- it('should re-throw non-authentication errors', async () => {
393
- // Create platform instance
394
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
395
-
396
- // Mock August.details to throw non-401 error
397
- const August = await import('august-yale')
398
- const networkError = new Error('Network timeout')
399
- const detailsMock = vi.fn().mockRejectedValueOnce(networkError)
400
- August.default.details = detailsMock
401
-
402
- // Call discoverDevices and expect it to throw
403
- await expect(platform.discoverDevices()).rejects.toThrow('Failed to discover devices: Network timeout')
404
- })
405
-
406
- it('should process devices successfully when no errors occur', async () => {
407
- // Create platform instance
408
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
409
-
410
- // Mock successful August.details response
411
- const August = await import('august-yale')
412
- const mockDevice = {
413
- lockId: 'test-lock-id',
414
- LockName: 'Test Lock',
415
- SerialNumber: '123456',
416
- } as any // Use any to avoid complex type requirements
417
-
418
- const detailsMock = vi.fn().mockResolvedValueOnce([mockDevice])
419
- August.default.details = detailsMock
420
-
421
- // Mock the Lock method to avoid actual device processing
422
- const lockSpy = vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
423
-
424
- // Call discoverDevices
425
- await expect(platform.discoverDevices()).resolves.toBeUndefined()
426
-
427
- // Verify successful processing
428
- expect(detailsMock).toHaveBeenCalled()
429
- // Due to the existing logic, single device arrays get wrapped: [devices] becomes [[device]]
430
- // So Lock gets called with [device] instead of device
431
- expect(lockSpy).toHaveBeenCalledWith([mockDevice])
432
- })
433
-
434
- it('should exclude locks matching excludeLockIds', async () => {
435
- const configWithExcludes: AugustPlatformConfig = {
436
- ...mockConfig,
437
- options: {
438
- logging: 'debug',
439
- excludeLockIds: ['excluded-lock-id'],
440
- },
441
- }
442
-
443
- platform = new AugustPlatform(mockLog, configWithExcludes, mockApi)
444
-
445
- const August = await import('august-yale')
446
- const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
447
- const mockDeviceExcluded = { lockId: 'excluded-lock-id', LockName: 'Excluded Lock' } as any
448
- August.default.details = vi.fn().mockResolvedValueOnce([mockDeviceIncluded, mockDeviceExcluded])
449
-
450
- const lockSpy = vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
451
-
452
- await platform.discoverDevices()
453
-
454
- // Only the non-excluded lock should be processed
455
- expect(lockSpy).toHaveBeenCalledTimes(1)
456
- expect(lockSpy).toHaveBeenCalledWith(expect.objectContaining({ lockId: 'included-lock-id' }))
457
- })
458
-
459
- it('should handle case-insensitive excludeLockIds matching', async () => {
460
- const configWithExcludes: AugustPlatformConfig = {
461
- ...mockConfig,
462
- options: {
463
- logging: 'debug',
464
- excludeLockIds: ['EXCLUDED-LOCK-ID'],
465
- },
466
- }
467
-
468
- platform = new AugustPlatform(mockLog, configWithExcludes, mockApi)
469
-
470
- const August = await import('august-yale')
471
- const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
472
- const mockDeviceExcluded = { lockId: 'excluded-lock-id', LockName: 'Excluded Lock' } as any
473
- August.default.details = vi.fn().mockResolvedValueOnce([mockDeviceIncluded, mockDeviceExcluded])
474
-
475
- const lockSpy = vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
476
-
477
- await platform.discoverDevices()
478
-
479
- expect(lockSpy).toHaveBeenCalledTimes(1)
480
- expect(lockSpy).toHaveBeenCalledWith(expect.objectContaining({ lockId: 'included-lock-id' }))
481
- })
482
-
483
- it('should unregister cached accessories for excluded lock IDs', async () => {
484
- const configWithExcludes: AugustPlatformConfig = {
485
- ...mockConfig,
486
- options: {
487
- logging: 'debug',
488
- excludeLockIds: ['excluded-lock-id'],
489
- },
490
- }
491
-
492
- // Make uuid.generate return predictable UUIDs based on input
493
- vi.mocked(mockApi.hap.uuid.generate).mockImplementation((input: any) => `uuid-${input}`)
494
-
495
- platform = new AugustPlatform(mockLog, configWithExcludes, mockApi)
496
-
497
- // Simulate a cached accessory for the excluded lock
498
- const mockAccessory = {
499
- UUID: 'uuid-excluded-lock-id',
500
- displayName: 'Excluded Lock',
501
- } as unknown as PlatformAccessory
502
- platform.accessories.push(mockAccessory)
503
-
504
- const August = await import('august-yale')
505
- const mockDevice = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
506
- August.default.details = vi.fn().mockResolvedValueOnce([mockDevice])
507
-
508
- vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
509
-
510
- await platform.discoverDevices()
511
-
512
- // Verify the cached accessory was unregistered
513
- expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(
514
- 'homebridge-august',
515
- 'August',
516
- [mockAccessory],
517
- )
518
- // Verify the accessory was removed from the accessories array
519
- expect(platform.accessories.find(a => a.UUID === 'uuid-excluded-lock-id')).toBeUndefined()
520
- })
521
-
522
- it('should not filter when excludeLockIds is empty', async () => {
523
- const configWithEmptyExcludes: AugustPlatformConfig = {
524
- ...mockConfig,
525
- options: {
526
- logging: 'debug',
527
- excludeLockIds: [],
528
- },
529
- }
530
-
531
- platform = new AugustPlatform(mockLog, configWithEmptyExcludes, mockApi)
532
-
533
- const August = await import('august-yale')
534
- const mockDeviceA = { lockId: 'lock-a', LockName: 'Lock A' } as any
535
- const mockDeviceB = { lockId: 'lock-b', LockName: 'Lock B' } as any
536
- August.default.details = vi.fn().mockResolvedValueOnce([mockDeviceA, mockDeviceB])
537
-
538
- const lockSpy = vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
539
-
540
- await platform.discoverDevices()
541
-
542
- expect(lockSpy).toHaveBeenCalledTimes(2)
543
- })
544
-
545
- it('should exclude locks when using per-device config', async () => {
546
- const configWithBoth: AugustPlatformConfig = {
547
- ...mockConfig,
548
- options: {
549
- logging: 'debug',
550
- excludeLockIds: ['excluded-lock-id'],
551
- devices: [
552
- { lockId: 'included-lock-id', configLockName: 'My Lock', overrideHomeKitEnabled: false } as any,
553
- ],
554
- },
555
- }
556
-
557
- platform = new AugustPlatform(mockLog, configWithBoth, mockApi)
558
-
559
- const August = await import('august-yale')
560
- const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
561
- const mockDeviceExcluded = { lockId: 'excluded-lock-id', LockName: 'Excluded Lock' } as any
562
- August.default.details = vi.fn().mockResolvedValueOnce([mockDeviceIncluded, mockDeviceExcluded])
563
-
564
- const lockSpy = vi.spyOn(platform as any, 'Lock').mockImplementation(async () => {})
565
-
566
- await platform.discoverDevices()
567
-
568
- // Only the included lock should reach the Lock method
569
- expect(lockSpy).toHaveBeenCalledTimes(1)
570
- })
571
- })
572
-
573
- describe('validated method', () => {
574
- it('should handle undefined credentials in config file gracefully', async () => {
575
- // Mock config file with undefined credentials
576
- const mockConfigFile = {
577
- platforms: [
578
- {
579
- platform: 'August',
580
- name: 'Test August',
581
- // credentials is undefined here
582
- },
583
- ],
584
- }
585
-
586
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
587
-
588
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
589
-
590
- // This should not throw an error when trying to set installId
591
- await expect(platform.validated()).resolves.toBeUndefined()
592
- })
593
-
594
- it('should handle null credentials in config file gracefully', async () => {
595
- // Mock config file with null credentials
596
- const mockConfigFile = {
597
- platforms: [
598
- {
599
- platform: 'August',
600
- name: 'Test August',
601
- credentials: null,
602
- },
603
- ],
604
- }
605
-
606
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
607
-
608
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
609
-
610
- // This should not throw an error when trying to set installId
611
- await expect(platform.validated()).resolves.toBeUndefined()
612
- })
613
-
614
- it('should initialize credentials object and save installId when credentials are missing', async () => {
615
- // Mock config file with undefined credentials
616
- const mockConfigFile = {
617
- platforms: [
618
- {
619
- platform: 'August',
620
- name: 'Test August',
621
- // credentials is undefined here
622
- },
623
- ],
624
- }
625
-
626
- vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
627
-
628
- platform = new AugustPlatform(mockLog, mockConfig, mockApi)
629
-
630
- await platform.validated()
631
-
632
- // Verify that writeFileSync was called (meaning the config was updated)
633
- expect(writeFileSync).toHaveBeenCalled()
634
-
635
- // Verify the credentials object was created and populated
636
- const writeCall = vi.mocked(writeFileSync).mock.calls[0]
637
- const savedConfig = JSON.parse(writeCall[1] as string)
638
- expect(savedConfig.platforms[0].credentials).toBeDefined()
639
- expect(savedConfig.platforms[0].credentials.installId).toBeDefined()
640
- })
641
- })
642
- })