@digital-alchemy/hass 24.9.4 → 24.10.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/extensions/call-proxy.extension.d.ts +1 -1
  3. package/dist/extensions/call-proxy.extension.js +4 -1
  4. package/dist/extensions/call-proxy.extension.js.map +1 -1
  5. package/dist/extensions/entity.extension.js +3 -0
  6. package/dist/extensions/entity.extension.js.map +1 -1
  7. package/dist/extensions/websocket-api.extension.js +5 -11
  8. package/dist/extensions/websocket-api.extension.js.map +1 -1
  9. package/dist/helpers/notify.helper.d.ts +2 -2
  10. package/package.json +16 -14
  11. package/scripts/mock-assistant.sh +5 -0
  12. package/scripts/run-e2e.sh +7 -0
  13. package/scripts/test.sh +2 -0
  14. package/src/dynamic.ts +4254 -0
  15. package/src/extensions/area.extension.ts +118 -0
  16. package/src/extensions/backup.extension.ts +63 -0
  17. package/src/extensions/call-proxy.extension.ts +122 -0
  18. package/src/extensions/config.extension.ts +119 -0
  19. package/src/extensions/conversation.extension.ts +46 -0
  20. package/src/extensions/device.extension.ts +56 -0
  21. package/src/extensions/entity.extension.ts +347 -0
  22. package/src/extensions/events.extension.ts +25 -0
  23. package/src/extensions/fetch-api.extension.ts +269 -0
  24. package/src/extensions/floor.extension.ts +76 -0
  25. package/src/extensions/id-by.extension.ts +157 -0
  26. package/src/extensions/index.ts +16 -0
  27. package/src/extensions/internal.extension.ts +145 -0
  28. package/src/extensions/label.extension.ts +83 -0
  29. package/src/extensions/reference.extension.ts +330 -0
  30. package/src/extensions/registry.extension.ts +44 -0
  31. package/src/extensions/websocket-api.extension.ts +551 -0
  32. package/src/extensions/zone.extension.ts +69 -0
  33. package/src/hass.module.ts +217 -0
  34. package/src/helpers/backup.helper.ts +11 -0
  35. package/src/helpers/constants.helper.ts +30 -0
  36. package/src/helpers/device.helper.ts +25 -0
  37. package/src/helpers/entity-state.helper.ts +171 -0
  38. package/src/helpers/features.helper.ts +580 -0
  39. package/src/helpers/fetch/calendar.ts +54 -0
  40. package/src/helpers/fetch/configuration.ts +75 -0
  41. package/src/helpers/fetch/index.ts +5 -0
  42. package/src/helpers/fetch/server-log.ts +28 -0
  43. package/src/helpers/fetch/service-list.ts +64 -0
  44. package/src/helpers/fetch/weather-forecasts.ts +86 -0
  45. package/src/helpers/fetch.helper.ts +328 -0
  46. package/src/helpers/id-by.helper.ts +53 -0
  47. package/src/helpers/index.ts +13 -0
  48. package/src/helpers/interfaces.helper.ts +340 -0
  49. package/src/helpers/manifest.helper.ts +0 -0
  50. package/src/helpers/notify.helper.ts +302 -0
  51. package/src/helpers/registry.ts +281 -0
  52. package/src/helpers/utility.helper.ts +147 -0
  53. package/src/helpers/websocket.helper.ts +117 -0
  54. package/src/index.ts +5 -0
  55. package/src/mock_assistant/extensions/area.extension.ts +62 -0
  56. package/src/mock_assistant/extensions/config.extension.ts +33 -0
  57. package/src/mock_assistant/extensions/device.extension.ts +44 -0
  58. package/src/mock_assistant/extensions/entity-registry.extension.ts +41 -0
  59. package/src/mock_assistant/extensions/entity.extension.ts +114 -0
  60. package/src/mock_assistant/extensions/events.extension.ts +37 -0
  61. package/src/mock_assistant/extensions/fetch.extension.ts +3 -0
  62. package/src/mock_assistant/extensions/fixtures.extension.ts +79 -0
  63. package/src/mock_assistant/extensions/floor.extension.ts +64 -0
  64. package/src/mock_assistant/extensions/index.ts +12 -0
  65. package/src/mock_assistant/extensions/label.extension.ts +64 -0
  66. package/src/mock_assistant/extensions/services.extension.ts +25 -0
  67. package/src/mock_assistant/extensions/websocket-api.extension.ts +84 -0
  68. package/src/mock_assistant/extensions/zone.extension.ts +65 -0
  69. package/src/mock_assistant/helpers/fixtures.ts +22 -0
  70. package/src/mock_assistant/helpers/index.ts +1 -0
  71. package/src/mock_assistant/index.ts +3 -0
  72. package/src/mock_assistant/main.ts +46 -0
  73. package/src/mock_assistant/mock-assistant.module.ts +90 -0
  74. package/src/quickboot.module.ts +23 -0
  75. package/src/testing/area.spec.ts +189 -0
  76. package/src/testing/backup.spec.ts +157 -0
  77. package/src/testing/config.spec.ts +188 -0
  78. package/src/testing/device.spec.ts +89 -0
  79. package/src/testing/entity.spec.ts +171 -0
  80. package/src/testing/events.spec.ts +78 -0
  81. package/src/testing/fetch-api.spec.ts +410 -0
  82. package/src/testing/fixtures.spec.ts +158 -0
  83. package/src/testing/floor.spec.ts +186 -0
  84. package/src/testing/id-by.spec.ts +140 -0
  85. package/src/testing/label.spec.ts +186 -0
  86. package/src/testing/ref-by.spec.ts +300 -0
  87. package/src/testing/websocket.spec.ts +63 -0
  88. package/src/testing/workflow.spec.ts +195 -0
  89. package/src/testing/zone.spec.ts +109 -0
