yoto-nodejs-client 0.0.4 → 0.0.6

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.
Files changed (45) hide show
  1. package/README.md +24 -6
  2. package/bin/auth.js +4 -3
  3. package/bin/device-model.js +25 -5
  4. package/bin/device-tui.js +25 -3
  5. package/bin/devices.js +58 -42
  6. package/bin/lib/cli-helpers.d.ts.map +1 -1
  7. package/bin/lib/cli-helpers.js +3 -1
  8. package/bin/lib/token-helpers.d.ts +4 -2
  9. package/bin/lib/token-helpers.d.ts.map +1 -1
  10. package/bin/lib/token-helpers.js +9 -8
  11. package/bin/refresh-token.js +4 -2
  12. package/bin/token-info.js +2 -2
  13. package/lib/api-client.d.ts +11 -10
  14. package/lib/api-client.d.ts.map +1 -1
  15. package/lib/api-client.js +12 -14
  16. package/lib/api-endpoints/auth.test.js +4 -4
  17. package/lib/api-endpoints/content.test.js +32 -32
  18. package/lib/api-endpoints/devices.d.ts +4 -4
  19. package/lib/api-endpoints/devices.js +2 -2
  20. package/lib/api-endpoints/devices.test.js +45 -45
  21. package/lib/api-endpoints/endpoint-test-helpers.d.ts +3 -4
  22. package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -1
  23. package/lib/api-endpoints/endpoint-test-helpers.js +21 -5
  24. package/lib/api-endpoints/family-library-groups.d.ts +3 -3
  25. package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
  26. package/lib/api-endpoints/family-library-groups.js +3 -3
  27. package/lib/api-endpoints/family-library-groups.test.js +29 -29
  28. package/lib/api-endpoints/family.test.js +11 -11
  29. package/lib/api-endpoints/icons.test.js +14 -14
  30. package/lib/mqtt/client.d.ts +124 -49
  31. package/lib/mqtt/client.d.ts.map +1 -1
  32. package/lib/mqtt/client.js +132 -50
  33. package/lib/mqtt/factory.d.ts +12 -5
  34. package/lib/mqtt/factory.d.ts.map +1 -1
  35. package/lib/mqtt/factory.js +39 -11
  36. package/lib/mqtt/index.js +2 -1
  37. package/lib/mqtt/mqtt.test.js +25 -22
  38. package/lib/token.d.ts +44 -2
  39. package/lib/token.d.ts.map +1 -1
  40. package/lib/token.js +142 -2
  41. package/lib/yoto-device.d.ts +392 -32
  42. package/lib/yoto-device.d.ts.map +1 -1
  43. package/lib/yoto-device.js +643 -99
  44. package/lib/yoto-device.test.js +193 -0
  45. package/package.json +1 -1
package/lib/api-client.js CHANGED
@@ -7,9 +7,9 @@
7
7
  * @import { YotoAudioUploadUrlResponse, YotoUploadCoverImageResponse, YotoCoverType } from './api-endpoints/media.js'
8
8
  * @import { YotoTokenResponse, YotoDeviceCodeResponse, YotoDevicePollResult } from './api-endpoints/auth.js'
9
9
  * @import { YotoMqttClient } from './mqtt/client.js'
10
- * @import { MqttClientOptions, YotoMqttOptions } from './mqtt/factory.js'
10
+ * @import { YotoMqttOptions } from './mqtt/factory.js'
11
11
  * @import { RequestOptions } from './api-endpoints/helpers.js'
12
- * @import { RefreshSuccessEvent } from './token.js'
12
+ * @import { OnTokenRefreshHandler } from './token.js'
13
13
  */
14
14
 
15
15
  import { RefreshableToken } from './token.js'
@@ -27,11 +27,12 @@ import { createYotoMqttClient } from './mqtt/index.js'
27
27
  * @property {string} clientId - OAuth client ID
28
28
  * @property {string} refreshToken - OAuth refresh token
29
29
  * @property {string} accessToken - Initial OAuth access token (JWT)
30
- * @property {(refreshedTokenData: RefreshSuccessEvent) => void | Promise<void>} onTokenRefresh - **REQUIRED** Callback invoked when tokens are refreshed. You MUST persist these tokens (to file, database, etc.) as the refresh can happen at any time during API calls. The refresh token may be rotated by the auth server. **DO NOT STUB THIS CALLBACK** - always implement proper persistence logic.
30
+ * @property {OnTokenRefreshHandler} onTokenRefresh - **REQUIRED** Callback invoked when tokens are refreshed. You MUST persist these tokens (to file, database, etc.) as the refresh can happen at any time during API calls. The refresh token may be rotated by the auth server. **DO NOT STUB THIS CALLBACK** - always implement proper persistence logic.
31
31
  * @property {number} [bufferSeconds=30] - Seconds before expiration to consider token expired
32
32
  * @property {() => void | Promise<void>} [onRefreshStart] - Optional callback invoked when token refresh starts. Defaults to console.log.
