@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.1
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/.env.example +1 -1
- package/README.md +11 -2
- package/dist/omadaClient/client.d.ts +6 -1
- package/dist/omadaClient/client.js +12 -1
- package/dist/omadaClient/client.js.map +1 -1
- package/dist/omadaClient/diagnose.d.ts +68 -0
- package/dist/omadaClient/diagnose.js +164 -0
- package/dist/omadaClient/diagnose.js.map +1 -0
- package/dist/omadaClient/index.d.ts +6 -0
- package/dist/omadaClient/index.js +15 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +124 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/tools/infra/actions.d.ts +1 -1
- package/dist/tools/infra/actions.js +1 -1
- package/dist/tools/omada/index.js +6 -0
- package/dist/tools/omada/index.js.map +1 -1
- package/dist/tools/omada/macGroups.d.ts +19 -0
- package/dist/tools/omada/macGroups.js +77 -0
- package/dist/tools/omada/macGroups.js.map +1 -0
- package/dist/tools/omada/namespace.js +8 -0
- package/dist/tools/omada/namespace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/.codex/review-rules.md +0 -16
- package/.dockerignore +0 -8
- package/.gitattributes +0 -30
- package/.github/workflows/ci.yml +0 -193
- package/AGENTS.md +0 -389
- package/Dockerfile +0 -50
- package/docker-compose.yml +0 -23
- package/docs/CHANGELOG.md +0 -341
- package/docs/COVERAGE.md +0 -219
- package/docs/QUICK_START.md +0 -240
- package/eslint.config.js +0 -33
- package/n8n/ai-agent-safety-monitor.json +0 -355
- package/openapi.json +0 -1259
- package/scripts/check-version-sync.mjs +0 -69
- package/scripts/generate-jwt.ts +0 -121
- package/src/config.ts +0 -440
- package/src/haClient/automations.ts +0 -261
- package/src/haClient/calendars.ts +0 -94
- package/src/haClient/config.ts +0 -52
- package/src/haClient/devices.ts +0 -653
- package/src/haClient/entities.ts +0 -128
- package/src/haClient/events.ts +0 -689
- package/src/haClient/history.ts +0 -73
- package/src/haClient/index.ts +0 -336
- package/src/haClient/request.ts +0 -142
- package/src/haClient/services.ts +0 -45
- package/src/haClient/states.ts +0 -60
- package/src/haClient/updates.ts +0 -58
- package/src/haClient/wsCommand.ts +0 -44
- package/src/index.ts +0 -164
- package/src/localAI/index.ts +0 -88
- package/src/localAI/prompts.ts +0 -50
- package/src/localAI/providers/base.ts +0 -120
- package/src/localAI/providers/ollama.ts +0 -78
- package/src/localAI/providers/openai.ts +0 -86
- package/src/localAI/types.ts +0 -67
- package/src/omadaClient/auth.ts +0 -178
- package/src/omadaClient/client.ts +0 -337
- package/src/omadaClient/device.ts +0 -118
- package/src/omadaClient/index.ts +0 -310
- package/src/omadaClient/network.ts +0 -284
- package/src/omadaClient/request.ts +0 -365
- package/src/omadaClient/security.ts +0 -51
- package/src/omadaClient/site.ts +0 -344
- package/src/ownerQuestions/service.ts +0 -536
- package/src/permissions/index.ts +0 -143
- package/src/resources/index.ts +0 -213
- package/src/server/auth.ts +0 -175
- package/src/server/chatFace.ts +0 -789
- package/src/server/chatSlice.ts +0 -57
- package/src/server/common.ts +0 -170
- package/src/server/eventSubscription.ts +0 -343
- package/src/server/http.ts +0 -626
- package/src/server/instructions.ts +0 -97
- package/src/server/rateLimiter.ts +0 -227
- package/src/server/stdio.ts +0 -41
- package/src/server/stream.ts +0 -205
- package/src/tools/ai/analyzeSensors.ts +0 -33
- package/src/tools/ai/index.ts +0 -20
- package/src/tools/common.ts +0 -442
- package/src/tools/homeassistant/askOwner.ts +0 -133
- package/src/tools/homeassistant/automationControls.ts +0 -66
- package/src/tools/homeassistant/calendar.ts +0 -97
- package/src/tools/homeassistant/callService.ts +0 -25
- package/src/tools/homeassistant/controlClimate.ts +0 -68
- package/src/tools/homeassistant/controlCover.ts +0 -48
- package/src/tools/homeassistant/controlFan.ts +0 -44
- package/src/tools/homeassistant/controlLight.ts +0 -38
- package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
- package/src/tools/homeassistant/createAutomation.ts +0 -35
- package/src/tools/homeassistant/deleteAutomation.ts +0 -24
- package/src/tools/homeassistant/entityAction.ts +0 -35
- package/src/tools/homeassistant/entityGuard.ts +0 -28
- package/src/tools/homeassistant/getAllSensors.ts +0 -46
- package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
- package/src/tools/homeassistant/getDomainSummary.ts +0 -29
- package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
- package/src/tools/homeassistant/getHistory.ts +0 -24
- package/src/tools/homeassistant/getState.ts +0 -24
- package/src/tools/homeassistant/getStates.ts +0 -46
- package/src/tools/homeassistant/getVersion.ts +0 -21
- package/src/tools/homeassistant/index.ts +0 -121
- package/src/tools/homeassistant/listAutomations.ts +0 -35
- package/src/tools/homeassistant/listEntities.ts +0 -44
- package/src/tools/homeassistant/listPersons.ts +0 -46
- package/src/tools/homeassistant/sceneAndScript.ts +0 -53
- package/src/tools/homeassistant/searchEntities.ts +0 -58
- package/src/tools/homeassistant/sendNotification.ts +0 -179
- package/src/tools/homeassistant/systemTools.ts +0 -52
- package/src/tools/homeassistant/triggerAutomation.ts +0 -24
- package/src/tools/index.ts +0 -78
- package/src/tools/infra/actions.ts +0 -95
- package/src/tools/infra/index.ts +0 -138
- package/src/tools/omada/blockClient.ts +0 -28
- package/src/tools/omada/cyclePoePort.ts +0 -32
- package/src/tools/omada/deleteClient.ts +0 -27
- package/src/tools/omada/disableClientRateLimit.ts +0 -23
- package/src/tools/omada/getClient.ts +0 -14
- package/src/tools/omada/getDevice.ts +0 -14
- package/src/tools/omada/getFirewallSetting.ts +0 -14
- package/src/tools/omada/getInternetInfo.ts +0 -14
- package/src/tools/omada/getLanNetworkList.ts +0 -14
- package/src/tools/omada/getLanProfileList.ts +0 -14
- package/src/tools/omada/getPortForwardingStatus.ts +0 -26
- package/src/tools/omada/getRateLimitProfiles.ts +0 -14
- package/src/tools/omada/getSiteNtpStatus.ts +0 -14
- package/src/tools/omada/getSsidDetail.ts +0 -22
- package/src/tools/omada/getSsidList.ts +0 -21
- package/src/tools/omada/getSwitchStackDetail.ts +0 -14
- package/src/tools/omada/getThreatList.ts +0 -42
- package/src/tools/omada/getWlanGroupList.ts +0 -14
- package/src/tools/omada/graph.ts +0 -187
- package/src/tools/omada/index.ts +0 -153
- package/src/tools/omada/listBlockedClients.ts +0 -17
- package/src/tools/omada/listClients.ts +0 -14
- package/src/tools/omada/listClientsActivity.ts +0 -25
- package/src/tools/omada/listClientsPastConnections.ts +0 -44
- package/src/tools/omada/listDevices.ts +0 -14
- package/src/tools/omada/listDevicesStats.ts +0 -41
- package/src/tools/omada/listMostActiveClients.ts +0 -14
- package/src/tools/omada/listSites.ts +0 -18
- package/src/tools/omada/namespace.ts +0 -1164
- package/src/tools/omada/searchDevices.ts +0 -20
- package/src/tools/omada/setClientRateLimit.ts +0 -26
- package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
- package/src/tools/omada/setSiteNtpServers.ts +0 -36
- package/src/tools/omada/setSsidEnabled.ts +0 -38
- package/src/tools/omada/unblockClient.ts +0 -26
- package/src/tools/types.ts +0 -11
- package/src/tools/utils/pagination-schema.ts +0 -27
- package/src/types/ai/index.ts +0 -11
- package/src/types/ai/types.ts +0 -67
- package/src/types/homeassistant/automations.ts +0 -43
- package/src/types/homeassistant/calendars.ts +0 -26
- package/src/types/homeassistant/config.ts +0 -14
- package/src/types/homeassistant/entities.ts +0 -14
- package/src/types/homeassistant/errors.ts +0 -33
- package/src/types/homeassistant/haConfig.ts +0 -7
- package/src/types/homeassistant/history.ts +0 -13
- package/src/types/homeassistant/index.ts +0 -46
- package/src/types/homeassistant/request.ts +0 -6
- package/src/types/homeassistant/serviceCall.ts +0 -18
- package/src/types/homeassistant/states.ts +0 -58
- package/src/types/homeassistant/updates.ts +0 -28
- package/src/types/index.ts +0 -12
- package/src/types/omada/activeClientInfo.ts +0 -17
- package/src/types/omada/clientActivity.ts +0 -32
- package/src/types/omada/clientBlockStatus.ts +0 -11
- package/src/types/omada/clientPastConnection.ts +0 -82
- package/src/types/omada/index.ts +0 -14
- package/src/types/omada/omadaApiResponse.ts +0 -5
- package/src/types/omada/omadaClientInfo.ts +0 -11
- package/src/types/omada/omadaDeviceInfo.ts +0 -11
- package/src/types/omada/omadaDeviceStats.ts +0 -31
- package/src/types/omada/omadaSiteSummary.ts +0 -5
- package/src/types/omada/oswStackDetail.ts +0 -11
- package/src/types/omada/paginatedResult.ts +0 -6
- package/src/types/omada/rateLimitProfile.ts +0 -58
- package/src/types/omada/threatInfo.ts +0 -38
- package/src/types/omada/tokenResult.ts +0 -6
- package/src/types.ts +0 -2
- package/src/utils/config-validations.ts +0 -68
- package/src/utils/jwt.ts +0 -112
- package/src/utils/logger.ts +0 -121
- package/src/utils/plainText.ts +0 -48
- package/src/utils/redact.ts +0 -59
- package/src/utils/sanitizeError.ts +0 -18
- package/src/utils/tlsDispatcher.ts +0 -69
- package/src/version.ts +0 -5
- package/tests/bdd/automation-creation.spec.ts +0 -214
- package/tests/bdd/automation-management.spec.ts +0 -267
- package/tests/bdd/calendar.spec.ts +0 -190
- package/tests/bdd/configuration.spec.ts +0 -283
- package/tests/bdd/device-control.spec.ts +0 -344
- package/tests/bdd/entity-listing.spec.ts +0 -208
- package/tests/bdd/entity-search.spec.ts +0 -183
- package/tests/bdd/history-retrieval.spec.ts +0 -195
- package/tests/bdd/notifications.spec.ts +0 -238
- package/tests/bdd/service-calls.spec.ts +0 -144
- package/tests/bdd/state-retrieval.spec.ts +0 -204
- package/tests/bench/hotpaths.bench.ts +0 -68
- package/tests/config.test.ts +0 -355
- package/tests/features/automation-creation.feature +0 -36
- package/tests/features/automation-management.feature +0 -54
- package/tests/features/calendar.feature +0 -37
- package/tests/features/configuration.feature +0 -70
- package/tests/features/device-control.feature +0 -100
- package/tests/features/entity-listing.feature +0 -51
- package/tests/features/entity-search.feature +0 -44
- package/tests/features/history-retrieval.feature +0 -38
- package/tests/features/notifications.feature +0 -37
- package/tests/features/service-calls.feature +0 -35
- package/tests/features/state-retrieval.feature +0 -37
- package/tests/fixtures/openapi-default-slice.json +0 -290
- package/tests/haClient/automationTrace.test.ts +0 -59
- package/tests/haClient/automations.test.ts +0 -149
- package/tests/haClient/config.test.ts +0 -117
- package/tests/haClient/entities.test.ts +0 -188
- package/tests/haClient/events.test.ts +0 -455
- package/tests/haClient/eventsTls.test.ts +0 -52
- package/tests/haClient/history.test.ts +0 -228
- package/tests/haClient/index.test.ts +0 -243
- package/tests/haClient/request.test.ts +0 -217
- package/tests/haClient/services.test.ts +0 -120
- package/tests/haClient/states.test.ts +0 -119
- package/tests/haClient/updates.test.ts +0 -193
- package/tests/index.test.ts +0 -160
- package/tests/integration/homeassistant.test.ts +0 -144
- package/tests/integration/omada.test.ts +0 -167
- package/tests/integration/server.test.ts +0 -151
- package/tests/localAI/index.test.ts +0 -177
- package/tests/localAI/ollama.test.ts +0 -187
- package/tests/localAI/openai.test.ts +0 -257
- package/tests/omadaClient/actions.test.ts +0 -161
- package/tests/omadaClient/clientBlocks.test.ts +0 -170
- package/tests/omadaClient/ntp.test.ts +0 -381
- package/tests/omadaClient/site.test.ts +0 -37
- package/tests/ownerQuestions/service.test.ts +0 -499
- package/tests/ownerQuestions/tools.test.ts +0 -186
- package/tests/permissions/index.test.ts +0 -217
- package/tests/resources/index.test.ts +0 -288
- package/tests/scripts/generateJwt.test.ts +0 -44
- package/tests/security/accessControl.test.ts +0 -223
- package/tests/security/authentication.test.ts +0 -117
- package/tests/security/configuration.test.ts +0 -170
- package/tests/security/cryptoTransport.test.ts +0 -172
- package/tests/security/dependencies.test.ts +0 -152
- package/tests/security/injection.test.ts +0 -117
- package/tests/security/insecureDesign.test.ts +0 -121
- package/tests/security/logging.test.ts +0 -131
- package/tests/security/resourceConsumption.test.ts +0 -142
- package/tests/server/auth.test.ts +0 -249
- package/tests/server/chatFace.test.ts +0 -320
- package/tests/server/common.test.ts +0 -84
- package/tests/server/http.test.ts +0 -561
- package/tests/server/instructions.test.ts +0 -152
- package/tests/server/memoizeJsonSchema.test.ts +0 -71
- package/tests/server/stdio.test.ts +0 -99
- package/tests/server/stream.test.ts +0 -285
- package/tests/server/toolVisibility.http.test.ts +0 -111
- package/tests/server/toolVisibility.test.ts +0 -129
- package/tests/tools/ai/analyzeSensors.test.ts +0 -227
- package/tests/tools/common.test.ts +0 -123
- package/tests/tools/homeassistant/handlers.test.ts +0 -872
- package/tests/tools/infra/invokeAction.test.ts +0 -280
- package/tests/tools/omada/graph.test.ts +0 -385
- package/tests/tools/omada/handlers.test.ts +0 -897
- package/tests/tools/omada/namespace.test.ts +0 -483
- package/tests/tools/registry.test.ts +0 -168
- package/tests/utils/config-validations.test.ts +0 -352
- package/tests/utils/jwt.test.ts +0 -115
- package/tests/utils/logger.test.ts +0 -327
- package/tests/utils/redact.test.ts +0 -105
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -38
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventSubscriber against a fake Home Assistant WebSocket server: commands are
|
|
3
|
-
* matched to results by id, each event copy reaches only its own subscription,
|
|
4
|
-
* and subscriptions are restored after a reconnect.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
8
|
-
import { WebSocketServer, type WebSocket } from 'ws';
|
|
9
|
-
import type { AddressInfo } from 'node:net';
|
|
10
|
-
|
|
11
|
-
import { EventSubscriber, HaCommandError, type HaEvent } from '../../src/haClient/events.js';
|
|
12
|
-
import { logger } from '../../src/utils/logger.js';
|
|
13
|
-
import { createHaWebSocketCommand } from '../../src/haClient/wsCommand.js';
|
|
14
|
-
|
|
15
|
-
interface FakeHa {
|
|
16
|
-
url: string;
|
|
17
|
-
received: Array<Record<string, unknown>>;
|
|
18
|
-
sockets: WebSocket[];
|
|
19
|
-
/** Reply to a command; default: success with an empty result. */
|
|
20
|
-
onCommand: (message: Record<string, unknown>, socket: WebSocket) => void;
|
|
21
|
-
close: () => Promise<void>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function fakeHa(): Promise<FakeHa> {
|
|
25
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
26
|
-
await new Promise<void>((resolve) => wss.once('listening', () => resolve()));
|
|
27
|
-
const fake: FakeHa = {
|
|
28
|
-
url: `http://127.0.0.1:${(wss.address() as AddressInfo).port}`,
|
|
29
|
-
received: [],
|
|
30
|
-
sockets: [],
|
|
31
|
-
onCommand: (message, socket) => socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: null })),
|
|
32
|
-
close: () =>
|
|
33
|
-
new Promise((resolve) => {
|
|
34
|
-
for (const socket of fake.sockets) socket.terminate();
|
|
35
|
-
wss.close(() => resolve());
|
|
36
|
-
}),
|
|
37
|
-
};
|
|
38
|
-
wss.on('connection', (socket) => {
|
|
39
|
-
fake.sockets.push(socket);
|
|
40
|
-
socket.send(JSON.stringify({ type: 'auth_required' }));
|
|
41
|
-
socket.on('message', (raw) => {
|
|
42
|
-
const message = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
43
|
-
fake.received.push(message);
|
|
44
|
-
if (message.type === 'auth') {
|
|
45
|
-
socket.send(JSON.stringify({ type: 'auth_ok' }));
|
|
46
|
-
} else {
|
|
47
|
-
fake.onCommand(message, socket);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
return fake;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const sendEvent = (socket: WebSocket, subscriptionId: number, eventType: string, data: Record<string, unknown> = {}) =>
|
|
55
|
-
socket.send(
|
|
56
|
-
JSON.stringify({
|
|
57
|
-
id: subscriptionId,
|
|
58
|
-
type: 'event',
|
|
59
|
-
event: { event_type: eventType, data, origin: 'REMOTE', time_fired: '', context: { id: 'c', parent_id: null, user_id: 'u' } },
|
|
60
|
-
})
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const until = async (condition: () => boolean, ms = 3000) => {
|
|
64
|
-
const deadline = Date.now() + ms;
|
|
65
|
-
while (!condition()) {
|
|
66
|
-
if (Date.now() > deadline) throw new Error('condition not met in time');
|
|
67
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
describe('EventSubscriber', () => {
|
|
72
|
-
let fake: FakeHa;
|
|
73
|
-
let subscriber: EventSubscriber;
|
|
74
|
-
|
|
75
|
-
afterEach(async () => {
|
|
76
|
-
subscriber?.disconnect();
|
|
77
|
-
await fake?.close();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('matches results to commands by id, even when HA answers out of order', async () => {
|
|
81
|
-
fake = await fakeHa();
|
|
82
|
-
const pending: Array<Record<string, unknown>> = [];
|
|
83
|
-
fake.onCommand = (message, socket) => {
|
|
84
|
-
pending.push(message);
|
|
85
|
-
if (pending.length === 2) {
|
|
86
|
-
for (const m of [...pending].reverse()) {
|
|
87
|
-
socket.send(JSON.stringify({ id: m.id, type: 'result', success: true, result: { echo: m.type } }));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
92
|
-
await subscriber.connect();
|
|
93
|
-
|
|
94
|
-
const [a, b] = await Promise.all([subscriber.sendCommand({ type: 'first' }), subscriber.sendCommand({ type: 'second' })]);
|
|
95
|
-
|
|
96
|
-
expect(a).toEqual({ echo: 'first' });
|
|
97
|
-
expect(b).toEqual({ echo: 'second' });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('rejects a failed command with HA\'s error code only, never its free-text message', async () => {
|
|
101
|
-
fake = await fakeHa();
|
|
102
|
-
fake.onCommand = (message, socket) =>
|
|
103
|
-
socket.send(
|
|
104
|
-
JSON.stringify({
|
|
105
|
-
id: message.id,
|
|
106
|
-
type: 'result',
|
|
107
|
-
success: false,
|
|
108
|
-
error: { code: 'unauthorized', message: 'Denied for https://ha.internal:8123/api?token=SECRET123' },
|
|
109
|
-
})
|
|
110
|
-
);
|
|
111
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
112
|
-
|
|
113
|
-
const error = await subscriber.sendCommand({ type: 'trace/list' }).catch((e: Error) => e);
|
|
114
|
-
expect((error as Error).message).toBe("Home Assistant rejected 'trace/list' (unauthorized)");
|
|
115
|
-
expect((error as Error).message).not.toContain('SECRET123');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('never logs Home Assistant\'s free-text error message, at any level', async () => {
|
|
119
|
-
fake = await fakeHa();
|
|
120
|
-
fake.onCommand = (message, socket) =>
|
|
121
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: false, error: { code: 'unauthorized', message: 'see https://ha/api?token=SECRET123' } }));
|
|
122
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
123
|
-
const spies = (['debug', 'info', 'warn', 'error'] as const).map((level) => vi.spyOn(logger, level));
|
|
124
|
-
try {
|
|
125
|
-
await subscriber.sendCommand({ type: 'trace/list' }).catch(() => undefined);
|
|
126
|
-
for (const spy of spies) {
|
|
127
|
-
expect(JSON.stringify(spy.mock.calls)).not.toContain('SECRET123');
|
|
128
|
-
}
|
|
129
|
-
} finally {
|
|
130
|
-
for (const spy of spies) spy.mockRestore();
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('a subscription Home Assistant refuses is reported to the caller and dropped', async () => {
|
|
135
|
-
fake = await fakeHa();
|
|
136
|
-
fake.onCommand = (message, socket) =>
|
|
137
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: false, error: { code: 'unauthorized', message: 'Unauthorized' } }));
|
|
138
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
139
|
-
await subscriber.connect();
|
|
140
|
-
|
|
141
|
-
const error = await subscriber.subscribeEventType('mobile_app_notification_action', () => undefined).catch((e: Error) => e);
|
|
142
|
-
|
|
143
|
-
expect(error).toBeInstanceOf(HaCommandError);
|
|
144
|
-
expect((error as HaCommandError).code).toBe('unauthorized');
|
|
145
|
-
expect(subscriber.getSubscriptionCount()).toBe(0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('a refusal seen after a reconnect goes to onRejected and drops the subscription', async () => {
|
|
149
|
-
fake = await fakeHa();
|
|
150
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't', reconnectInterval: 20 });
|
|
151
|
-
const rejected: HaCommandError[] = [];
|
|
152
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', () => undefined, (e) => rejected.push(e));
|
|
153
|
-
|
|
154
|
-
// After the reconnect the token is no longer an admin.
|
|
155
|
-
fake.onCommand = (message, socket) =>
|
|
156
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: false, error: { code: 'unauthorized', message: 'no' } }));
|
|
157
|
-
fake.sockets[0].terminate();
|
|
158
|
-
|
|
159
|
-
await until(() => rejected.length === 1);
|
|
160
|
-
expect(rejected[0].code).toBe('unauthorized');
|
|
161
|
-
expect(subscriber.getSubscriptionCount()).toBe(0);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('a refusal after an offline start reaches onRejected', async () => {
|
|
165
|
-
const probe = await fakeHa();
|
|
166
|
-
const url = probe.url;
|
|
167
|
-
const port = Number(new URL(url).port);
|
|
168
|
-
await probe.close();
|
|
169
|
-
|
|
170
|
-
subscriber = new EventSubscriber({ baseUrl: url, token: 't', reconnectInterval: 20, maxReconnectDelay: 50 });
|
|
171
|
-
const rejected: HaCommandError[] = [];
|
|
172
|
-
// HA is down: queued, no error.
|
|
173
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', () => undefined, (e) => rejected.push(e));
|
|
174
|
-
|
|
175
|
-
// HA comes up with a non-admin token: the subscription is refused.
|
|
176
|
-
const wss = new WebSocketServer({ port });
|
|
177
|
-
fake = { url, received: [], sockets: [], onCommand: () => undefined, close: () => new Promise((r) => { for (const c of wss.clients) c.terminate(); wss.close(() => r()); }) };
|
|
178
|
-
wss.on('connection', (socket) => {
|
|
179
|
-
fake.sockets.push(socket);
|
|
180
|
-
socket.send(JSON.stringify({ type: 'auth_required' }));
|
|
181
|
-
socket.on('message', (raw) => {
|
|
182
|
-
const m = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
183
|
-
fake.received.push(m);
|
|
184
|
-
socket.send(JSON.stringify(m.type === 'auth' ? { type: 'auth_ok' } : { id: m.id, type: 'result', success: false, error: { code: 'unauthorized', message: 'no' } }));
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
await until(() => rejected.length === 1);
|
|
189
|
-
expect(rejected[0].code).toBe('unauthorized');
|
|
190
|
-
expect(subscriber.getSubscriptionCount()).toBe(0);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('delivers an event HA sends right after the confirmation, in the same read', async () => {
|
|
194
|
-
fake = await fakeHa();
|
|
195
|
-
fake.onCommand = (message, socket) => {
|
|
196
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: null }));
|
|
197
|
-
if (message.type === 'subscribe_events') {
|
|
198
|
-
// Same tick, back to back: the client reads both before any promise continuation.
|
|
199
|
-
sendEvent(socket, message.id as number, 'mobile_app_notification_action', { action: 'immediate' });
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
203
|
-
const seen: HaEvent[] = [];
|
|
204
|
-
|
|
205
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => seen.push(e));
|
|
206
|
-
|
|
207
|
-
await until(() => seen.length === 1);
|
|
208
|
-
expect(seen[0].data.action).toBe('immediate');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('a confirmation that arrives after the timeout does not leave a duplicate subscription on HA', async () => {
|
|
212
|
-
fake = await fakeHa();
|
|
213
|
-
const active = new Set<number>();
|
|
214
|
-
let subscribeCalls = 0;
|
|
215
|
-
fake.onCommand = (message, socket) => {
|
|
216
|
-
const reply = () => socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: null }));
|
|
217
|
-
if (message.type === 'subscribe_events') {
|
|
218
|
-
active.add(message.id as number); // HA registers it at once...
|
|
219
|
-
if (++subscribeCalls === 1) {
|
|
220
|
-
setTimeout(reply, 120); // ...but the first confirmation is late
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
} else if (message.type === 'unsubscribe_events') {
|
|
224
|
-
active.delete(message.subscription as number);
|
|
225
|
-
}
|
|
226
|
-
reply();
|
|
227
|
-
};
|
|
228
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't', commandTimeout: 40, resubscribeRetryDelay: 20 });
|
|
229
|
-
|
|
230
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', () => undefined);
|
|
231
|
-
await until(() => subscribeCalls === 2);
|
|
232
|
-
await new Promise((r) => setTimeout(r, 200)); // the late confirmation has arrived by now
|
|
233
|
-
|
|
234
|
-
expect(active.size).toBe(1);
|
|
235
|
-
const ids = fake.received.filter((m) => m.type === 'subscribe_events').map((m) => m.id);
|
|
236
|
-
expect(fake.received.filter((m) => m.type === 'unsubscribe_events').map((m) => m.subscription)).toEqual([ids[0]]);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('retries a subscription whose confirmation never arrived', async () => {
|
|
240
|
-
fake = await fakeHa();
|
|
241
|
-
let subscribeCalls = 0;
|
|
242
|
-
fake.onCommand = (message, socket) => {
|
|
243
|
-
if (message.type === 'subscribe_events' && ++subscribeCalls === 1) return; // lost
|
|
244
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: null }));
|
|
245
|
-
};
|
|
246
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't', commandTimeout: 50, resubscribeRetryDelay: 50 });
|
|
247
|
-
const seen: HaEvent[] = [];
|
|
248
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => seen.push(e));
|
|
249
|
-
|
|
250
|
-
await until(() => subscribeCalls === 2);
|
|
251
|
-
const confirmed = fake.received.filter((m) => m.type === 'subscribe_events')[1];
|
|
252
|
-
await new Promise((r) => setTimeout(r, 20));
|
|
253
|
-
sendEvent(fake.sockets[0], confirmed.id as number, 'mobile_app_notification_action', { action: 'ok' });
|
|
254
|
-
await until(() => seen.length === 1);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('replaces an unexpected error code with unknown_error', async () => {
|
|
258
|
-
fake = await fakeHa();
|
|
259
|
-
fake.onCommand = (message, socket) =>
|
|
260
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: false, error: { code: 'x?token=SECRET', message: 'boom' } }));
|
|
261
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
262
|
-
|
|
263
|
-
await expect(subscriber.sendCommand({ type: 'trace/list' })).rejects.toThrow("Home Assistant rejected 'trace/list' (unknown_error)");
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('reports a connection failure without the internal address', async () => {
|
|
267
|
-
fake = await fakeHa();
|
|
268
|
-
const url = fake.url;
|
|
269
|
-
await fake.close();
|
|
270
|
-
subscriber = new EventSubscriber({ baseUrl: url, token: 't', maxReconnectAttempts: 0 });
|
|
271
|
-
|
|
272
|
-
const error = await subscriber.connect().catch((e: Error) => e);
|
|
273
|
-
expect((error as Error).message).toBe('Could not connect to the Home Assistant WebSocket');
|
|
274
|
-
expect((error as Error).message).not.toContain('127.0.0.1');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('unsubscribing while HA is offline forgets the subscription for good', async () => {
|
|
278
|
-
fake = await fakeHa();
|
|
279
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't', reconnectInterval: 20 });
|
|
280
|
-
await subscriber.subscribeEventType('kept_event', () => undefined);
|
|
281
|
-
const gone = await subscriber.subscribeEventType('gone_event', () => undefined);
|
|
282
|
-
|
|
283
|
-
fake.sockets[0].terminate(); // HA goes away
|
|
284
|
-
await until(() => !subscriber.isConnected());
|
|
285
|
-
await expect(subscriber.unsubscribe(gone)).resolves.toBeUndefined();
|
|
286
|
-
expect(subscriber.getSubscriptionCount()).toBe(1);
|
|
287
|
-
|
|
288
|
-
// On reconnect only the remaining subscription is restored.
|
|
289
|
-
await until(() => fake.sockets.length === 2 && fake.received.filter((m) => m.type === 'subscribe_events').length === 3);
|
|
290
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
291
|
-
const restored = fake.received.filter((m) => m.type === 'subscribe_events').slice(2).map((m) => m.event_type);
|
|
292
|
-
expect(restored).toEqual(['kept_event']);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('a subscription cancelled before HA confirms it is cancelled on HA\'s side too', async () => {
|
|
296
|
-
fake = await fakeHa();
|
|
297
|
-
let confirm!: () => void;
|
|
298
|
-
fake.onCommand = (message, socket) => {
|
|
299
|
-
const reply = () => socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: null }));
|
|
300
|
-
if (message.type === 'subscribe_events') confirm = reply;
|
|
301
|
-
else reply();
|
|
302
|
-
};
|
|
303
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
304
|
-
await subscriber.connect();
|
|
305
|
-
|
|
306
|
-
const subscribing = subscriber.subscribeEventType('slow_event', () => undefined);
|
|
307
|
-
await until(() => fake.received.some((m) => m.type === 'subscribe_events'));
|
|
308
|
-
const id = [...(subscriber as unknown as { subscriptions: Map<string, unknown> }).subscriptions.keys()][0];
|
|
309
|
-
await subscriber.unsubscribe(id); // before HA confirmed
|
|
310
|
-
confirm();
|
|
311
|
-
await subscribing;
|
|
312
|
-
|
|
313
|
-
const sent = fake.received.find((m) => m.type === 'subscribe_events')!;
|
|
314
|
-
await until(() => fake.received.some((m) => m.type === 'unsubscribe_events'));
|
|
315
|
-
expect(fake.received.find((m) => m.type === 'unsubscribe_events')).toMatchObject({ subscription: sent.id });
|
|
316
|
-
expect(subscriber.getSubscriptionCount()).toBe(0);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it('delivers each event once per subscription when two subscribe to the same type', async () => {
|
|
320
|
-
fake = await fakeHa();
|
|
321
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't' });
|
|
322
|
-
const first: HaEvent[] = [];
|
|
323
|
-
const second: HaEvent[] = [];
|
|
324
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => first.push(e));
|
|
325
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => second.push(e));
|
|
326
|
-
|
|
327
|
-
// HA sends one copy per subscription, each tagged with that subscription's id.
|
|
328
|
-
const ids = fake.received.filter((m) => m.type === 'subscribe_events').map((m) => m.id as number);
|
|
329
|
-
for (const id of ids) sendEvent(fake.sockets[0], id, 'mobile_app_notification_action', { action: 'X' });
|
|
330
|
-
|
|
331
|
-
await until(() => first.length + second.length >= 2);
|
|
332
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
333
|
-
expect(first).toHaveLength(1);
|
|
334
|
-
expect(second).toHaveLength(1);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('restores its subscriptions after the connection drops', async () => {
|
|
338
|
-
fake = await fakeHa();
|
|
339
|
-
subscriber = new EventSubscriber({ baseUrl: fake.url, token: 't', reconnectInterval: 20 });
|
|
340
|
-
const seen: HaEvent[] = [];
|
|
341
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => seen.push(e));
|
|
342
|
-
|
|
343
|
-
fake.sockets[0].terminate(); // HA restarts
|
|
344
|
-
await until(() => fake.received.filter((m) => m.type === 'subscribe_events').length === 2);
|
|
345
|
-
|
|
346
|
-
const resubscribe = fake.received.filter((m) => m.type === 'subscribe_events')[1];
|
|
347
|
-
expect(resubscribe.event_type).toBe('mobile_app_notification_action');
|
|
348
|
-
sendEvent(fake.sockets[1], resubscribe.id as number, 'mobile_app_notification_action', { action: 'after-restart' });
|
|
349
|
-
await until(() => seen.length === 1);
|
|
350
|
-
expect(seen[0].data.action).toBe('after-restart');
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('a subscription made while HA is down goes live when HA comes up', async () => {
|
|
354
|
-
// Reserve a port, then free it: HA is "down" on that port at first.
|
|
355
|
-
const probe = await fakeHa();
|
|
356
|
-
const url = probe.url;
|
|
357
|
-
const port = Number(new URL(url).port);
|
|
358
|
-
await probe.close();
|
|
359
|
-
|
|
360
|
-
subscriber = new EventSubscriber({ baseUrl: url, token: 't', reconnectInterval: 20, maxReconnectDelay: 50 });
|
|
361
|
-
await expect(subscriber.connect()).rejects.toThrow();
|
|
362
|
-
const seen: HaEvent[] = [];
|
|
363
|
-
await subscriber.subscribeEventType('mobile_app_notification_action', (e) => seen.push(e));
|
|
364
|
-
|
|
365
|
-
// Home Assistant starts on the same port.
|
|
366
|
-
const wss = new WebSocketServer({ port });
|
|
367
|
-
fake = { url, received: [], sockets: [], onCommand: () => undefined, close: () => new Promise((r) => { for (const c of wss.clients) c.terminate(); wss.close(() => r()); }) };
|
|
368
|
-
wss.on('connection', (socket) => {
|
|
369
|
-
fake.sockets.push(socket);
|
|
370
|
-
socket.send(JSON.stringify({ type: 'auth_required' }));
|
|
371
|
-
socket.on('message', (raw) => {
|
|
372
|
-
const m = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
373
|
-
fake.received.push(m);
|
|
374
|
-
socket.send(JSON.stringify(m.type === 'auth' ? { type: 'auth_ok' } : { id: m.id, type: 'result', success: true, result: null }));
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
await until(() => fake.received.some((m) => m.type === 'subscribe_events'));
|
|
379
|
-
const sub = fake.received.find((m) => m.type === 'subscribe_events')!;
|
|
380
|
-
expect(sub.event_type).toBe('mobile_app_notification_action');
|
|
381
|
-
sendEvent(fake.sockets[0], sub.id as number, 'mobile_app_notification_action', { action: 'late' });
|
|
382
|
-
await until(() => seen.length === 1);
|
|
383
|
-
// Sent once, not again on top of an existing subscription.
|
|
384
|
-
expect(fake.received.filter((m) => m.type === 'subscribe_events')).toHaveLength(1);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('keeps retrying with a capped delay instead of giving up', async () => {
|
|
388
|
-
fake = await fakeHa();
|
|
389
|
-
const url = fake.url;
|
|
390
|
-
await fake.close();
|
|
391
|
-
subscriber = new EventSubscriber({ baseUrl: url, token: 't', reconnectInterval: 5, maxReconnectDelay: 20 });
|
|
392
|
-
|
|
393
|
-
await expect(subscriber.connect()).rejects.toThrow();
|
|
394
|
-
// Many more attempts than the old hard limit of 10, still trying.
|
|
395
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
396
|
-
expect((subscriber as unknown as { reconnectAttempts: number }).reconnectAttempts).toBeGreaterThan(10);
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
describe('createHaWebSocketCommand', () => {
|
|
401
|
-
let fake: FakeHa;
|
|
402
|
-
afterEach(async () => fake?.close());
|
|
403
|
-
|
|
404
|
-
it('fails within its deadline when Home Assistant never completes authentication', async () => {
|
|
405
|
-
fake = await fakeHa();
|
|
406
|
-
fake.sockets = [];
|
|
407
|
-
const silent = new WebSocketServer({ port: 0 });
|
|
408
|
-
await new Promise<void>((resolve) => silent.once('listening', () => resolve()));
|
|
409
|
-
try {
|
|
410
|
-
const command = createHaWebSocketCommand({
|
|
411
|
-
baseUrl: `http://127.0.0.1:${(silent.address() as AddressInfo).port}`,
|
|
412
|
-
token: 't',
|
|
413
|
-
timeoutMs: 200,
|
|
414
|
-
});
|
|
415
|
-
const started = Date.now();
|
|
416
|
-
await expect(command({ type: 'trace/list' })).rejects.toThrow(/timed out/i);
|
|
417
|
-
expect(Date.now() - started).toBeLessThan(2000);
|
|
418
|
-
} finally {
|
|
419
|
-
for (const client of silent.clients) client.terminate();
|
|
420
|
-
await new Promise<void>((resolve) => silent.close(() => resolve()));
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('fails at once when Home Assistant closes the connection before authentication', async () => {
|
|
425
|
-
fake = await fakeHa();
|
|
426
|
-
const closing = new WebSocketServer({ port: 0 });
|
|
427
|
-
await new Promise<void>((resolve) => closing.once('listening', () => resolve()));
|
|
428
|
-
closing.on('connection', (socket) => socket.close());
|
|
429
|
-
try {
|
|
430
|
-
const command = createHaWebSocketCommand({
|
|
431
|
-
baseUrl: `http://127.0.0.1:${(closing.address() as AddressInfo).port}`,
|
|
432
|
-
token: 't',
|
|
433
|
-
timeoutMs: 5000,
|
|
434
|
-
});
|
|
435
|
-
const started = Date.now();
|
|
436
|
-
await expect(command({ type: 'trace/list' })).rejects.toThrow(/before authentication/);
|
|
437
|
-
expect(Date.now() - started).toBeLessThan(2000);
|
|
438
|
-
} finally {
|
|
439
|
-
await new Promise<void>((resolve) => closing.close(() => resolve()));
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('connects, sends one command, returns its result and disconnects', async () => {
|
|
444
|
-
fake = await fakeHa();
|
|
445
|
-
fake.onCommand = (message, socket) =>
|
|
446
|
-
socket.send(JSON.stringify({ id: message.id, type: 'result', success: true, result: [{ run_id: 'r1' }] }));
|
|
447
|
-
|
|
448
|
-
const command = createHaWebSocketCommand({ baseUrl: fake.url, token: 't' });
|
|
449
|
-
const result = await command({ type: 'trace/list', domain: 'automation', item_id: '1700' });
|
|
450
|
-
|
|
451
|
-
expect(result).toEqual([{ run_id: 'r1' }]);
|
|
452
|
-
expect(fake.received.find((m) => m.type === 'trace/list')).toMatchObject({ domain: 'automation', item_id: '1700' });
|
|
453
|
-
await until(() => fake.sockets[0].readyState === fake.sockets[0].CLOSED);
|
|
454
|
-
});
|
|
455
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Home Assistant WebSocket connections follow HA_STRICT_SSL like the REST
|
|
3
|
-
* client: certificate verification is skipped only when strictSsl is false.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
-
|
|
8
|
-
const constructed: Array<{ url: string; options: Record<string, unknown> | undefined }> = [];
|
|
9
|
-
|
|
10
|
-
vi.mock('ws', async (importOriginal) => {
|
|
11
|
-
const actual = await importOriginal<typeof import('ws')>();
|
|
12
|
-
class RecordingWebSocket extends actual.WebSocket {
|
|
13
|
-
constructor(url: string, options?: Record<string, unknown>) {
|
|
14
|
-
constructed.push({ url, options });
|
|
15
|
-
super(url, options as never);
|
|
16
|
-
this.on('error', () => undefined);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return { ...actual, default: RecordingWebSocket, WebSocket: RecordingWebSocket };
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const { EventSubscriber } = await import('../../src/haClient/events.js');
|
|
23
|
-
const { createHaWebSocketCommand } = await import('../../src/haClient/wsCommand.js');
|
|
24
|
-
|
|
25
|
-
// Port 9 (discard) on localhost: the connection fails fast; only the options matter.
|
|
26
|
-
const HTTPS_HA = 'https://127.0.0.1:9';
|
|
27
|
-
|
|
28
|
-
describe('WebSocket TLS policy', () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
constructed.length = 0;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it.each([
|
|
34
|
-
[undefined, true],
|
|
35
|
-
[true, true],
|
|
36
|
-
[false, false],
|
|
37
|
-
])('strictSsl=%s -> rejectUnauthorized=%s (event subscriber)', async (strictSsl, expected) => {
|
|
38
|
-
const subscriber = new EventSubscriber({ baseUrl: HTTPS_HA, token: 't', strictSsl, maxReconnectAttempts: 0, connectTimeout: 500 });
|
|
39
|
-
await subscriber.connect().catch(() => undefined);
|
|
40
|
-
subscriber.disconnect();
|
|
41
|
-
|
|
42
|
-
expect(constructed[0].url).toBe('wss://127.0.0.1:9/api/websocket');
|
|
43
|
-
expect(constructed[0].options).toMatchObject({ rejectUnauthorized: expected });
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('the one-shot command used for traces passes the policy through', async () => {
|
|
47
|
-
const command = createHaWebSocketCommand({ baseUrl: HTTPS_HA, token: 't', strictSsl: false, timeoutMs: 500 });
|
|
48
|
-
await command({ type: 'trace/list' }).catch(() => undefined);
|
|
49
|
-
|
|
50
|
-
expect(constructed[0].options).toMatchObject({ rejectUnauthorized: false });
|
|
51
|
-
});
|
|
52
|
-
});
|