ring-client-api 14.0.0-beta.1 → 14.0.0-beta.2
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/lib/location.d.ts +4 -4
- package/lib/location.js +35 -18
- package/lib/streaming/webrtc-connection.d.ts +1 -1
- package/lib/streaming/webrtc-connection.js +1 -1
- package/package.json +6 -9
- package/lib/test/rest-client.spec.d.ts +0 -1
- package/lib/test/rest-client.spec.js +0 -241
- package/lib/test/ring-camera.spec.d.ts +0 -1
- package/lib/test/ring-camera.spec.js +0 -36
package/lib/location.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Socket as SocketIOSocket } from 'socket.io-client';
|
|
2
1
|
import type { Observable } from 'rxjs';
|
|
2
|
+
import { WebSocket } from 'undici';
|
|
3
3
|
import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs';
|
|
4
4
|
import type { AccountMonitoringStatus, AlarmMode, AssetSession, MessageDataType, MessageType, RingDeviceData, SocketIoMessage, TicketAsset, UserLocation, CameraEventResponse, CameraEventOptions, HistoryOptions, RingDeviceHistoryEvent, LocationModeResponse, LocationModeSettings, LocationMode, LocationModeSharing, LocationModeSettingsResponse, LocationModeInput } from './ring-types.ts';
|
|
5
5
|
import type { RingRestClient } from './rest-client.ts';
|
|
@@ -21,7 +21,7 @@ export declare class Location extends Subscribed {
|
|
|
21
21
|
private onLocationModeRequested;
|
|
22
22
|
reconnecting: boolean;
|
|
23
23
|
private disconnected;
|
|
24
|
-
connectionPromise?: Promise<
|
|
24
|
+
connectionPromise?: Promise<WebSocket>;
|
|
25
25
|
securityPanel?: RingDevice;
|
|
26
26
|
assets?: TicketAsset[];
|
|
27
27
|
receivedAssetDeviceLists: string[];
|
|
@@ -46,8 +46,8 @@ export declare class Location extends Subscribed {
|
|
|
46
46
|
get id(): string;
|
|
47
47
|
get locationId(): string;
|
|
48
48
|
get name(): string;
|
|
49
|
-
createConnection(): Promise<
|
|
50
|
-
getConnection(): Promise<
|
|
49
|
+
createConnection(): Promise<WebSocket>;
|
|
50
|
+
getConnection(): Promise<WebSocket>;
|
|
51
51
|
sendMessage(message: {
|
|
52
52
|
msg: MessageType;
|
|
53
53
|
datatype?: MessageDataType;
|
package/lib/location.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WebSocket } from 'undici';
|
|
2
2
|
import { BehaviorSubject, firstValueFrom, merge, ReplaySubject, Subject, } from 'rxjs';
|
|
3
3
|
import { concatMap, debounceTime, distinctUntilChanged, filter, map, scan, shareReplay, skip, } from 'rxjs/operators';
|
|
4
4
|
import { delay, generateUuid, logDebug, logError, logInfo } from "./util.js";
|
|
@@ -116,7 +116,7 @@ export class Location extends Subscribed {
|
|
|
116
116
|
logError('Node 15 is not currently supported by the Ring client. Please install the latest Node 14 instead. May not be able to fetch devices from Ring Alarm and Smart Lighting Hubs on this version of node.');
|
|
117
117
|
}
|
|
118
118
|
const { assets, ticket, host } = await this.restClient.request({
|
|
119
|
-
url: appApi(
|
|
119
|
+
url: appApi(`clap/tickets?locationID=${this.id}&enableExtendedEmergencyCellUsage=true&requestedTransport=ws`),
|
|
120
120
|
}), supportedAssets = assets.filter(isWebSocketSupportedAsset);
|
|
121
121
|
this.assets = supportedAssets;
|
|
122
122
|
this.receivedAssetDeviceLists.length = 0;
|
|
@@ -126,7 +126,7 @@ export class Location extends Subscribed {
|
|
|
126
126
|
logError(errorMessage);
|
|
127
127
|
throw new Error(errorMessage);
|
|
128
128
|
}
|
|
129
|
-
const
|
|
129
|
+
const url = `wss://${host}/ws?authcode=${ticket}&ack=false`, socket = new WebSocket(url), reconnect = () => {
|
|
130
130
|
if (this.reconnecting && this.connectionPromise) {
|
|
131
131
|
return this.connectionPromise;
|
|
132
132
|
}
|
|
@@ -135,30 +135,44 @@ export class Location extends Subscribed {
|
|
|
135
135
|
logInfo('Reconnecting location socket.io connection');
|
|
136
136
|
}
|
|
137
137
|
this.reconnecting = true;
|
|
138
|
-
|
|
138
|
+
socket.close();
|
|
139
139
|
return (this.connectionPromise = delay(1000).then(() => {
|
|
140
140
|
return this.createConnection();
|
|
141
141
|
}));
|
|
142
142
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
socket.addEventListener('message', (event) => {
|
|
144
|
+
try {
|
|
145
|
+
const { msg: message, channel } = JSON.parse(event.data);
|
|
146
|
+
this.onMessage.next(message);
|
|
147
|
+
if (message.datatype === 'HubDisconnectionEventType') {
|
|
148
|
+
logInfo('Location connection told to reconnect');
|
|
149
|
+
return reconnect();
|
|
150
|
+
}
|
|
151
|
+
if (channel === 'DataUpdate') {
|
|
152
|
+
this.onDataUpdate.next(message);
|
|
153
|
+
}
|
|
148
154
|
}
|
|
149
|
-
|
|
155
|
+
catch (e) {
|
|
156
|
+
logError('Error parsing message from server: ' + event.data);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
socket.addEventListener('error', (error) => {
|
|
160
|
+
logDebug('WebSocket error: ' + error);
|
|
161
|
+
return reconnect();
|
|
150
162
|
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
socket.addEventListener('close', (event) => {
|
|
164
|
+
logDebug(`WebSocket connection closed: ${event.code}, ${event.reason}`);
|
|
165
|
+
return reconnect();
|
|
166
|
+
});
|
|
167
|
+
this.reconnecting = false;
|
|
154
168
|
return new Promise((resolve, reject) => {
|
|
155
|
-
|
|
156
|
-
resolve(
|
|
169
|
+
socket.addEventListener('open', () => {
|
|
170
|
+
resolve(socket);
|
|
157
171
|
this.onConnected.next(true);
|
|
158
172
|
logInfo('Ring connected to socket.io server');
|
|
159
173
|
assets.forEach((asset) => this.requestList(deviceListMessageType, asset.uuid));
|
|
160
|
-
});
|
|
161
|
-
|
|
174
|
+
}, { once: true });
|
|
175
|
+
socket.addEventListener('error', reject, { once: true });
|
|
162
176
|
}).catch(reconnect);
|
|
163
177
|
}
|
|
164
178
|
getConnection() {
|
|
@@ -173,7 +187,10 @@ export class Location extends Subscribed {
|
|
|
173
187
|
async sendMessage(message) {
|
|
174
188
|
const connection = await this.getConnection();
|
|
175
189
|
message.seq = this.seq++;
|
|
176
|
-
connection.
|
|
190
|
+
connection.send(JSON.stringify({
|
|
191
|
+
channel: 'message',
|
|
192
|
+
msg: message,
|
|
193
|
+
}));
|
|
177
194
|
}
|
|
178
195
|
async sendCommandToSecurityPanel(commandType, data) {
|
|
179
196
|
const securityPanel = await this.getSecurityPanel();
|
|
@@ -17,7 +17,7 @@ export declare class WebrtcConnection extends Subscribed {
|
|
|
17
17
|
readonly onMessage: ReplaySubject<{
|
|
18
18
|
method: string;
|
|
19
19
|
}>;
|
|
20
|
-
readonly onWsOpen: import("rxjs").Observable<
|
|
20
|
+
readonly onWsOpen: import("rxjs").Observable<Event>;
|
|
21
21
|
readonly onAudioRtp: Subject<RtpPacket>;
|
|
22
22
|
readonly onVideoRtp: Subject<RtpPacket>;
|
|
23
23
|
private readonly pc;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebSocket } from '
|
|
1
|
+
import { WebSocket } from 'undici';
|
|
2
2
|
import { firstValueFrom, fromEvent, interval, ReplaySubject, Subject, } from 'rxjs';
|
|
3
3
|
import { concatMap, take } from 'rxjs/operators';
|
|
4
4
|
import { generateUuid, logDebug, logError, logInfo } from "../util.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "14.0.0-beta.
|
|
3
|
+
"version": "14.0.0-beta.2",
|
|
4
4
|
"description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -24,28 +24,25 @@
|
|
|
24
24
|
"debug": "^4.4.0",
|
|
25
25
|
"json-bigint": "^1.0.0",
|
|
26
26
|
"rxjs": "^7.8.2",
|
|
27
|
-
"socket.io-client": "^2.5.0",
|
|
28
27
|
"systeminformation": "^5.25.11",
|
|
29
28
|
"undici": "^7.8.0",
|
|
30
29
|
"uuid": "^11.1.0",
|
|
31
|
-
"werift": "0.22.1"
|
|
32
|
-
"ws": "^8.18.1"
|
|
30
|
+
"werift": "0.22.1"
|
|
33
31
|
},
|
|
34
32
|
"devDependencies": {
|
|
35
33
|
"@types/debug": "4.1.12",
|
|
36
34
|
"@types/json-bigint": "^1.0.4",
|
|
37
|
-
"@types/node": "22.
|
|
38
|
-
"@types/socket.io-client": "1.4.36",
|
|
35
|
+
"@types/node": "22.15.17",
|
|
39
36
|
"@types/uuid": "10.0.0",
|
|
40
37
|
"@types/ws": "^8.18.1",
|
|
41
38
|
"eslint-config-shared": "*",
|
|
42
|
-
"msw": "^2.
|
|
39
|
+
"msw": "^2.8.0",
|
|
43
40
|
"tsconfig": "*",
|
|
44
41
|
"typescript": "5.8.3",
|
|
45
|
-
"vitest": "^3.1.
|
|
42
|
+
"vitest": "^3.1.3"
|
|
46
43
|
},
|
|
47
44
|
"engines": {
|
|
48
|
-
"node": "^
|
|
45
|
+
"node": "^20 || ^22"
|
|
49
46
|
},
|
|
50
47
|
"repository": {
|
|
51
48
|
"type": "git",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { http, HttpResponse } from 'msw';
|
|
2
|
-
import { setupServer } from 'msw/node';
|
|
3
|
-
import { RingRestClient } from "../rest-client.js";
|
|
4
|
-
import { clearTimeouts, getHardwareId, toBase64 } from "../util.js";
|
|
5
|
-
import { firstValueFrom } from 'rxjs';
|
|
6
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, } from 'vitest';
|
|
7
|
-
let sessionCreatedCount = 0, client;
|
|
8
|
-
const email = 'some@one.com', password = 'abc123!', phone = '+1xxxxxxxx89', twoFactorAuthCode = '123456', hardwareIdPromise = getHardwareId(), accessToken = 'ey__accees_token', secondAccessToken = 'ey__second_accees_token', refreshToken = 'ey__refresh_token', secondRefreshToken = 'ey__second_refresh_token', thirdRefreshToken = 'ey__third_refresh_token', server = setupServer(http.post('https://oauth.ring.com/oauth/token', async ({ request: req }) => {
|
|
9
|
-
const body = await req.json();
|
|
10
|
-
if (req.headers.get('2fa-support') !== 'true' ||
|
|
11
|
-
req.headers.get('User-Agent') !== 'android:com.ringapp' ||
|
|
12
|
-
req.headers.get('hardware_id') !== (await hardwareIdPromise)) {
|
|
13
|
-
return HttpResponse.json({
|
|
14
|
-
code: 1,
|
|
15
|
-
error: 'Invalid auth headers: ' + JSON.stringify(req.headers, null, 2),
|
|
16
|
-
}, { status: 400 });
|
|
17
|
-
}
|
|
18
|
-
if (body.grant_type === 'refresh_token') {
|
|
19
|
-
if (body.refresh_token === refreshToken) {
|
|
20
|
-
// Valid refresh token
|
|
21
|
-
return HttpResponse.json({
|
|
22
|
-
access_token: accessToken,
|
|
23
|
-
expires_in: 3600,
|
|
24
|
-
refresh_token: secondRefreshToken,
|
|
25
|
-
scope: 'client',
|
|
26
|
-
token_type: 'Bearer',
|
|
27
|
-
}, { status: 200 });
|
|
28
|
-
}
|
|
29
|
-
if (body.refresh_token === secondRefreshToken) {
|
|
30
|
-
// Valid refresh token
|
|
31
|
-
return HttpResponse.json({
|
|
32
|
-
access_token: secondAccessToken,
|
|
33
|
-
expires_in: 3600,
|
|
34
|
-
refresh_token: thirdRefreshToken,
|
|
35
|
-
scope: 'client',
|
|
36
|
-
token_type: 'Bearer',
|
|
37
|
-
}, { status: 200 });
|
|
38
|
-
}
|
|
39
|
-
// Invalid refresh token
|
|
40
|
-
return HttpResponse.json({
|
|
41
|
-
error: 'invalid_grant',
|
|
42
|
-
error_description: 'token is invalid or does not exists',
|
|
43
|
-
}, { status: 401 });
|
|
44
|
-
}
|
|
45
|
-
if (body.grant_type !== 'password' ||
|
|
46
|
-
body.client_id !== 'ring_official_android' ||
|
|
47
|
-
body.scope !== 'client') {
|
|
48
|
-
return HttpResponse.json({
|
|
49
|
-
code: 1,
|
|
50
|
-
error: 'Invalid auth request: ' + JSON.stringify(body),
|
|
51
|
-
}, { status: 400 });
|
|
52
|
-
}
|
|
53
|
-
if (body.username !== email || body.password !== password) {
|
|
54
|
-
// Wrong username or password
|
|
55
|
-
return HttpResponse.json({
|
|
56
|
-
error: 'access_denied',
|
|
57
|
-
error_description: 'invalid user credentials',
|
|
58
|
-
}, { status: 401 });
|
|
59
|
-
}
|
|
60
|
-
if (req.headers.get('2fa-code') &&
|
|
61
|
-
req.headers.get('2fa-code') !== twoFactorAuthCode) {
|
|
62
|
-
// Wrong 2fa code
|
|
63
|
-
return HttpResponse.json({
|
|
64
|
-
err_msg: 'bad request response from dependency service',
|
|
65
|
-
error: 'Verification Code is invalid or expired',
|
|
66
|
-
}, { status: 400 });
|
|
67
|
-
}
|
|
68
|
-
if (req.headers.get('2fa-code') === twoFactorAuthCode) {
|
|
69
|
-
// Successfull login with correct 2fa code
|
|
70
|
-
return HttpResponse.json({
|
|
71
|
-
access_token: accessToken,
|
|
72
|
-
expires_in: 3600,
|
|
73
|
-
refresh_token: refreshToken,
|
|
74
|
-
scope: 'client',
|
|
75
|
-
token_type: 'Bearer',
|
|
76
|
-
responseTimestamp: Date.now(),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
// 2fa code not provided, so return the 2fa prompt
|
|
80
|
-
return HttpResponse.json({
|
|
81
|
-
next_time_in_secs: 60,
|
|
82
|
-
phone,
|
|
83
|
-
tsv_state: 'sms',
|
|
84
|
-
}, { status: 412 });
|
|
85
|
-
}), http.post('https://api.ring.com/clients_api/session', async ({ request: req }) => {
|
|
86
|
-
const authHeader = req.headers.get('Authorization');
|
|
87
|
-
if (authHeader !== `Bearer ${accessToken}` &&
|
|
88
|
-
authHeader !== `Bearer ${secondAccessToken}`) {
|
|
89
|
-
// Invalid access token used
|
|
90
|
-
return HttpResponse.json({}, { status: 401 });
|
|
91
|
-
}
|
|
92
|
-
const body = await req.json();
|
|
93
|
-
if (body.device.hardware_id !== (await getHardwareId()) ||
|
|
94
|
-
body.device.metadata.api_version !== 11 ||
|
|
95
|
-
body.device.metadata.device_model !== 'ring-client-api' ||
|
|
96
|
-
body.device.os !== 'android') {
|
|
97
|
-
return HttpResponse.text('Bad session request: ' + JSON.stringify(body, null, 2), { status: 400 });
|
|
98
|
-
}
|
|
99
|
-
// Fake a response from the session endpoint, incrementing the sessionCreatedCount
|
|
100
|
-
sessionCreatedCount++;
|
|
101
|
-
return HttpResponse.json({
|
|
102
|
-
profile: {
|
|
103
|
-
id: 1234,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
}));
|
|
107
|
-
async function wrapRefreshToken(rt) {
|
|
108
|
-
return toBase64(JSON.stringify({
|
|
109
|
-
rt,
|
|
110
|
-
hid: await hardwareIdPromise,
|
|
111
|
-
}));
|
|
112
|
-
}
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
sessionCreatedCount = 0;
|
|
115
|
-
});
|
|
116
|
-
beforeAll(() => {
|
|
117
|
-
// Establish requests interception layer before all tests.
|
|
118
|
-
server.listen();
|
|
119
|
-
});
|
|
120
|
-
afterAll(() => {
|
|
121
|
-
// Clean up after all tests are done, preventing this
|
|
122
|
-
// interception layer from affecting irrelevant tests.
|
|
123
|
-
server.close();
|
|
124
|
-
});
|
|
125
|
-
afterEach(() => {
|
|
126
|
-
client.clearTimeouts();
|
|
127
|
-
clearTimeouts();
|
|
128
|
-
});
|
|
129
|
-
describe('getAuth', () => {
|
|
130
|
-
it('should throw and set the 2fa prompt', async () => {
|
|
131
|
-
client = new RingRestClient({
|
|
132
|
-
password,
|
|
133
|
-
email,
|
|
134
|
-
});
|
|
135
|
-
await expect(() => client.getAuth()).rejects.toThrow('Your Ring account is configured to use 2-factor authentication (2fa). See https://github.com/dgreif/ring/wiki/Refresh-Tokens for details.');
|
|
136
|
-
expect(client.promptFor2fa).toEqual(`Please enter the code sent to ${phone} via sms`);
|
|
137
|
-
expect(client.using2fa).toEqual(true);
|
|
138
|
-
});
|
|
139
|
-
it('should accept a 2fa code', async () => {
|
|
140
|
-
client = new RingRestClient({
|
|
141
|
-
password,
|
|
142
|
-
email,
|
|
143
|
-
});
|
|
144
|
-
// ignore the first reject, it's tested above
|
|
145
|
-
await expect(() => client.getAuth()).rejects.toThrow();
|
|
146
|
-
// call getAuth again with the 2fa code, which should succeed
|
|
147
|
-
const auth = await client.getAuth(twoFactorAuthCode);
|
|
148
|
-
expect(auth).toMatchObject({
|
|
149
|
-
access_token: accessToken,
|
|
150
|
-
refresh_token: await wrapRefreshToken(refreshToken),
|
|
151
|
-
});
|
|
152
|
-
expect(client.refreshToken).toEqual(await wrapRefreshToken(refreshToken));
|
|
153
|
-
});
|
|
154
|
-
it('should handle invalid credentials', async () => {
|
|
155
|
-
client = new RingRestClient({
|
|
156
|
-
password: 'incorrect password',
|
|
157
|
-
email,
|
|
158
|
-
});
|
|
159
|
-
await expect(() => client.getAuth()).rejects.toThrow('Failed to fetch oauth token from Ring. Verify that your email and password are correct. (error: access_denied)');
|
|
160
|
-
});
|
|
161
|
-
it('should handle invalid 2fa code', async () => {
|
|
162
|
-
client = new RingRestClient({
|
|
163
|
-
password,
|
|
164
|
-
email,
|
|
165
|
-
});
|
|
166
|
-
// ignore the first reject, it's it('ove
|
|
167
|
-
await expect(() => client.getAuth()).rejects.toThrow();
|
|
168
|
-
// call getAuth again with an invalid 2fa code, which should fail
|
|
169
|
-
await expect(() => client.getAuth('invalid 2fa code')).rejects.toThrow('Verification Code is invalid or expired');
|
|
170
|
-
expect(client.promptFor2fa).toEqual('Invalid 2fa code entered. Please try again.');
|
|
171
|
-
});
|
|
172
|
-
it('should establish a valid auth token with a valid refresh token', async () => {
|
|
173
|
-
client = new RingRestClient({
|
|
174
|
-
refreshToken,
|
|
175
|
-
});
|
|
176
|
-
expect(await client.getCurrentAuth()).toMatchObject({
|
|
177
|
-
access_token: accessToken,
|
|
178
|
-
refresh_token: await wrapRefreshToken(secondRefreshToken),
|
|
179
|
-
});
|
|
180
|
-
expect(client.refreshToken).toEqual(await wrapRefreshToken(secondRefreshToken));
|
|
181
|
-
});
|
|
182
|
-
it('should emit an event when a new refresh token is created', async () => {
|
|
183
|
-
client = new RingRestClient({
|
|
184
|
-
refreshToken,
|
|
185
|
-
});
|
|
186
|
-
const refreshedPromise = firstValueFrom(client.onRefreshTokenUpdated), auth = await client.getAuth();
|
|
187
|
-
expect(auth).toMatchObject({
|
|
188
|
-
access_token: accessToken,
|
|
189
|
-
refresh_token: await wrapRefreshToken(secondRefreshToken),
|
|
190
|
-
});
|
|
191
|
-
expect(await refreshedPromise).toEqual({
|
|
192
|
-
oldRefreshToken: refreshToken,
|
|
193
|
-
newRefreshToken: await wrapRefreshToken(secondRefreshToken),
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe('fetch', () => {
|
|
198
|
-
let invalidateFirstAccessToken = false;
|
|
199
|
-
beforeEach(() => {
|
|
200
|
-
invalidateFirstAccessToken = false;
|
|
201
|
-
server.use(http.get('https://api.ring.com/clients_api/some_endpoint', ({ request: req }) => {
|
|
202
|
-
const authHeader = req.headers.get('Authorization');
|
|
203
|
-
if (invalidateFirstAccessToken &&
|
|
204
|
-
authHeader === `Bearer ${accessToken}`) {
|
|
205
|
-
// Original access token used, but no longer valid
|
|
206
|
-
return HttpResponse.json({}, { status: 401 });
|
|
207
|
-
}
|
|
208
|
-
if (authHeader !== `Bearer ${accessToken}` &&
|
|
209
|
-
authHeader !== `Bearer ${secondAccessToken}`) {
|
|
210
|
-
// Invalid access token used
|
|
211
|
-
return HttpResponse.json({}, { status: 401 });
|
|
212
|
-
}
|
|
213
|
-
if (sessionCreatedCount === 0) {
|
|
214
|
-
// Session not created yet
|
|
215
|
-
return HttpResponse.json({
|
|
216
|
-
error: 'Session not found for ' + req.headers.get('hardware_id'),
|
|
217
|
-
}, { status: 404 });
|
|
218
|
-
}
|
|
219
|
-
return HttpResponse.json([]);
|
|
220
|
-
}));
|
|
221
|
-
});
|
|
222
|
-
it('should include the auth token as a header', async () => {
|
|
223
|
-
client = new RingRestClient({
|
|
224
|
-
refreshToken,
|
|
225
|
-
});
|
|
226
|
-
const response = await client.request({
|
|
227
|
-
url: 'https://api.ring.com/clients_api/some_endpoint',
|
|
228
|
-
});
|
|
229
|
-
expect(response).toEqual([]);
|
|
230
|
-
});
|
|
231
|
-
it('should fetch a new auth token if the first is no longer valid', async () => {
|
|
232
|
-
client = new RingRestClient({
|
|
233
|
-
refreshToken,
|
|
234
|
-
});
|
|
235
|
-
invalidateFirstAccessToken = true;
|
|
236
|
-
const response = await client.request({
|
|
237
|
-
url: 'https://api.ring.com/clients_api/some_endpoint',
|
|
238
|
-
});
|
|
239
|
-
expect(response).toEqual([]);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { getBatteryLevel, cleanSnapshotUuid } from "../ring-camera.js";
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
describe('Ring Camera', () => {
|
|
4
|
-
describe('battery level', () => {
|
|
5
|
-
it('should handle string battery life', () => {
|
|
6
|
-
expect(getBatteryLevel({
|
|
7
|
-
battery_life: '49',
|
|
8
|
-
})).toEqual(49);
|
|
9
|
-
});
|
|
10
|
-
it('should handle null battery life', () => {
|
|
11
|
-
expect(getBatteryLevel({ battery_life: null })).toEqual(null);
|
|
12
|
-
});
|
|
13
|
-
it('should handle right battery only', () => {
|
|
14
|
-
expect(getBatteryLevel({ battery_life: null, battery_life_2: 24 })).toEqual(24);
|
|
15
|
-
});
|
|
16
|
-
it('should handle left battery only', () => {
|
|
17
|
-
expect(getBatteryLevel({ battery_life: 76, battery_life_2: null })).toEqual(76);
|
|
18
|
-
});
|
|
19
|
-
it('should handle dual batteries', () => {
|
|
20
|
-
expect(getBatteryLevel({ battery_life: '92', battery_life_2: 84 })).toEqual(84);
|
|
21
|
-
expect(getBatteryLevel({ battery_life: '92', battery_life_2: 100 })).toEqual(92);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
describe('cleanSnapshotUuid', () => {
|
|
25
|
-
it('should return the original uuid if it is already clean', () => {
|
|
26
|
-
expect(cleanSnapshotUuid('c2a0a397-3538-422d-bb4e-51837a56b870')).toBe('c2a0a397-3538-422d-bb4e-51837a56b870');
|
|
27
|
-
});
|
|
28
|
-
it('should return remove anything after :', () => {
|
|
29
|
-
expect(cleanSnapshotUuid('c2a0a397-3538-422d-bb4e-51837a56b870:122429140')).toBe('c2a0a397-3538-422d-bb4e-51837a56b870');
|
|
30
|
-
});
|
|
31
|
-
it('should handle falsy values', () => {
|
|
32
|
-
expect(cleanSnapshotUuid(undefined)).toBe(undefined);
|
|
33
|
-
expect(cleanSnapshotUuid(null)).toBe(null);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|