33
33
  * @property {(error: Error) => void | Promise<void>} [onRefreshError] - Optional callback invoked when token refresh fails with a transient error. Defaults to console.warn.
34
34
  * @property {(error: Error) => void | Promise<void>} [onInvalid] - Optional callback invoked when refresh token is permanently invalid. Defaults to console.error.
35
+ * @property {string} [mqttSessionId] - Stable unique client ID suffix used for MQTT connections (defaults to a random UUID per YotoClient instance)
35
36
  * @property {string} [userAgent] - Optional user agent string to identify your application
36
37
  * @property {RequestOptions} [defaultRequestOptions] - Default undici request options for all requests (dispatcher, timeouts, etc.)
37
38
  */
@@ -187,15 +188,13 @@ export class YotoClient {
187
188
  clientId,
188
189
  refreshToken,
189
190
  accessToken,
190
- ...(bufferSeconds !== undefined && { bufferSeconds })
191
+ ...(bufferSeconds !== undefined && { bufferSeconds }),
192
+ onTokenRefresh
191
193
  })
192
194
 
193
195
  this.#userAgent = userAgent
194
196
  this.#defaultRequestOptions = defaultRequestOptions
195
197
 
196
- // Listen for token refresh events and call the user's callbacks
197
- this.#token.on('refresh:success', onTokenRefresh)
198
-
199
198
  this.#token.on('refresh:start', onRefreshStart || (() => {
200
199
  console.log('Token refresh started')
201
200
  }))
