edilkamin 1.6.1 → 1.7.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.
@@ -1,10 +1,26 @@
1
1
  import { strict as assert } from "assert";
2
+ import * as amplifyAuth from "aws-amplify/auth";
2
3
  import axios from "axios";
4
+ import pako from "pako";
3
5
  import sinon from "sinon";
4
6
 
5
7
  import { configure, createAuthService } from "../src/library";
6
8
  import { API_URL } from "./constants";
7
9
 
10
+ /**
11
+ * Helper to create a gzip-compressed Buffer object for testing.
12
+ */
13
+ const createGzippedBuffer = (
14
+ data: unknown,
15
+ ): { type: "Buffer"; data: number[] } => {
16
+ const json = JSON.stringify(data);
17
+ const compressed = pako.gzip(json);
18
+ return {
19
+ type: "Buffer",
20
+ data: Array.from(compressed),
21
+ };
22
+ };
23
+
8
24
  describe("library", () => {
9
25
  let axiosStub: sinon.SinonStub;
10
26
  const expectedToken = "mockJwtToken";
@@ -22,7 +38,7 @@ describe("library", () => {
22
38
  });
23
39
 
24
40
  describe("signIn", () => {
25
- it("should sign in and return the JWT token", async () => {
41
+ it("should sign in and return the ID token by default", async () => {
26
42
  const expectedUsername = "testuser";
27
43
  const expectedPassword = "testpassword";
28
44
  const signIn = sinon.stub().resolves({ isSignedIn: true });
@@ -30,6 +46,7 @@ describe("library", () => {
30
46
  const fetchAuthSession = sinon.stub().resolves({
31
47
  tokens: {
32
48
  idToken: { toString: () => expectedToken },
49
+ accessToken: { toString: () => "accessToken" },
33
50
  },
34
51
  });
35
52
  const authStub = {
@@ -41,7 +58,7 @@ describe("library", () => {
41
58
  const authService = createAuthService(authStub as any);
42
59
  const token = await authService.signIn(
43
60
  expectedUsername,
44
- expectedPassword
61
+ expectedPassword,
45
62
  );
46
63
  assert.deepEqual(authStub.signOut.args, [[]]);
47
64
  assert.deepEqual(signIn.args, [
@@ -50,6 +67,32 @@ describe("library", () => {
50
67
  assert.equal(token, expectedToken);
51
68
  });
52
69
 
70
+ it("should sign in and return the access token in legacy mode", async () => {
71
+ const expectedUsername = "testuser";
72
+ const expectedPassword = "testpassword";
73
+ const signIn = sinon.stub().resolves({ isSignedIn: true });
74
+ const signOut = sinon.stub();
75
+ const fetchAuthSession = sinon.stub().resolves({
76
+ tokens: {
77
+ accessToken: { toString: () => expectedToken },
78
+ idToken: { toString: () => "idToken" },
79
+ },
80
+ });
81
+ const authStub = {
82
+ signIn,
83
+ signOut,
84
+ fetchAuthSession,
85
+ };
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const authService = createAuthService(authStub as any);
88
+ const token = await authService.signIn(
89
+ expectedUsername,
90
+ expectedPassword,
91
+ true, // legacy mode
92
+ );
93
+ assert.equal(token, expectedToken);
94
+ });
95
+
53
96
  it("should throw an error if sign-in fails", async () => {
54
97
  const expectedUsername = "testuser";
55
98
  const expectedPassword = "testpassword";
@@ -72,14 +115,90 @@ describe("library", () => {
72
115
  {
73
116
  name: "AssertionError",
74
117
  message: "Sign-in failed",
75
- }
118
+ },
76
119
  );
77
120
  });
78
121
  });
79
122
 
123
+ describe("getSession", () => {
124
+ it("should return idToken by default", async () => {
125
+ const mockAuth = {
126
+ signIn: sinon.stub().resolves({ isSignedIn: true }),
127
+ signOut: sinon.stub().resolves(),
128
+ fetchAuthSession: sinon.stub().resolves({
129
+ tokens: {
130
+ idToken: { toString: () => "mock-id-token" },
131
+ accessToken: { toString: () => "mock-access-token" },
132
+ },
133
+ }),
134
+ };
135
+ const { getSession, signIn } = createAuthService(
136
+ mockAuth as unknown as typeof amplifyAuth,
137
+ );
138
+ await signIn("user", "pass");
139
+ const token = await getSession();
140
+ assert.equal(token, "mock-id-token");
141
+ });
142
+
143
+ it("should return accessToken when legacy=true", async () => {
144
+ const mockAuth = {
145
+ signIn: sinon.stub().resolves({ isSignedIn: true }),
146
+ signOut: sinon.stub().resolves(),
147
+ fetchAuthSession: sinon.stub().resolves({
148
+ tokens: {
149
+ idToken: { toString: () => "mock-id-token" },
150
+ accessToken: { toString: () => "mock-access-token" },
151
+ },
152
+ }),
153
+ };
154
+ const { getSession, signIn } = createAuthService(
155
+ mockAuth as unknown as typeof amplifyAuth,
156
+ );
157
+ await signIn("user", "pass");
158
+ const token = await getSession(false, true);
159
+ assert.equal(token, "mock-access-token");
160
+ });
161
+
162
+ it("should throw error when no session exists", async () => {
163
+ const mockAuth = {
164
+ signIn: sinon.stub().resolves({ isSignedIn: true }),
165
+ signOut: sinon.stub().resolves(),
166
+ fetchAuthSession: sinon.stub().resolves({ tokens: null }),
167
+ };
168
+ const { getSession } = createAuthService(
169
+ mockAuth as unknown as typeof amplifyAuth,
170
+ );
171
+ await assert.rejects(async () => getSession(), {
172
+ name: "AssertionError",
173
+ message: "No session found - please sign in first",
174
+ });
175
+ });
176
+
177
+ it("should pass forceRefresh to fetchAuthSession", async () => {
178
+ const mockAuth = {
179
+ signIn: sinon.stub().resolves({ isSignedIn: true }),
180
+ signOut: sinon.stub().resolves(),
181
+ fetchAuthSession: sinon.stub().resolves({
182
+ tokens: {
183
+ idToken: { toString: () => "mock-id-token" },
184
+ accessToken: { toString: () => "mock-access-token" },
185
+ },
186
+ }),
187
+ };
188
+ const { getSession, signIn } = createAuthService(
189
+ mockAuth as unknown as typeof amplifyAuth,
190
+ );
191
+ await signIn("user", "pass");
192
+ await getSession(true);
193
+ assert.ok(mockAuth.fetchAuthSession.calledWith({ forceRefresh: true }));
194
+ });
195
+ });
196
+
80
197
  describe("configure", () => {
81
198
  const expectedApi = [
82
199
  "deviceInfo",
200
+ "registerDevice",
201
+ "editDevice",
83
202
  "setPower",
84
203
  "setPowerOff",
85
204
  "setPowerOn",
@@ -234,7 +353,7 @@ describe("library", () => {
234
353
  api: ReturnType<typeof configure>,
235
354
  token: string,
236
355
  mac: string,
237
- value: number
356
+ value: number,
238
357
  ) => api.setTargetTemperature(token, mac, value),
239
358
  payload: {
240
359
  name: "enviroment_1_temperature",
@@ -254,7 +373,7 @@ describe("library", () => {
254
373
  api,
255
374
  expectedToken,
256
375
  "mockMacAddress",
257
- payload.value
376
+ payload.value,
258
377
  );
259
378
 
260
379
  assert.deepEqual(mockAxios.put.args, [
@@ -273,4 +392,282 @@ describe("library", () => {
273
392
  });
274
393
  });
275
394
  });
395
+
396
+ describe("registerDevice", () => {
397
+ it("should call POST /device with correct payload", async () => {
398
+ const mockResponse = {
399
+ macAddress: "AABBCCDDEEFF",
400
+ deviceName: "Test Stove",
401
+ deviceRoom: "Living Room",
402
+ serialNumber: "EDK123",
403
+ };
404
+ const mockAxios = {
405
+ post: sinon.stub().resolves({ data: mockResponse }),
406
+ get: sinon.stub(),
407
+ put: sinon.stub(),
408
+ };
409
+ axiosStub.returns(mockAxios);
410
+ const api = configure("https://example.com/api");
411
+
412
+ const result = await api.registerDevice(
413
+ expectedToken,
414
+ "AA:BB:CC:DD:EE:FF",
415
+ "EDK123",
416
+ "Test Stove",
417
+ "Living Room",
418
+ );
419
+
420
+ assert.deepEqual(mockAxios.post.args, [
421
+ [
422
+ "device",
423
+ {
424
+ macAddress: "AABBCCDDEEFF",
425
+ deviceName: "Test Stove",
426
+ deviceRoom: "Living Room",
427
+ serialNumber: "EDK123",
428
+ },
429
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
430
+ ],
431
+ ]);
432
+ assert.deepEqual(result, mockResponse);
433
+ });
434
+
435
+ it("should normalize MAC address by removing colons", async () => {
436
+ const mockAxios = {
437
+ post: sinon.stub().resolves({ data: {} }),
438
+ get: sinon.stub(),
439
+ put: sinon.stub(),
440
+ };
441
+ axiosStub.returns(mockAxios);
442
+ const api = configure("https://example.com/api");
443
+
444
+ await api.registerDevice(expectedToken, "AA:BB:CC:DD:EE:FF", "EDK123");
445
+
446
+ assert.equal(mockAxios.post.args[0][1].macAddress, "AABBCCDDEEFF");
447
+ });
448
+
449
+ it("should use empty strings as defaults for name and room", async () => {
450
+ const mockAxios = {
451
+ post: sinon.stub().resolves({ data: {} }),
452
+ get: sinon.stub(),
453
+ put: sinon.stub(),
454
+ };
455
+ axiosStub.returns(mockAxios);
456
+ const api = configure("https://example.com/api");
457
+
458
+ await api.registerDevice(expectedToken, "AABBCCDDEEFF", "EDK123");
459
+
460
+ assert.equal(mockAxios.post.args[0][1].deviceName, "");
461
+ assert.equal(mockAxios.post.args[0][1].deviceRoom, "");
462
+ });
463
+ });
464
+
465
+ describe("editDevice", () => {
466
+ it("should call PUT /device/{mac} with correct payload", async () => {
467
+ const mockResponse = {
468
+ macAddress: "AABBCCDDEEFF",
469
+ deviceName: "Updated Name",
470
+ deviceRoom: "Basement",
471
+ serialNumber: "EDK123",
472
+ };
473
+ const mockAxios = {
474
+ put: sinon.stub().resolves({ data: mockResponse }),
475
+ get: sinon.stub(),
476
+ post: sinon.stub(),
477
+ };
478
+ axiosStub.returns(mockAxios);
479
+ const api = configure("https://example.com/api");
480
+
481
+ const result = await api.editDevice(
482
+ expectedToken,
483
+ "AA:BB:CC:DD:EE:FF",
484
+ "Updated Name",
485
+ "Basement",
486
+ );
487
+
488
+ assert.deepEqual(mockAxios.put.args, [
489
+ [
490
+ "device/AABBCCDDEEFF",
491
+ {
492
+ deviceName: "Updated Name",
493
+ deviceRoom: "Basement",
494
+ },
495
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
496
+ ],
497
+ ]);
498
+ assert.deepEqual(result, mockResponse);
499
+ });
500
+
501
+ it("should use empty strings as defaults for name and room", async () => {
502
+ const mockAxios = {
503
+ put: sinon.stub().resolves({ data: {} }),
504
+ get: sinon.stub(),
505
+ post: sinon.stub(),
506
+ };
507
+ axiosStub.returns(mockAxios);
508
+ const api = configure("https://example.com/api");
509
+
510
+ await api.editDevice(expectedToken, "AABBCCDDEEFF");
511
+
512
+ assert.equal(mockAxios.put.args[0][1].deviceName, "");
513
+ assert.equal(mockAxios.put.args[0][1].deviceRoom, "");
514
+ });
515
+ });
516
+
517
+ describe("deviceInfo with compressed responses", () => {
518
+ it("should decompress Buffer-encoded status field", async () => {
519
+ const statusData = {
520
+ commands: { power: true },
521
+ temperatures: { enviroment: 19, board: 25 },
522
+ };
523
+ const mockResponse = {
524
+ status: createGzippedBuffer(statusData),
525
+ nvm: {
526
+ user_parameters: {
527
+ enviroment_1_temperature: 22,
528
+ },
529
+ },
530
+ };
531
+
532
+ const mockAxios = {
533
+ get: sinon.stub().resolves({ data: mockResponse }),
534
+ };
535
+ axiosStub.returns(mockAxios);
536
+ const api = configure("https://example.com/api");
537
+
538
+ const result = await api.deviceInfo(expectedToken, "mockMacAddress");
539
+
540
+ assert.deepEqual(result.status, statusData);
541
+ });
542
+
543
+ it("should decompress Buffer-encoded nvm field", async () => {
544
+ const nvmData = {
545
+ user_parameters: {
546
+ enviroment_1_temperature: 22,
547
+ enviroment_2_temperature: 0,
548
+ enviroment_3_temperature: 0,
549
+ is_auto: false,
550
+ is_sound_active: true,
551
+ },
552
+ };
553
+ const mockResponse = {
554
+ status: {
555
+ commands: { power: true },
556
+ temperatures: { enviroment: 19 },
557
+ },
558
+ nvm: createGzippedBuffer(nvmData),
559
+ };
560
+
561
+ const mockAxios = {
562
+ get: sinon.stub().resolves({ data: mockResponse }),
563
+ };
564
+ axiosStub.returns(mockAxios);
565
+ const api = configure("https://example.com/api");
566
+
567
+ const result = await api.deviceInfo(expectedToken, "mockMacAddress");
568
+
569
+ assert.deepEqual(result.nvm, nvmData);
570
+ });
571
+
572
+ it("should handle fully compressed response (status and nvm)", async () => {
573
+ const statusData = {
574
+ commands: { power: false },
575
+ temperatures: { enviroment: 21, board: 30 },
576
+ };
577
+ const nvmData = {
578
+ user_parameters: {
579
+ enviroment_1_temperature: 20,
580
+ enviroment_2_temperature: 0,
581
+ enviroment_3_temperature: 0,
582
+ is_auto: true,
583
+ is_sound_active: false,
584
+ },
585
+ };
586
+ const mockResponse = {
587
+ status: createGzippedBuffer(statusData),
588
+ nvm: createGzippedBuffer(nvmData),
589
+ };
590
+
591
+ const mockAxios = {
592
+ get: sinon.stub().resolves({ data: mockResponse }),
593
+ };
594
+ axiosStub.returns(mockAxios);
595
+ const api = configure("https://example.com/api");
596
+
597
+ const result = await api.deviceInfo(expectedToken, "mockMacAddress");
598
+
599
+ assert.deepEqual(result.status, statusData);
600
+ assert.deepEqual(result.nvm, nvmData);
601
+ });
602
+
603
+ it("should work with getPower on compressed response", async () => {
604
+ const statusData = {
605
+ commands: { power: true },
606
+ temperatures: { enviroment: 19 },
607
+ };
608
+ const mockResponse = {
609
+ status: createGzippedBuffer(statusData),
610
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
611
+ };
612
+
613
+ const mockAxios = {
614
+ get: sinon.stub().resolves({ data: mockResponse }),
615
+ };
616
+ axiosStub.returns(mockAxios);
617
+ const api = configure("https://example.com/api");
618
+
619
+ const result = await api.getPower(expectedToken, "mockMacAddress");
620
+
621
+ assert.equal(result, true);
622
+ });
623
+
624
+ it("should work with getEnvironmentTemperature on compressed response", async () => {
625
+ const statusData = {
626
+ commands: { power: true },
627
+ temperatures: { enviroment: 19, board: 25 },
628
+ };
629
+ const mockResponse = {
630
+ status: createGzippedBuffer(statusData),
631
+ nvm: { user_parameters: { enviroment_1_temperature: 22 } },
632
+ };
633
+
634
+ const mockAxios = {
635
+ get: sinon.stub().resolves({ data: mockResponse }),
636
+ };
637
+ axiosStub.returns(mockAxios);
638
+ const api = configure("https://example.com/api");
639
+
640
+ const result = await api.getEnvironmentTemperature(
641
+ expectedToken,
642
+ "mockMacAddress",
643
+ );
644
+
645
+ assert.equal(result, 19);
646
+ });
647
+
648
+ it("should work with getTargetTemperature on compressed response", async () => {
649
+ const nvmData = {
650
+ user_parameters: {
651
+ enviroment_1_temperature: 22,
652
+ },
653
+ };
654
+ const mockResponse = {
655
+ status: { commands: { power: true }, temperatures: { enviroment: 19 } },
656
+ nvm: createGzippedBuffer(nvmData),
657
+ };
658
+
659
+ const mockAxios = {
660
+ get: sinon.stub().resolves({ data: mockResponse }),
661
+ };
662
+ axiosStub.returns(mockAxios);
663
+ const api = configure("https://example.com/api");
664
+
665
+ const result = await api.getTargetTemperature(
666
+ expectedToken,
667
+ "mockMacAddress",
668
+ );
669
+
670
+ assert.equal(result, 22);
671
+ });
672
+ });
276
673
  });