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.
- package/README.md +24 -6
- package/bin/auth.js +4 -3
- package/bin/device-model.js +25 -5
- package/bin/device-tui.js +25 -3
- package/bin/devices.js +58 -42
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +3 -1
- package/bin/lib/token-helpers.d.ts +4 -2
- package/bin/lib/token-helpers.d.ts.map +1 -1
- package/bin/lib/token-helpers.js +9 -8
- package/bin/refresh-token.js +4 -2
- package/bin/token-info.js +2 -2
- package/lib/api-client.d.ts +11 -10
- package/lib/api-client.d.ts.map +1 -1
- package/lib/api-client.js +12 -14
- package/lib/api-endpoints/auth.test.js +4 -4
- package/lib/api-endpoints/content.test.js +32 -32
- package/lib/api-endpoints/devices.d.ts +4 -4
- package/lib/api-endpoints/devices.js +2 -2
- package/lib/api-endpoints/devices.test.js +45 -45
- package/lib/api-endpoints/endpoint-test-helpers.d.ts +3 -4
- package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -1
- package/lib/api-endpoints/endpoint-test-helpers.js +21 -5
- package/lib/api-endpoints/family-library-groups.d.ts +3 -3
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
- package/lib/api-endpoints/family-library-groups.js +3 -3
- package/lib/api-endpoints/family-library-groups.test.js +29 -29
- package/lib/api-endpoints/family.test.js +11 -11
- package/lib/api-endpoints/icons.test.js +14 -14
- package/lib/mqtt/client.d.ts +124 -49
- package/lib/mqtt/client.d.ts.map +1 -1
- package/lib/mqtt/client.js +132 -50
- package/lib/mqtt/factory.d.ts +12 -5
- package/lib/mqtt/factory.d.ts.map +1 -1
- package/lib/mqtt/factory.js +39 -11
- package/lib/mqtt/index.js +2 -1
- package/lib/mqtt/mqtt.test.js +25 -22
- package/lib/token.d.ts +44 -2
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +142 -2
- package/lib/yoto-device.d.ts +392 -32
- package/lib/yoto-device.d.ts.map +1 -1
- package/lib/yoto-device.js +643 -99
- package/lib/yoto-device.test.js +193 -0
- 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 {
|
|
10
|
+
* @import { YotoMqttOptions } from './mqtt/factory.js'
|
|
11
11
|
* @import { RequestOptions } from './api-endpoints/helpers.js'
|
|
12
|
-
* @import {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
668
|
+
await this.#token.getAccessToken()
|
|
671
669
|
|
|
672
670
|
/** @type {YotoMqttOptions} */
|
|
673
671
|
const opts = {
|
|
674
672
|
deviceId,
|
|
675
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
27
|
-
assert.
|
|
28
|
-
assert.
|
|
29
|
-
assert.
|
|
30
|
-
assert.
|
|
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.
|
|
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.
|
|
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.
|
|
121
|
-
assert.
|
|
122
|
-
assert.
|
|
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.
|
|
160
|
-
assert.
|
|
161
|
-
assert.
|
|
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.
|
|
165
|
-
assert.
|
|
166
|
-
assert.
|
|
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.
|
|
179
|
-
assert.
|
|
180
|
-
assert.
|
|
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.
|
|
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.
|
|
190
|
-
assert.
|
|
191
|
-
assert.
|
|
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.
|
|
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 {
|
|
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.
|
|
27
|
-
assert.
|
|
28
|
-
assert.
|
|
29
|
-
assert.
|
|
30
|
-
assert.
|
|
31
|
-
assert.
|
|
32
|
-
assert.
|
|
33
|
-
assert.
|
|
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.
|
|
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.
|
|
96
|
+
assert.equal(typeof status.batteryLevelPercentage, 'number', 'Battery level should be number')
|
|
97
97
|
}
|
|
98
98
|
if (status.isCharging !== undefined) {
|
|
99
|
-
assert.
|
|
99
|
+
assert.equal(typeof status.isCharging, 'boolean', 'isCharging should be boolean')
|
|
100
100
|
}
|
|
101
101
|
if (status.isOnline !== undefined) {
|
|
102
|
-
assert.
|
|
102
|
+
assert.equal(typeof status.isOnline, 'boolean', 'isOnline should be boolean')
|
|
103
103
|
}
|
|
104
104
|
if (status.userVolumePercentage !== undefined) {
|
|
105
|
-
assert.
|
|
105
|
+
assert.equal(typeof status.userVolumePercentage, 'number', 'User volume should be number')
|
|
106
106
|
}
|
|
107
107
|
if (status.systemVolumePercentage !== undefined) {
|
|
108
|
-
assert.
|
|
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.
|
|
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.
|
|
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.
|
|
290
|
-
assert.
|
|
291
|
-
assert.
|
|
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.
|
|
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 !==
|
|
299
|
-
assert.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
313
|
+
assert.equal(typeof config.device.shortcuts, 'object', 'Shortcuts should be an object')
|
|
314
314
|
if (config.device.shortcuts.versionId) {
|
|
315
|
-
assert.
|
|
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.
|
|
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 {{
|
|
3
|
+
* @returns {{ token: RefreshableToken }}
|
|
4
4
|
*/
|
|
5
5
|
export function loadTestTokens(): {
|
|
6
|
-
|
|
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":"
|
|
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"}
|