@@ -216,6 +215,7 @@ export class YotoClient {
216
215
  get token () {
217
216
  return this.#token
218
217
  }
218
+
219
219
  // ============================================================================
220
220
  // Content API
221
221
  // ============================================================================
@@ -438,7 +438,7 @@ export class YotoClient {
438
438
  async createGroup ({ group, requestOptions }) {
439
439
  const accessToken = await this.#token.getAccessToken()
440
440
  return await FamilyLibraryGroups.createGroup({
441
- token: accessToken,
441
+ accessToken,
442
442
  userAgent: this.#userAgent,
443
443
  requestOptions: requestOptions || this.#defaultRequestOptions,
444
444
  group
@@ -661,19 +661,17 @@ export class YotoClient {
661
661
 
662
662
  /**
663
663
  * Create an MQTT client for a device
664
- * @param {Object} params
665
- * @param {string} params.deviceId - Device ID to connect to
666
- * @param {MqttClientOptions} [params.mqttOptions] - MQTT.js client options (excluding deviceId and accessToken which are provided automatically)
664
+ * @param {Omit<YotoMqttOptions, 'token'>} YotoMqttOptions
667
665
  * @returns {Promise<YotoMqttClient>}
668
666
  */
669
667
  async createMqttClient ({ deviceId, mqttOptions }) {
670
- const accessToken = await this.#token.getAccessToken()
668
+ await this.#token.getAccessToken()
671
669
 
672
670
  /** @type {YotoMqttOptions} */
673
671
  const opts = {
674
672
  deviceId,
675
- accessToken,
676
- ...mqttOptions
673
+ token: this.#token,
674
+ ...(mqttOptions && { mqttOptions })
677
675
  }
678
676
 
679
677
  return createYotoMqttClient(opts)
@@ -4,7 +4,7 @@ import { exchangeToken, pollForDeviceToken } from './auth.js'
4
4
  import { YotoAPIError } from './helpers.js'
5
5
  import { loadTestTokens } from './endpoint-test-helpers.js'
6
6
 
7
- const { clientId } = loadTestTokens()
7
+ const { token } = loadTestTokens()
8
8
 
9
9
  test('exchangeToken - refresh flow', async (t) => {
10
10
  await t.test('should fail with invalid refresh token', async () => {
@@ -13,7 +13,7 @@ test('exchangeToken - refresh flow', async (t) => {
13
13
  await exchangeToken({
14
14
  grantType: 'refresh_token',
15
15
  refreshToken: 'invalid-refresh-token',
16
- clientId
16
+ clientId: token.clientId
17
17
  })
18
18
  },
19
19
  (err) => {
@@ -33,7 +33,7 @@ test('pollForDeviceToken', async (t) => {
33
33
  async () => {
34
34
  await pollForDeviceToken({
35
35
  deviceCode: 'invalid-device-code',
36
- clientId,
36
+ clientId: token.clientId,
37
37
  currentInterval: 5000
38
38
  })
39
39
  },
@@ -54,7 +54,7 @@ test('pollForDeviceToken', async (t) => {
54
54
  async () => {
55
55
  await pollForDeviceToken({
56
56
  deviceCode: 'expired-device-code-xyz',
57
- clientId,
57
+ clientId: token.clientId,
58
58
  currentInterval: 5000
59
59
  })
60
60
  },
@@ -4,12 +4,12 @@ import { getContent, getUserMyoContent } from './content.js'
4
4
  import { YotoAPIError } from './helpers.js'
5
5
  import { loadTestTokens, logResponse } from './endpoint-test-helpers.js'
6
6
 
7
- const { accessToken } = loadTestTokens()
7
+ const { token } = loadTestTokens()
8
8
 
9
9
  test('getUserMyoContent', async (t) => {
10
10
  await t.test('should fetch user MYO cards', async () => {
11
11
  const response = await getUserMyoContent({
12
- accessToken
12
+ accessToken: await token.getAccessToken()
13
13
  })
14
14
 
15
15
  // Log response for type verification and documentation
@@ -23,15 +23,15 @@ test('getUserMyoContent', async (t) => {
23
23
  // Validate card structure
24
24
  const card = response.cards[0]
25
25
  assert.ok(card, 'Card should exist')
26
- assert.ok(typeof card.cardId === 'string', 'Card should have cardId string')
27
- assert.ok(typeof card.title === 'string', 'Card should have title string')
28
- assert.ok(typeof card.createdAt === 'string', 'Card should have createdAt string')
29
- assert.ok(typeof card.updatedAt === 'string', 'Card should have updatedAt string')
30
- assert.ok(typeof card.userId === 'string', 'Card should have userId string')
26
+ assert.equal(typeof card.cardId, 'string', 'Card should have cardId string')
27
+ assert.equal(typeof card.title, 'string', 'Card should have title string')
28
+ assert.equal(typeof card.createdAt, 'string', 'Card should have createdAt string')
29
+ assert.equal(typeof card.updatedAt, 'string', 'Card should have updatedAt string')
30
+ assert.equal(typeof card.userId, 'string', 'Card should have userId string')
31
31
  assert.ok(card.content, 'Card should have content object')
32
32
  assert.ok(card.metadata, 'Card should have metadata object')
33
33
  assert.ok(card.metadata.media, 'Metadata should have media object')
34
- assert.ok(typeof card.metadata.media.duration === 'number', 'Media should have duration number')
34
+ assert.equal(typeof card.metadata.media.duration, 'number', 'Media should have duration number')
35
35
 
36
36
  // Validate metadata category enum if present and non-empty
37
37
  if (card.metadata.category && card.metadata.category.length > 0) {
@@ -51,7 +51,7 @@ test('getUserMyoContent', async (t) => {
51
51
  if (card.metadata.status) {
52
52
  const validStatuses = ['new', 'inprogress', 'complete', 'live', 'archived']
53
53
  assert.ok(validStatuses.includes(card.metadata.status.name), `Status name should be valid: ${card.metadata.status.name}`)
54
- assert.ok(typeof card.metadata.status.updatedAt === 'string', 'Status should have updatedAt string')
54
+ assert.equal(typeof card.metadata.status.updatedAt, 'string', 'Status should have updatedAt string')
55
55
  }
56
56
 
57
57
  if (card.metadata.playbackDirection) {
@@ -61,7 +61,7 @@ test('getUserMyoContent', async (t) => {
61
61
 
62
62
  await t.test('should accept showDeleted parameter', async () => {
63
63
  const response = await getUserMyoContent({
64
- accessToken,
64
+ accessToken: await token.getAccessToken(),
65
65
  showDeleted: true
66
66
  })
67
67
 
@@ -93,7 +93,7 @@ test('getContent', async (t) => {
93
93
  // Get real card IDs to test with
94
94
  await t.test('setup - get card IDs from user content', async () => {
95
95
  const response = await getUserMyoContent({
96
- accessToken
96
+ accessToken: await token.getAccessToken()
97
97
  })
98
98
 
99
99
  assert.ok(response.cards.length > 0, 'User should have at least one card for getContent tests')
@@ -106,7 +106,7 @@ test('getContent', async (t) => {
106
106
  // Test with multiple cards to ensure consistency
107
107
  for (const cardId of testCardIds) {
108
108
  const content = await getContent({
109
- accessToken,
109
+ accessToken: await token.getAccessToken(),
110
110
  cardId
111
111
  })
112
112
 
@@ -117,9 +117,9 @@ test('getContent', async (t) => {
117
117
  assert.ok(content, 'Response should exist')
118
118
  assert.ok(content.card, 'Response should have card property')
119
119
  assert.strictEqual(content.card.cardId, cardId, 'Returned card ID should match requested ID')
120
- assert.ok(typeof content.card.title === 'string', 'Card should have title')
121
- assert.ok(typeof content.card.createdAt === 'string', 'Card should have createdAt')
122
- assert.ok(typeof content.card.updatedAt === 'string', 'Card should have updatedAt')
120
+ assert.equal(typeof content.card.title, 'string', 'Card should have title')
121
+ assert.equal(typeof content.card.createdAt, 'string', 'Card should have createdAt')
122
+ assert.equal(typeof content.card.updatedAt, 'string', 'Card should have updatedAt')
123
123
  assert.ok(content.card.content, 'Card should have content property')
124
124
  assert.ok(Array.isArray(content.card.content.chapters), 'Content should have chapters array')
125
125
  assert.ok(content.card.metadata, 'Card should have metadata')
@@ -143,7 +143,7 @@ test('getContent', async (t) => {
143
143
  assert.ok(cardId, 'Card ID should exist')
144
144
 
145
145
  const content = await getContent({
146
- accessToken,
146
+ accessToken: await token.getAccessToken(),
147
147
  cardId
148
148
  })
149
149
 
@@ -156,14 +156,14 @@ test('getContent', async (t) => {
156
156
  assert.ok(chapter, 'Chapter should exist')
157
157
 
158
158
  // Validate chapter structure
159
- assert.ok(typeof chapter.key === 'string', 'Chapter should have key string')
160
- assert.ok(typeof chapter.title === 'string', 'Chapter should have title string')
161
- assert.ok(typeof chapter.overlayLabel === 'string', 'Chapter should have overlayLabel string')
159
+ assert.equal(typeof chapter.key, 'string', 'Chapter should have key string')
160
+ assert.equal(typeof chapter.title, 'string', 'Chapter should have title string')
161
+ assert.equal(typeof chapter.overlayLabel, 'string', 'Chapter should have overlayLabel string')
162
162
  assert.ok(Array.isArray(chapter.tracks), 'Chapter should have tracks array')
163
163
  assert.ok(chapter.display, 'Chapter should have display object')
164
- assert.ok(typeof chapter.display.icon16x16 === 'string', 'Chapter display should have icon16x16 string')
165
- assert.ok(typeof chapter.duration === 'number', 'Chapter should have duration number')
166
- assert.ok(typeof chapter.fileSize === 'number', 'Chapter should have fileSize number')
164
+ assert.equal(typeof chapter.display.icon16x16, 'string', 'Chapter display should have icon16x16 string')
165
+ assert.equal(typeof chapter.duration, 'number', 'Chapter should have duration number')
166
+ assert.equal(typeof chapter.fileSize, 'number', 'Chapter should have fileSize number')
167
167
  assert.ok('availableFrom' in chapter, 'Chapter should have availableFrom property')
168
168
  assert.ok('ambient' in chapter, 'Chapter should have ambient property')
169
169
  assert.ok('defaultTrackDisplay' in chapter, 'Chapter should have defaultTrackDisplay property')
@@ -175,24 +175,24 @@ test('getContent', async (t) => {
175
175
  assert.ok(track, 'Track should exist')
176
176
 
177
177
  // Validate track structure
178
- assert.ok(typeof track.key === 'string', 'Track should have key string')
179
- assert.ok(typeof track.title === 'string', 'Track should have title string')
180
- assert.ok(typeof track.trackUrl === 'string', 'Track should have trackUrl string')
178
+ assert.equal(typeof track.key, 'string', 'Track should have key string')
179
+ assert.equal(typeof track.title, 'string', 'Track should have title string')
180
+ assert.equal(typeof track.trackUrl, 'string', 'Track should have trackUrl string')
181
181
  assert.ok(track.trackUrl.startsWith('yoto:#'), 'Track URL should start with "yoto:#"')
182
- assert.ok(typeof track.format === 'string', 'Track should have format string')
182
+ assert.equal(typeof track.format, 'string', 'Track should have format string')
183
183
 
184
184
  // Validate audio format enum
185
185
  const validFormats = ['mp3', 'aac', 'opus', 'ogg']
186
186
  assert.ok(validFormats.includes(track.format), `Track format should be valid: ${track.format}`)
187
187
 
188
188
  assert.ok(track.type === 'audio' || track.type === 'stream', 'Track type should be "audio" or "stream"')
189
- assert.ok(typeof track.overlayLabel === 'string', 'Track should have overlayLabel string')
190
- assert.ok(typeof track.duration === 'number', 'Track should have duration number')
191
- assert.ok(typeof track.fileSize === 'number', 'Track should have fileSize number')
189
+ assert.equal(typeof track.overlayLabel, 'string', 'Track should have overlayLabel string')
190
+ assert.equal(typeof track.duration, 'number', 'Track should have duration number')
191
+ assert.equal(typeof track.fileSize, 'number', 'Track should have fileSize number')
192
192
  assert.ok(track.channels === 'stereo' || track.channels === 'mono', 'Track channels should be "stereo" or "mono"')
193
193
  assert.ok('ambient' in track, 'Track should have ambient property')
194
194
  assert.ok(track.display, 'Track should have display object')
195
- assert.ok(typeof track.display.icon16x16 === 'string', 'Track display should have icon16x16 string')
195
+ assert.equal(typeof track.display.icon16x16, 'string', 'Track display should have icon16x16 string')
196
196
  assert.ok(track.display.icon16x16.startsWith('yoto:#'), 'Track display icon should start with "yoto:#"')
197
197
  }
198
198
  }
@@ -203,7 +203,7 @@ test('getContent', async (t) => {
203
203
  assert.ok(cardId, 'Card ID should exist')
204
204
 
205
205
  const content = await getContent({
206
- accessToken,
206
+ accessToken: await token.getAccessToken(),
207
207
  cardId,
208
208
  timezone: 'Pacific/Auckland'
209
209
  })
@@ -220,7 +220,7 @@ test('getContent', async (t) => {
220
220
  assert.ok(cardId, 'Card ID should exist')
221
221
 
222
222
  const content = await getContent({
223
- accessToken,
223
+ accessToken: await token.getAccessToken(),
224
224
  cardId,
225
225
  playable: true,
226
226
  signingType: 's3'
@@ -185,7 +185,7 @@ export function getDeviceStatus({ accessToken, userAgent, deviceId, requestOptio
185
185
  * @property {string} bluetoothEnabled - Bluetooth enabled state ('0' or '1')
186
186
  * @property {boolean} btHeadphonesEnabled - Bluetooth headphones enabled
187
187
  * @property {string} clockFace - Clock face style (e.g., 'digital-sun')
188
- * @property {string} dayDisplayBrightness - Day display brightness (e.g., 'auto', '100')
188
+ * @property {string} dayDisplayBrightness - Day display brightness (e.g., 'auto', '0' - '100')
189
189
  * @property {string} dayTime - Day mode start time (e.g., '07:00')
190
190
  * @property {string} dayYotoDaily - Day mode Yoto Daily card path
191
191
  * @property {string} dayYotoRadio - Day mode Yoto Radio card path
@@ -198,7 +198,7 @@ export function getDeviceStatus({ accessToken, userAgent, deviceId, requestOptio
198
198
  * @property {string} locale - Device locale (e.g., 'en') (undocumented)
199
199
  * @property {string} maxVolumeLimit - Maximum volume limit
200
200
  * @property {string} nightAmbientColour - Night ambient light color (hex code)
201
- * @property {string} nightDisplayBrightness - Night display brightness
201
+ * @property {string} nightDisplayBrightness - Night display brightness (e.g., 'auto', '0' - '100')
202
202
  * @property {string} nightMaxVolumeLimit - Night maximum volume limit
203
203
  * @property {string} nightTime - Night mode start time (e.g., '19:20')
204
204
  * @property {string} nightYotoDaily - Night mode Yoto Daily card path
@@ -865,7 +865,7 @@ export type YotoDeviceConfig = {
865
865
  */
866
866
  clockFace: string;
867
867
  /**
868
- * - Day display brightness (e.g., 'auto', '100')
868
+ * - Day display brightness (e.g., 'auto', '0' - '100')
869
869
  */
870
870
  dayDisplayBrightness: string;
871
871
  /**
@@ -917,7 +917,7 @@ export type YotoDeviceConfig = {
917
917
  */
918
918
  nightAmbientColour: string;
919
919
  /**
920
- * - Night display brightness
920
+ * - Night display brightness (e.g., 'auto', '0' - '100')
921
921
  */
922
922
  nightDisplayBrightness: string;
923
923
  /**
@@ -226,7 +226,7 @@ export async function getDeviceStatus ({
226
226
  * @property {string} bluetoothEnabled - Bluetooth enabled state ('0' or '1')
227
227
  * @property {boolean} btHeadphonesEnabled - Bluetooth headphones enabled
228
228
  * @property {string} clockFace - Clock face style (e.g., 'digital-sun')
229
- * @property {string} dayDisplayBrightness - Day display brightness (e.g., 'auto', '100')
229
+ * @property {string} dayDisplayBrightness - Day display brightness (e.g., 'auto', '0' - '100')
230
230
  * @property {string} dayTime - Day mode start time (e.g., '07:00')
231
231
  * @property {string} dayYotoDaily - Day mode Yoto Daily card path
232
232
  * @property {string} dayYotoRadio - Day mode Yoto Radio card path
@@ -239,7 +239,7 @@ export async function getDeviceStatus ({
239
239
  * @property {string} locale - Device locale (e.g., 'en') (undocumented)
240
240
  * @property {string} maxVolumeLimit - Maximum volume limit
241
241
  * @property {string} nightAmbientColour - Night ambient light color (hex code)
242
- * @property {string} nightDisplayBrightness - Night display brightness
242
+ * @property {string} nightDisplayBrightness - Night display brightness (e.g., 'auto', '0' - '100')
243
243
  * @property {string} nightMaxVolumeLimit - Night maximum volume limit
244
244
  * @property {string} nightTime - Night mode start time (e.g., '19:20')
245
245
  * @property {string} nightYotoDaily - Night mode Yoto Daily card path
@@ -1,15 +1,15 @@
1
1
  import test from 'node:test'
2
- import assert from 'node:assert'
2
+ import assert from 'node:assert/strict'
3
3
  import { getDevices, getDeviceStatus, getDeviceConfig } from './devices.js'
4
4
  import { YotoAPIError } from './helpers.js'
5
5
  import { loadTestTokens, logResponse } from './endpoint-test-helpers.js'
6
6
 
7
- const { accessToken } = loadTestTokens()
7
+ const { token } = loadTestTokens()
8
8
 
9
9
  test('getDevices', async (t) => {
10
10
  await t.test('should fetch user devices', async () => {
11
11
  const response = await getDevices({
12
- accessToken
12
+ accessToken: await token.getAccessToken()
13
13
  })
14
14
 
15
15
  // Log response for type verification and documentation
@@ -23,14 +23,14 @@ test('getDevices', async (t) => {
23
23
  // Validate device structure
24
24
  const device = response.devices[0]
25
25
  assert.ok(device, 'Device should exist')
26
- assert.ok(typeof device.deviceId === 'string', 'Device should have deviceId string')
27
- assert.ok(typeof device.name === 'string', 'Device should have name string')
28
- assert.ok(typeof device.description === 'string', 'Device should have description string')
29
- assert.ok(typeof device.online === 'boolean', 'Device should have online boolean')
30
- assert.ok(typeof device.releaseChannel === 'string', 'Device should have releaseChannel string')
31
- assert.ok(typeof device.deviceType === 'string', 'Device should have deviceType string')
32
- assert.ok(typeof device.deviceFamily === 'string', 'Device should have deviceFamily string')
33
- assert.ok(typeof device.deviceGroup === 'string', 'Device should have deviceGroup string')
26
+ assert.equal(typeof device.deviceId, 'string', 'Device should have deviceId string')
27
+ assert.equal(typeof device.name, 'string', 'Device should have name string')
28
+ assert.equal(typeof device.description, 'string', 'Device should have description string')
29
+ assert.equal(typeof device.online, 'boolean', 'Device should have online boolean')
30
+ assert.equal(typeof device.releaseChannel, 'string', 'Device should have releaseChannel string')
31
+ assert.equal(typeof device.deviceType, 'string', 'Device should have deviceType string')
32
+ assert.equal(typeof device.deviceFamily, 'string', 'Device should have deviceFamily string')
33
+ assert.equal(typeof device.deviceGroup, 'string', 'Device should have deviceGroup string')
34
34
  })
35
35
 
36
36
  await t.test('should fail with invalid token', async () => {
@@ -61,7 +61,7 @@ test('getDeviceStatus', async (t) => {
61
61
  // Get real device IDs to test with
62
62
  await t.test('setup - get device IDs', async () => {
63
63
  const response = await getDevices({
64
- accessToken
64
+ accessToken: await token.getAccessToken()
65
65
  })
66
66
 
67
67
  assert.ok(response.devices.length > 0, 'User should have at least one device for getDeviceStatus tests')
@@ -79,7 +79,7 @@ test('getDeviceStatus', async (t) => {
79
79
  assert.ok(deviceId, 'Device ID should exist')
80
80
 
81
81
  const status = await getDeviceStatus({
82
- accessToken,
82
+ accessToken: await token.getAccessToken(),
83
83
  deviceId
84
84
  })
85
85
 
@@ -89,23 +89,23 @@ test('getDeviceStatus', async (t) => {
89
89
  // Validate response structure matches YotoDeviceStatusResponse
90
90
  assert.ok(status, 'Response should exist')
91
91
  assert.strictEqual(status.deviceId, deviceId, 'Returned device ID should match requested ID')
92
- assert.ok(typeof status.updatedAt === 'string', 'Status should have updatedAt string')
92
+ assert.equal(typeof status.updatedAt, 'string', 'Status should have updatedAt string')
93
93
 
94
94
  // Optional fields - only validate type if present
95
95
  if (status.batteryLevelPercentage !== undefined) {
96
- assert.ok(typeof status.batteryLevelPercentage === 'number', 'Battery level should be number')
96
+ assert.equal(typeof status.batteryLevelPercentage, 'number', 'Battery level should be number')
97
97
  }
98
98
  if (status.isCharging !== undefined) {
99
- assert.ok(typeof status.isCharging === 'boolean', 'isCharging should be boolean')
99
+ assert.equal(typeof status.isCharging, 'boolean', 'isCharging should be boolean')
100
100
  }
101
101
  if (status.isOnline !== undefined) {
102
- assert.ok(typeof status.isOnline === 'boolean', 'isOnline should be boolean')
102
+ assert.equal(typeof status.isOnline, 'boolean', 'isOnline should be boolean')
103
103
  }
104
104
  if (status.userVolumePercentage !== undefined) {
105
- assert.ok(typeof status.userVolumePercentage === 'number', 'User volume should be number')
105
+ assert.equal(typeof status.userVolumePercentage, 'number', 'User volume should be number')
106
106
  }
107
107
  if (status.systemVolumePercentage !== undefined) {
108
- assert.ok(typeof status.systemVolumePercentage === 'number', 'System volume should be number')
108
+ assert.equal(typeof status.systemVolumePercentage, 'number', 'System volume should be number')
109
109
  }
110
110
  if (status.temperatureCelcius !== undefined && status.temperatureCelcius !== null) {
111
111
  assert.ok(
@@ -114,7 +114,7 @@ test('getDeviceStatus', async (t) => {
114
114
  )
115
115
  }
116
116
  if (status.wifiStrength !== undefined) {
117
- assert.ok(typeof status.wifiStrength === 'number', 'WiFi strength should be number')
117
+ assert.equal(typeof status.wifiStrength, 'number', 'WiFi strength should be number')
118
118
  }
119
119
  if (status.cardInsertionState !== undefined) {
120
120
  assert.ok([0, 1, 2].includes(status.cardInsertionState), 'Card insertion state should be 0, 1, or 2')
@@ -131,7 +131,7 @@ test('getDeviceStatus', async (t) => {
131
131
  // Test with multiple devices to ensure consistency
132
132
  for (const deviceId of testDeviceIds) {
133
133
  const status = await getDeviceStatus({
134
- accessToken,
134
+ accessToken: await token.getAccessToken(),
135
135
  deviceId
136
136
  })
137
137
 
@@ -146,7 +146,7 @@ test('getDeviceStatus', async (t) => {
146
146
  }
147
147
 
148
148
  const status = await getDeviceStatus({
149
- accessToken,
149
+ accessToken: await token.getAccessToken(),
150
150
  deviceId: onlineDeviceId
151
151
  })
152
152
 
@@ -163,7 +163,7 @@ test('getDeviceStatus', async (t) => {
163
163
  }
164
164
 
165
165
  const status = await getDeviceStatus({
166
- accessToken,
166
+ accessToken: await token.getAccessToken(),
167
167
  deviceId: offlineDeviceId
168
168
  })
169
169
 
@@ -178,13 +178,13 @@ test('getDeviceStatus', async (t) => {
178
178
  await assert.rejects(
179
179
  async () => {
180
180
  await getDeviceStatus({
181
- accessToken,
181
+ accessToken: await token.getAccessToken(),
182
182
  deviceId: 'invalid-device-id-12345'
183
183
  })
184
184
  },
185
185
  (err) => {
186
186
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
187
- assert.ok(err.statusCode === 404, 'Should return 404 for invalid device ID')
187
+ assert.equal(err.statusCode, 404, 'Should return 404 for invalid device ID')
188
188
  assert.ok(err.body, 'Error should have body')
189
189
  return true
190
190
  }
@@ -225,7 +225,7 @@ test('getDeviceConfig', async (t) => {
225
225
  // Get real device IDs to test with
226
226
  await t.test('setup - get device IDs', async () => {
227
227
  const response = await getDevices({
228
- accessToken
228
+ accessToken: await token.getAccessToken()
229
229
  })
230
230
 
231
231
  assert.ok(response.devices.length > 0, 'User should have at least one device for getDeviceConfig tests')
@@ -251,7 +251,7 @@ test('getDeviceConfig', async (t) => {
251
251
  for (const deviceId of testDeviceIds) {
252
252
  try {
253
253
  const config = await getDeviceConfig({
254
- accessToken,
254
+ accessToken: await token.getAccessToken(),
255
255
  deviceId
256
256
  })
257
257
  if (config.device.config.alarms && config.device.config.alarms.length > 0) {
@@ -273,7 +273,7 @@ test('getDeviceConfig', async (t) => {
273
273
  assert.ok(deviceId, 'Device ID should exist')
274
274
 
275
275
  const config = await getDeviceConfig({
276
- accessToken,
276
+ accessToken: await token.getAccessToken(),
277
277
  deviceId
278
278
  })
279
279
 
@@ -286,33 +286,33 @@ test('getDeviceConfig', async (t) => {
286
286
  assert.strictEqual(config.device.deviceId, deviceId, 'Returned device ID should match requested ID')
287
287
 
288
288
  // Validate device structure
289
- assert.ok(typeof config.device.deviceFamily === 'string', 'Device should have deviceFamily string')
290
- assert.ok(typeof config.device.deviceType === 'string', 'Device should have deviceType string')
291
- assert.ok(typeof config.device.online === 'boolean', 'Device should have online boolean')
289
+ assert.equal(typeof config.device.deviceFamily, 'string', 'Device should have deviceFamily string')
290
+ assert.equal(typeof config.device.deviceType, 'string', 'Device should have deviceType string')
291
+ assert.equal(typeof config.device.online, 'boolean', 'Device should have online boolean')
292
292
 
293
293
  // Validate config object exists
294
294
  assert.ok(config.device.config, 'Device should have config object')
295
- assert.ok(typeof config.device.config === 'object', 'Config should be an object')
295
+ assert.equal(typeof config.device.config, 'object', 'Config should be an object')
296
296
 
297
297
  // Validate some config fields
298
- if (config.device.config.ambientColour !== undefined) {
299
- assert.ok(typeof config.device.config.ambientColour === 'string', 'ambientColour should be string')
298
+ if (config.device.config.ambientColour !== null) {
299
+ assert.equal(typeof config.device.config.ambientColour, 'string', 'ambientColour should be string')
300
300
  }
301
301
  if (config.device.config.clockFace !== undefined) {
302
- assert.ok(typeof config.device.config.clockFace === 'string', 'clockFace should be string')
302
+ assert.equal(typeof config.device.config.clockFace, 'string', 'clockFace should be string')
303
303
  }
304
304
  if (config.device.config.repeatAll !== undefined) {
305
- assert.ok(typeof config.device.config.repeatAll === 'boolean', 'repeatAll should be boolean')
305
+ assert.equal(typeof config.device.config.repeatAll, 'boolean', 'repeatAll should be boolean')
306
306
  }
307
307
  if (config.device.config.volumeLevel !== undefined) {
308
- assert.ok(typeof config.device.config.volumeLevel === 'string', 'volumeLevel should be string')
308
+ assert.equal(typeof config.device.config.volumeLevel, 'string', 'volumeLevel should be string')
309
309
  }
310
310
 
311
311
  // Validate shortcuts if present (beta feature)
312
312
  if (config.device.shortcuts) {
313
- assert.ok(typeof config.device.shortcuts === 'object', 'Shortcuts should be an object')
313
+ assert.equal(typeof config.device.shortcuts, 'object', 'Shortcuts should be an object')
314
314
  if (config.device.shortcuts.versionId) {
315
- assert.ok(typeof config.device.shortcuts.versionId === 'string', 'versionId should be string')
315
+ assert.equal(typeof config.device.shortcuts.versionId, 'string', 'versionId should be string')
316
316
  }
317
317
  }
318
318
  })
@@ -321,7 +321,7 @@ test('getDeviceConfig', async (t) => {
321
321
  // Test with different device types to ensure config properties vary appropriately
322
322
  for (const [deviceType, deviceId] of deviceTypeMap) {
323
323
  const config = await getDeviceConfig({
324
- accessToken,
324
+ accessToken: await token.getAccessToken(),
325
325
  deviceId
326
326
  })
327
327
 
@@ -335,7 +335,7 @@ test('getDeviceConfig', async (t) => {
335
335
 
336
336
  // Config should always be present but properties may vary by device type
337
337
  assert.ok(config.device.config, 'Config should exist for all device types')
338
- assert.ok(typeof config.device.config === 'object', 'Config should be object')
338
+ assert.equal(typeof config.device.config, 'object', 'Config should be object')
339
339
  }
340
340
  })
341
341
 
@@ -343,7 +343,7 @@ test('getDeviceConfig', async (t) => {
343
343
  // Log configurations for different device types to see variations
344
344
  for (const [deviceType, deviceId] of deviceTypeMap) {
345
345
  const config = await getDeviceConfig({
346
- accessToken,
346
+ accessToken: await token.getAccessToken(),
347
347
  deviceId
348
348
  })
349
349
 
@@ -358,7 +358,7 @@ test('getDeviceConfig', async (t) => {
358
358
  }
359
359
 
360
360
  const config = await getDeviceConfig({
361
- accessToken,
361
+ accessToken: await token.getAccessToken(),
362
362
  deviceId: onlineDeviceId
363
363
  })
364
364
 
@@ -378,7 +378,7 @@ test('getDeviceConfig', async (t) => {
378
378
  }
379
379
 
380
380
  const config = await getDeviceConfig({
381
- accessToken,
381
+ accessToken: await token.getAccessToken(),
382
382
  deviceId: deviceWithAlarmsId
383
383
  })
384
384
 
@@ -448,7 +448,7 @@ test('getDeviceConfig', async (t) => {
448
448
  await assert.rejects(
449
449
  async () => {
450
450
  await getDeviceConfig({
451
- accessToken,
451
+ accessToken: await token.getAccessToken(),
452
452
  deviceId: 'invalid-device-id-12345'
453
453
  })
454
454
  },
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * Load tokens from .env file for testing
3
- * @returns {{accessToken: string, refreshToken: string, clientId: string}}
3
+ * @returns {{ token: RefreshableToken }}
4
4
  */
5
5
  export function loadTestTokens(): {
6
- accessToken: string;
7
- refreshToken: string;
8
- clientId: string;
6
+ token: RefreshableToken;
9
7
  };
10
8
  /**
11
9
  * Log API response for type verification and documentation.
@@ -25,4 +23,5 @@ export function loadTestTokens(): {
25
23
  * logResponse('GET DEVICES', devices)
26
24
  */
27
25
  export function logResponse(label: string, response: any): void;
26
+ import { RefreshableToken } from '../token.js';
28
27
  //# sourceMappingURL=endpoint-test-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"endpoint-test-helpers.d.ts","sourceRoot":"","sources":["endpoint-test-helpers.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,kCAFa;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAiCzE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,mCAPW,MAAM,YACN,GAAG,QASb"}
1
+ {"version":3,"file":"endpoint-test-helpers.d.ts","sourceRoot":"","sources":["endpoint-test-helpers.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,kCAFa;IAAE,KAAK,EAAE,gBAAgB,CAAA;CAAE,CA+CvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,mCAPW,MAAM,YACN,GAAG,QASb;iCA1EgC,aAAa"}