@@ -0,0 +1,90 @@
1
+ import { CreateLibrary, createModule, SINGLE } from "@digital-alchemy/core";
2
+ import { join } from "path";
3
+ import { cwd } from "process";
4
+
5
+ import { LIB_HASS } from "..";
6
+ import {
7
+ MockAreaExtension,
8
+ MockConfig,
9
+ MockDeviceExtension,
10
+ MockEntityExtension,
11
+ MockEntityRegistryExtension,
12
+ MockEvents,
13
+ MockFixtures,
14
+ MockFloorExtension,
15
+ MockLabelExtension,
16
+ MockServices,
17
+ MockWebsocketAPI,
18
+ MockZoneExtension,
19
+ } from "./extensions";
20
+
21
+ export const LIB_MOCK_ASSISTANT = CreateLibrary({
22
+ configuration: {
23
+ EMIT_SLEEP: {
24
+ default: SINGLE,
25
+ description: "Time in ms to wait after emitting entity update events",
26
+ type: "number",
27
+ },
28
+ FIXTURES_FILE: {
29
+ default: join(cwd(), "fixtures.json"),
30
+ description: [],
31
+ type: "string",
32
+ },
33
+ PASS_AUTH: {
34
+ default: true,
35
+ description: "Auto pass for auth challenges",
36
+ type: "boolean",
37
+ },
38
+ },
39
+ depends: [LIB_HASS],
40
+ name: "mock_assistant",
41
+ priorityInit: [
42
+ "socket",
43
+ "floor",
44
+ "device",
45
+ "area",
46
+ "label",
47
+ "config",
48
+ "entity",
49
+ "entity_registry",
50
+ "services",
51
+ "fixtures",
52
+ ],
53
+ services: {
54
+ area: MockAreaExtension,
55
+ config: MockConfig,
56
+ device: MockDeviceExtension,
57
+ entity: MockEntityExtension,
58
+ entity_registry: MockEntityRegistryExtension,
59
+ events: MockEvents,
60
+ fixtures: MockFixtures,
61
+ floor: MockFloorExtension,
62
+ label: MockLabelExtension,
63
+ services: MockServices,
64
+ socket: MockWebsocketAPI,
65
+ zone: MockZoneExtension,
66
+ },
67
+ });
68
+
69
+ declare module "@digital-alchemy/core" {
70
+ export interface LoadedModules {
71
+ mock_assistant: typeof LIB_MOCK_ASSISTANT;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @internal
77
+ *
78
+ * Make your own
79
+ */
80
+ export const createTestRunner = () =>
81
+ createModule
82
+ .fromLibrary(LIB_HASS)
83
+ .extend()
84
+ .toTest()
85
+ .configure({
86
+ boilerplate: { IS_TEST: true },
87
+ })
88
+ .appendLibrary(LIB_MOCK_ASSISTANT);
89
+
90
+ export const hassTestRunner = createTestRunner();
@@ -0,0 +1,23 @@
1
+ import { CreateApplication, TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { LIB_HASS } from ".";
4
+
5
+ /**
6
+ * Use from the node command line to probe apis
7
+ * Not for normal application usage
8
+ */
9
+ export async function QuickBoot(name: string) {
10
+ let out: TServiceParams;
11
+ const app = CreateApplication({
12
+ libraries: [LIB_HASS],
13
+ // @ts-expect-error fake app, name used for loading credentials
14
+ name,
15
+ services: {
16
+ Loader(params: TServiceParams) {
17
+ out = params;
18
+ },
19
+ },
20
+ });
21
+ await app.bootstrap();
22
+ return out;
23
+ }
@@ -0,0 +1,189 @@
1
+ import { sleep } from "@digital-alchemy/core";
2
+
3
+ import { TAreaId } from "../dynamic";
4
+ import { AREA_REGISTRY_UPDATED, AreaDetails } from "../helpers";
5
+ import { hassTestRunner, INTERNAL_MESSAGE } from "../mock_assistant";
6
+
7
+ describe("Area", () => {
8
+ const EXAMPLE_AREA = {
9
+ area_id: "empty_area" as TAreaId,
10
+ floor_id: null,
11
+ icon: null,
12
+ labels: [],
13
+ name: "Empty Area",
14
+ picture: null,
15
+ } as AreaDetails;
16
+
17
+ afterEach(async () => {
18
+ await hassTestRunner.teardown();
19
+ jest.restoreAllMocks();
20
+ });
21
+
22
+ describe("Lifecycle", () => {
23
+ it("should force values to be available before ready", async () => {
24
+ expect.assertions(1);
25
+
26
+ const app = await hassTestRunner.run(({ mock_assistant, lifecycle, hass }) => {
27
+ const spy = jest.fn();
28
+ mock_assistant.socket.connection.on(INTERNAL_MESSAGE, spy);
29
+ lifecycle.onReady(async () => {
30
+ await hass.area.list();
31
+ expect(spy).toHaveBeenCalledWith(
32
+ expect.objectContaining({ type: "config/area_registry/list" }),
33
+ );
34
+ }, -1);
35
+ });
36
+ await app.teardown();
37
+ });
38
+ });
39
+
40
+ describe("API", () => {
41
+ describe("Formatting", () => {
42
+ it("should call list properly", async () => {
43
+ expect.assertions(1);
44
+ await hassTestRunner.run(({ lifecycle, hass }) => {
45
+ const spy = jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
46
+ lifecycle.onReady(async () => {
47
+ await hass.area.list();
48
+ expect(spy).toHaveBeenCalledWith(
49
+ expect.objectContaining({ type: "config/area_registry/list" }),
50
+ );
51
+ });
52
+ });
53
+ });
54
+
55
+ it("should call update properly", async () => {
56
+ expect.assertions(1);
57
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
58
+ const spy = jest
59
+ .spyOn(hass.socket, "sendMessage")
60
+ .mockImplementation(async () => undefined);
61
+ lifecycle.onReady(async () => {
62
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
63
+ await hass.area.update(EXAMPLE_AREA);
64
+
65
+ expect(spy).toHaveBeenCalledWith({
66
+ type: "config/area_registry/update",
67
+ ...EXAMPLE_AREA,
68
+ });
69
+ });
70
+ });
71
+ });
72
+
73
+ it("should debounce updates properly", async () => {
74
+ expect.assertions(1);
75
+ await hassTestRunner.run(({ lifecycle, hass }) => {
76
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
77
+ let counter = 0;
78
+ hass.events.onAreaRegistryUpdate(() => counter++);
79
+ lifecycle.onReady(async () => {
80
+ setImmediate(async () => {
81
+ hass.socket.socketEvents.emit("area_registry_updated");
82
+ await sleep(5);
83
+ hass.socket.socketEvents.emit("area_registry_updated");
84
+ await sleep(5);
85
+ hass.socket.socketEvents.emit("area_registry_updated");
86
+ await sleep(75);
87
+ hass.socket.socketEvents.emit("area_registry_updated");
88
+ });
89
+ await sleep(200);
90
+ expect(counter).toBe(2);
91
+ });
92
+ });
93
+ });
94
+
95
+ it("should call delete properly", async () => {
96
+ expect.assertions(1);
97
+
98
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
99
+ const spy = jest
100
+ .spyOn(hass.socket, "sendMessage")
101
+ .mockImplementation(async () => undefined);
102
+ lifecycle.onReady(async () => {
103
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
104
+ await hass.area.delete(EXAMPLE_AREA.area_id);
105
+
106
+ expect(spy).toHaveBeenCalledWith({
107
+ area_id: "empty_area",
108
+ type: "config/area_registry/delete",
109
+ });
110
+ });
111
+ });
112
+ });
113
+
114
+ it("should call create properly", async () => {
115
+ expect.assertions(1);
116
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
117
+ const spy = jest
118
+ .spyOn(hass.socket, "sendMessage")
119
+ .mockImplementation(async () => undefined);
120
+ lifecycle.onReady(async () => {
121
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
122
+ await hass.area.create(EXAMPLE_AREA);
123
+
124
+ expect(spy).toHaveBeenCalledWith({
125
+ type: "config/area_registry/create",
126
+ ...EXAMPLE_AREA,
127
+ });
128
+ });
129
+ });
130
+ });
131
+ });
132
+
133
+ describe("Order of operations", () => {
134
+ it("should wait for an update before returning when updating", async () => {
135
+ expect.assertions(1);
136
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
137
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
138
+ lifecycle.onReady(async () => {
139
+ const response = hass.area.update(EXAMPLE_AREA);
140
+ let order = "";
141
+ setTimeout(() => {
142
+ order += "a";
143
+ event.emit(AREA_REGISTRY_UPDATED);
144
+ }, 5);
145
+ await response;
146
+ order += "b";
147
+ expect(order).toEqual("ab");
148
+ });
149
+ });
150
+ });
151
+
152
+ it("should wait for an update before returning when deleting", async () => {
153
+ expect.assertions(1);
154
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
155
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
156
+ lifecycle.onReady(async () => {
157
+ const response = hass.area.delete("example_area" as TAreaId);
158
+ let order = "";
159
+ setTimeout(() => {
160
+ order += "a";
161
+ event.emit(AREA_REGISTRY_UPDATED);
162
+ }, 5);
163
+ await response;
164
+ order += "b";
165
+ expect(order).toEqual("ab");
166
+ });
167
+ });
168
+ });
169
+
170
+ it("should wait for an update before returning when creating", async () => {
171
+ expect.assertions(1);
172
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
173
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
174
+ lifecycle.onReady(async () => {
175
+ const response = hass.area.create(EXAMPLE_AREA);
176
+ let order = "";
177
+ setTimeout(() => {
178
+ order += "a";
179
+ event.emit(AREA_REGISTRY_UPDATED);
180
+ }, 5);
181
+ await response;
182
+ order += "b";
183
+ expect(order).toEqual("ab");
184
+ });
185
+ });
186
+ });
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,157 @@
1
+ import { SINGLE } from "@digital-alchemy/core";
2
+
3
+ import { BackupResponse } from "../helpers";
4
+ import { hassTestRunner } from "../mock_assistant";
5
+
6
+ describe("Backup", () => {
7
+ afterEach(async () => {
8
+ await hassTestRunner.teardown();
9
+ jest.restoreAllMocks();
10
+ });
11
+
12
+ it("should format removes properly", async () => {
13
+ expect.assertions(1);
14
+ await hassTestRunner.run(({ lifecycle, hass }) => {
15
+ lifecycle.onReady(async () => {
16
+ const spy = jest.spyOn(hass.socket, "sendMessage");
17
+ await hass.backup.remove("test");
18
+ expect(spy).toHaveBeenCalledWith(
19
+ expect.objectContaining({
20
+ slug: "test",
21
+ type: "backup/remove",
22
+ }),
23
+ );
24
+ });
25
+ });
26
+ });
27
+
28
+ it("should format list requests properly", async () => {
29
+ expect.assertions(1);
30
+ await hassTestRunner.run(({ lifecycle, hass }) => {
31
+ lifecycle.onReady(async () => {
32
+ const spy = jest.spyOn(hass.socket, "sendMessage");
33
+ await hass.backup.list();
34
+ expect(spy).toHaveBeenCalledWith(
35
+ expect.objectContaining({
36
+ type: "backup/info",
37
+ }),
38
+ );
39
+ });
40
+ });
41
+ });
42
+
43
+ it("should first attempt to sign with downloads", async () => {
44
+ expect.assertions(1);
45
+ await hassTestRunner.run(({ lifecycle, hass }) => {
46
+ lifecycle.onReady(async () => {
47
+ const spy = jest
48
+ .spyOn(hass.socket, "sendMessage")
49
+ .mockImplementation(async () => undefined);
50
+ await hass.backup.download("test", "/foo/bar");
51
+ expect(spy).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ path: `/api/backup/download/test`,
54
+ type: "auth/sign_path",
55
+ }),
56
+ );
57
+ });
58
+ });
59
+ });
60
+
61
+ it("should use the sign path to download", async () => {
62
+ expect.assertions(1);
63
+ await hassTestRunner.run(({ lifecycle, hass }) => {
64
+ lifecycle.onReady(async () => {
65
+ const path = "/test/thing";
66
+ const destination = "/foo/bar";
67
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => ({ path }));
68
+ const spy = jest.spyOn(hass.fetch, "download").mockImplementation(async () => undefined);
69
+ await hass.backup.download("test", destination);
70
+ expect(spy).toHaveBeenCalledWith(destination, expect.objectContaining({ url: path }));
71
+ });
72
+ });
73
+ });
74
+
75
+ it("should use the sign path to download", async () => {
76
+ expect.assertions(1);
77
+ await hassTestRunner.run(({ lifecycle, hass }) => {
78
+ lifecycle.onReady(async () => {
79
+ const path = "/test/thing";
80
+ const destination = "/foo/bar";
81
+ jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => ({ path }));
82
+ const spy = jest.spyOn(hass.fetch, "download").mockImplementation(async () => undefined);
83
+ await hass.backup.download("test", destination);
84
+ expect(spy).toHaveBeenCalledWith(destination, expect.objectContaining({ url: path }));
85
+ });
86
+ });
87
+ });
88
+
89
+ it("should not start a new backup if one is already in progress", async () => {
90
+ expect.assertions(1);
91
+ await hassTestRunner.configure({ hass: { RETRY_INTERVAL: 0 } }).run(({ lifecycle, hass }) => {
92
+ lifecycle.onReady(async () => {
93
+ const spy = jest.spyOn(hass.socket, "sendMessage");
94
+ const responses = [
95
+ { backing_up: true, backups: [] },
96
+ { backing_up: false, backups: [] },
97
+ ] as BackupResponse[];
98
+
99
+ jest.spyOn(hass.backup, "list").mockImplementation(async () => responses.shift());
100
+
101
+ await hass.backup.generate();
102
+ expect(spy).not.toHaveBeenCalledWith({ type: "backup/generate" });
103
+ });
104
+ });
105
+ });
106
+
107
+ it("should start a new backup if one is not already in progress", async () => {
108
+ expect.assertions(1);
109
+ await hassTestRunner.configure({ hass: { RETRY_INTERVAL: 0 } }).run(({ lifecycle, hass }) => {
110
+ lifecycle.onReady(async () => {
111
+ const spy = jest.spyOn(hass.socket, "sendMessage");
112
+ const responses = [
113
+ { backing_up: false, backups: [] },
114
+ { backing_up: true, backups: [] },
115
+ { backing_up: false, backups: [] },
116
+ ] as BackupResponse[];
117
+
118
+ jest.spyOn(hass.backup, "list").mockImplementation(async () => responses.shift());
119
+
120
+ await hass.backup.generate();
121
+ expect(spy).toHaveBeenCalledWith(expect.objectContaining({ type: "backup/generate" }));
122
+ });
123
+ });
124
+ });
125
+
126
+ it("should confirm a new backup is in progress before monitoring", async () => {
127
+ expect.assertions(1);
128
+ await hassTestRunner.configure({ hass: { RETRY_INTERVAL: 0 } }).run(({ lifecycle, hass }) => {
129
+ lifecycle.onReady(async () => {
130
+ const responses = [
131
+ // not backing up
132
+ { backing_up: false, backups: [] },
133
+ // waiting...
134
+ { backing_up: false, backups: [] },
135
+ // waiting...
136
+ { backing_up: false, backups: [] },
137
+ // waiting...
138
+ { backing_up: false, backups: [] },
139
+ // true breaks out of loop confirm loop
140
+ { backing_up: true, backups: [] },
141
+ // false breaks out of actively backing up loop
142
+ { backing_up: false, backups: [] },
143
+ // should never be sent
144
+ { backing_up: false, backups: [] },
145
+ ] as BackupResponse[];
146
+ const length = responses.length;
147
+
148
+ const spy = jest
149
+ .spyOn(hass.backup, "list")
150
+ .mockImplementation(async () => responses.shift());
151
+
152
+ await hass.backup.generate();
153
+ expect(spy).toHaveBeenCalledTimes(length - SINGLE);
154
+ });
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,188 @@
1
+ import { env } from "process";
2
+
3
+ import { hassTestRunner } from "../mock_assistant";
4
+
5
+ const TOKEN = "DEFAULTS";
6
+
7
+ describe("Config", () => {
8
+ beforeEach(() => {
9
+ delete env.HASSIO_TOKEN;
10
+ delete env.SUPERVISOR_TOKEN;
11
+ delete env.HASS_SERVER;
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await hassTestRunner.teardown();
16
+ jest.restoreAllMocks();
17
+ });
18
+
19
+ describe("Auto", () => {
20
+ // # Should do nothing if variables do not exist
21
+ it("should do nothing if variables do not exist", async () => {
22
+ expect.assertions(2);
23
+ const BASE_URL = "http://localhost:9123";
24
+ await hassTestRunner.configure({ hass: { BASE_URL, TOKEN } }).run(({ lifecycle, config }) => {
25
+ lifecycle.onPostConfig(() => {
26
+ expect(config.hass.BASE_URL).toBe(BASE_URL);
27
+ expect(config.hass.TOKEN).toBe(TOKEN);
28
+ });
29
+ });
30
+ });
31
+
32
+ // # Should set BASE_URL & TOKEN if provided env
33
+ it("should set BASE_URL & TOKEN if provided env", async () => {
34
+ expect.assertions(2);
35
+ env.HASSIO_TOKEN = "FOO";
36
+ await hassTestRunner
37
+ .configure({
38
+ hass: { BASE_URL: "http://localhost:9123", TOKEN: TOKEN },
39
+ })
40
+ .run(({ lifecycle, config }) => {
41
+ lifecycle.onPostConfig(() => {
42
+ expect(config.hass.BASE_URL).toBe("http://supervisor/core");
43
+ expect(config.hass.TOKEN).toBe("FOO");
44
+ });
45
+ });
46
+ });
47
+
48
+ // # Should use HASSIO_TOKEN over SUPERVISOR_TOKEN
49
+ it("should use HASSIO_TOKEN over SUPERVISOR_TOKEN", async () => {
50
+ expect.assertions(1);
51
+ env.HASSIO_TOKEN = "FOO";
52
+ env.SUPERVISOR_TOKEN = "BAR";
53
+
54
+ await hassTestRunner.run(({ lifecycle, config }) => {
55
+ lifecycle.onPostConfig(() => {
56
+ expect(config.hass.TOKEN).toBe("FOO");
57
+ });
58
+ });
59
+ });
60
+
61
+ // # Should allow SUPERVISOR_TOKEN
62
+ it("should allow SUPERVISOR_TOKEN", async () => {
63
+ expect.assertions(1);
64
+ env.SUPERVISOR_TOKEN = "BAR";
65
+ await hassTestRunner.run(({ lifecycle, config }) => {
66
+ lifecycle.onPostConfig(() => {
67
+ expect(config.hass.TOKEN).toBe("BAR");
68
+ });
69
+ });
70
+ });
71
+
72
+ // # Should allow HASS_SERVER
73
+ it("should allow HASS_SERVER", async () => {
74
+ expect.assertions(2);
75
+ env.HASSIO_TOKEN = "FOO";
76
+ env.HASS_SERVER = "http://test/url";
77
+
78
+ await hassTestRunner.run(({ lifecycle, config }) => {
79
+ lifecycle.onPostConfig(() => {
80
+ expect(config.hass.TOKEN).toBe("FOO");
81
+ expect(config.hass.BASE_URL).toBe("http://test/url");
82
+ });
83
+ });
84
+ });
85
+ });
86
+
87
+ describe("Validate Config", () => {
88
+ // # Should not exit if not set
89
+ it("should not exit if not set", async () => {
90
+ expect.assertions(1);
91
+ const exitSpy = jest
92
+ .spyOn(process, "exit")
93
+ // @ts-expect-error testing
94
+ .mockImplementation(() => {});
95
+
96
+ await hassTestRunner
97
+ .configure({
98
+ hass: { TOKEN: "TEST" },
99
+ })
100
+ .run(() => {});
101
+ expect(exitSpy).not.toHaveBeenCalled();
102
+ });
103
+
104
+ // # Should info log on success
105
+ it("should info log on success", async () => {
106
+ const exitSpy = jest
107
+ .spyOn(process, "exit")
108
+ // @ts-expect-error testing
109
+ .mockImplementation(() => {});
110
+ let spy: jest.SpyInstance;
111
+
112
+ await hassTestRunner
113
+ .configure({
114
+ hass: { VALIDATE_CONFIGURATION: true },
115
+ })
116
+ .run(({ internal, hass }) => {
117
+ const logger = internal.boilerplate.logger.getBaseLogger();
118
+ spy = jest.spyOn(logger, "info").mockImplementation(() => {});
119
+ jest
120
+ .spyOn(hass.fetch, "checkCredentials")
121
+ .mockImplementation(async () => ({ message: "ok" }));
122
+ });
123
+
124
+ expect(exitSpy).toHaveBeenCalledWith(1);
125
+ expect(spy).toHaveBeenCalledWith("hass:configure", { name: "onPostConfig" }, "ok");
126
+ });
127
+
128
+ // # Should error log on bad token
129
+ it("should error log on bad token", async () => {
130
+ let spy: jest.SpyInstance;
131
+ const exitSpy = jest
132
+ .spyOn(process, "exit")
133
+ // @ts-expect-error testing
134
+ .mockImplementation(() => {});
135
+
136
+ await hassTestRunner
137
+ .configure({ hass: { TOKEN: "TEST", VALIDATE_CONFIGURATION: true } })
138
+ .run(({ internal, hass }) => {
139
+ const logger = internal.boilerplate.logger.getBaseLogger();
140
+ spy = jest.spyOn(logger, "error").mockImplementation(() => {});
141
+ jest
142
+ .spyOn(hass.fetch, "checkCredentials")
143
+ // anything that isn't the success works
144
+ .mockImplementation(async () => ({ message: "big_bad_error" }));
145
+ });
146
+
147
+ expect(exitSpy).toHaveBeenCalledWith(0);
148
+ expect(spy).toHaveBeenCalledWith(
149
+ "hass:configure",
150
+ { name: "onPostConfig" },
151
+ String({ message: "big_bad_error" }),
152
+ );
153
+ });
154
+
155
+ // # Should error log on bad url
156
+ it("should error log on bad url", async () => {
157
+ const error = new Error("BOOM");
158
+ let spy: jest.SpyInstance;
159
+ const exitSpy = jest
160
+ .spyOn(process, "exit")
161
+ // @ts-expect-error testing
162
+ .mockImplementation(() => {});
163
+ jest.spyOn(console, "log").mockImplementation(() => {});
164
+
165
+ jest.spyOn(console, "error").mockImplementation(() => {});
166
+
167
+ await hassTestRunner
168
+ .configure({ hass: { TOKEN: "TEST", VALIDATE_CONFIGURATION: true } })
169
+ .run(({ internal, hass }) => {
170
+ const logger = internal.boilerplate.logger.getBaseLogger();
171
+ spy = jest.spyOn(logger, "error").mockImplementation(() => {});
172
+ jest
173
+ .spyOn(hass.fetch, "checkCredentials")
174
+ // anything that isn't the success works
175
+ .mockImplementation(async () => {
176
+ throw error;
177
+ });
178
+ });
179
+
180
+ expect(exitSpy).toHaveBeenCalledWith(0);
181
+ expect(spy).toHaveBeenCalledWith(
182
+ "hass:configure",
183
+ { error, name: "onPostConfig" },
184
+ "failed to send request",
185
+ );
186
+ });
187
+ });
188
+ });