edilkamin 1.4.0 → 1.4.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.
@@ -2,8 +2,7 @@ name: Publish
2
2
 
3
3
  on:
4
4
  push:
5
- tags:
6
- - "*"
5
+ pull_request:
7
6
 
8
7
  jobs:
9
8
  build:
@@ -17,6 +16,8 @@ jobs:
17
16
  registry-url: "https://registry.npmjs.org"
18
17
  - run: yarn install
19
18
  - run: yarn build
19
+ - run: npm publish --dry-run
20
20
  - run: npm publish
21
+ if: startsWith(github.ref, 'refs/tags/')
21
22
  env:
22
23
  NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
@@ -19,4 +19,11 @@ jobs:
19
19
  - run: yarn install
20
20
  - run: yarn lint
21
21
  - run: yarn build
22
- - run: npm publish --dry-run
22
+ - run: yarn test
23
+ - uses: codecov/codecov-action@v5
24
+ with:
25
+ files: ./coverage/lcov.info
26
+ token: ${{ secrets.CODECOV_TOKEN }}
27
+ fail_ci_if_error: true
28
+ env:
29
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Tests](https://github.com/AndreMiras/edilkamin.js/workflows/Tests/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/tests.yml)
4
4
  [![CLI Tests](https://github.com/AndreMiras/edilkamin.js/actions/workflows/cli-tests.yml/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/cli-tests.yml)
5
+ [![codecov](https://codecov.io/gh/AndreMiras/edilkamin.js/graph/badge.svg?token=YG3LKXNZWU)](https://app.codecov.io/gh/AndreMiras/edilkamin.js/tree/main)
5
6
  [![Documentation](https://github.com/AndreMiras/edilkamin.js/workflows/Documentation/badge.svg)](https://github.com/AndreMiras/edilkamin.js/actions/workflows/documentation.yml)
6
7
  [![npm version](https://badge.fury.io/js/edilkamin.svg)](https://badge.fury.io/js/edilkamin)
7
8
 
package/dist/esm/cli.js CHANGED
@@ -8,10 +8,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
- import { signIn, configure } from "./library";
12
11
  import { Command } from "commander";
13
12
  import readline from "readline";
14
13
  import { version } from "../package.json";
14
+ import { configure, signIn } from "./library";
15
15
  const promptPassword = () => {
16
16
  const rl = readline.createInterface({
17
17
  input: process.stdin,
@@ -61,10 +61,11 @@ const createProgram = () => {
61
61
  .description("Retrieve device info for a specific MAC address")
62
62
  .requiredOption("-m, --mac <macAddress>", "MAC address of the device")).action((options) => __awaiter(void 0, void 0, void 0, function* () {
63
63
  const { username, password, mac } = options;
64
+ const normalizedMac = mac.replace(/:/g, "");
64
65
  const pwd = password || (yield promptPassword());
65
66
  const jwtToken = yield signIn(username, pwd);
66
67
  const api = configure(); // Use the default API configuration
67
- const deviceInfo = yield api.deviceInfo(jwtToken, mac);
68
+ const deviceInfo = yield api.deviceInfo(jwtToken, normalizedMac);
68
69
  console.log("Device Info:", deviceInfo.data);
69
70
  }));
70
71
  return program;
@@ -1,4 +1,4 @@
1
1
  export { API_URL } from "./constants";
2
+ export { configure, signIn } from "./library";
2
3
  export { CommandsType, DeviceInfoType, StatusType, TemperaturesType, UserParametersType, } from "./types";
3
- export { signIn, configure } from "./library";
4
4
  export declare const deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<import("./types").DeviceInfoType, any>>, setPower: (jwtToken: string, macAddress: string, value: number) => Promise<import("axios").AxiosResponse<any, any>>, setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any>>, setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any>>;
package/dist/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { configure } from "./library";
2
2
  export { API_URL } from "./constants";
3
- export { signIn, configure } from "./library";
3
+ export { configure, signIn } from "./library";
4
4
  export const { deviceInfo, setPower, setPowerOff, setPowerOn } = configure();
@@ -1,7 +1,21 @@
1
+ import * as amplifyAuth from "aws-amplify/auth";
1
2
  import { DeviceInfoType } from "./types";
2
3
  /**
3
- * Sign in to return the JWT token.
4
+ * Generates headers with a JWT token for authenticated requests.
5
+ * @param {string} jwtToken - The JWT token for authorization.
6
+ * @returns {object} - The headers object with the Authorization field.
4
7
  */
8
+ declare const headers: (jwtToken: string) => {
9
+ Authorization: string;
10
+ };
11
+ /**
12
+ * Creates an authentication service with sign-in functionality.
13
+ * @param {typeof amplifyAuth} auth - The authentication module to use.
14
+ * @returns {object} - An object containing authentication-related methods.
15
+ */
16
+ declare const createAuthService: (auth: typeof amplifyAuth) => {
17
+ signIn: (username: string, password: string) => Promise<string>;
18
+ };
5
19
  declare const signIn: (username: string, password: string) => Promise<string>;
6
20
  declare const configure: (baseURL?: string) => {
7
21
  deviceInfo: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<DeviceInfoType, any>>;
@@ -9,4 +23,4 @@ declare const configure: (baseURL?: string) => {
9
23
  setPowerOff: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any>>;
10
24
  setPowerOn: (jwtToken: string, macAddress: string) => Promise<import("axios").AxiosResponse<any, any>>;
11
25
  };
12
- export { signIn, configure };
26
+ export { configure, createAuthService, headers, signIn };
@@ -17,28 +17,54 @@ const amplifyconfiguration = {
17
17
  aws_user_pools_id: "eu-central-1_BYmQ2VBlo",
18
18
  aws_user_pools_web_client_id: "7sc1qltkqobo3ddqsk4542dg2h",
19
19
  };
20
- Amplify.configure(amplifyconfiguration);
20
+ /**
21
+ * Generates headers with a JWT token for authenticated requests.
22
+ * @param {string} jwtToken - The JWT token for authorization.
23
+ * @returns {object} - The headers object with the Authorization field.
24
+ */
21
25
  const headers = (jwtToken) => ({ Authorization: `Bearer ${jwtToken}` });
22
26
  /**
23
- * Sign in to return the JWT token.
27
+ * Configures Amplify if not already configured.
28
+ * Ensures the configuration is only applied once.
24
29
  */
25
- const signIn = (username, password) => __awaiter(void 0, void 0, void 0, function* () {
26
- // in case the user is already signed in, refs:
27
- // https://github.com/aws-amplify/amplify-js/issues/13813
28
- yield amplifyAuth.signOut();
29
- const { isSignedIn } = yield amplifyAuth.signIn({
30
- username,
31
- password,
30
+ const configureAmplify = () => {
31
+ const currentConfig = Amplify.getConfig();
32
+ if (Object.keys(currentConfig).length !== 0)
33
+ return;
34
+ Amplify.configure(amplifyconfiguration);
35
+ };
36
+ /**
37
+ * Creates an authentication service with sign-in functionality.
38
+ * @param {typeof amplifyAuth} auth - The authentication module to use.
39
+ * @returns {object} - An object containing authentication-related methods.
40
+ */
41
+ const createAuthService = (auth) => {
42
+ /**
43
+ * Signs in a user with the provided credentials.
44
+ * @param {string} username - The username of the user.
45
+ * @param {string} password - The password of the user.
46
+ * @returns {Promise<string>} - The JWT token of the signed-in user.
47
+ * @throws {Error} - If sign-in fails or no tokens are retrieved.
48
+ */
49
+ const signIn = (username, password) => __awaiter(void 0, void 0, void 0, function* () {
50
+ configureAmplify();
51
+ yield auth.signOut(); // Ensure the user is signed out first
52
+ const { isSignedIn } = yield auth.signIn({ username, password });
53
+ assert.ok(isSignedIn, "Sign-in failed");
54
+ const { tokens } = yield auth.fetchAuthSession();
55
+ assert.ok(tokens, "No tokens found");
56
+ return tokens.accessToken.toString();
32
57
  });
33
- assert.ok(isSignedIn);
34
- const { tokens } = yield amplifyAuth.fetchAuthSession();
35
- assert.ok(tokens);
36
- return tokens.accessToken.toString();
37
- });
58
+ return { signIn };
59
+ };
60
+ // Create the default auth service using amplifyAuth
61
+ const { signIn } = createAuthService(amplifyAuth);
38
62
  const deviceInfo = (axiosInstance) => (jwtToken, macAddress) => axiosInstance.get(`device/${macAddress}/info`, {
39
63
  headers: headers(jwtToken),
40
64
  });
41
- const mqttCommand = (axiosInstance) => (jwtToken, macAddress, payload) => axiosInstance.put("mqtt/command", Object.assign({ mac_address: macAddress }, payload), { headers: headers(jwtToken) });
65
+ const mqttCommand = (axiosInstance) =>
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ (jwtToken, macAddress, payload) => axiosInstance.put("mqtt/command", Object.assign({ mac_address: macAddress }, payload), { headers: headers(jwtToken) });
42
68
  const setPower = (axiosInstance) => (jwtToken, macAddress, value) => mqttCommand(axiosInstance)(jwtToken, macAddress, { name: "power", value });
43
69
  const setPowerOn = (axiosInstance) => (jwtToken, macAddress) => setPower(axiosInstance)(jwtToken, macAddress, 1);
44
70
  const setPowerOff = (axiosInstance) => (jwtToken, macAddress) => setPower(axiosInstance)(jwtToken, macAddress, 0);
@@ -55,4 +81,4 @@ const configure = (baseURL = API_URL) => {
55
81
  setPowerOn: setPowerOnInstance,
56
82
  };
57
83
  };
58
- export { signIn, configure };
84
+ export { configure, createAuthService, headers, signIn };
@@ -8,26 +8,99 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { strict as assert } from "assert";
11
- import sinon from "sinon";
12
11
  import axios from "axios";
13
- import { configure } from "../src/library";
12
+ import sinon from "sinon";
13
+ import { configure, createAuthService } from "../src/library";
14
+ import { API_URL } from "./constants";
14
15
  describe("library", () => {
15
16
  let axiosStub;
16
17
  beforeEach(() => {
17
18
  axiosStub = sinon.stub(axios, "create").returns({
18
19
  get: sinon.stub(),
19
20
  put: sinon.stub(),
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
22
  });
21
23
  });
22
24
  afterEach(() => {
23
25
  sinon.restore();
24
26
  });
27
+ describe("signIn", () => {
28
+ it("should sign in and return the JWT token", () => __awaiter(void 0, void 0, void 0, function* () {
29
+ const expectedUsername = "testuser";
30
+ const expectedPassword = "testpassword";
31
+ const expectedToken = "mockJwtToken";
32
+ const signIn = sinon.stub().resolves({ isSignedIn: true });
33
+ const signOut = sinon.stub();
34
+ const fetchAuthSession = sinon.stub().resolves({
35
+ tokens: {
36
+ accessToken: { toString: () => expectedToken },
37
+ },
38
+ });
39
+ const authStub = {
40
+ signIn,
41
+ signOut,
42
+ fetchAuthSession,
43
+ };
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ const authService = createAuthService(authStub);
46
+ const token = yield authService.signIn(expectedUsername, expectedPassword);
47
+ assert.deepEqual(authStub.signOut.args, [[]]);
48
+ assert.deepEqual(signIn.args, [
49
+ [{ username: expectedUsername, password: expectedPassword }],
50
+ ]);
51
+ assert.equal(token, expectedToken);
52
+ }));
53
+ it("should throw an error if sign-in fails", () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const expectedUsername = "testuser";
55
+ const expectedPassword = "testpassword";
56
+ const expectedToken = "mockJwtToken";
57
+ const signIn = sinon.stub().resolves({ isSignedIn: false });
58
+ const signOut = sinon.stub();
59
+ const fetchAuthSession = sinon.stub().resolves({
60
+ tokens: {
61
+ accessToken: { toString: () => expectedToken },
62
+ },
63
+ });
64
+ const authStub = {
65
+ signIn,
66
+ signOut,
67
+ fetchAuthSession,
68
+ };
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const authService = createAuthService(authStub);
71
+ yield assert.rejects(() => __awaiter(void 0, void 0, void 0, function* () { return authService.signIn(expectedUsername, expectedPassword); }), {
72
+ name: "AssertionError",
73
+ message: "Sign-in failed",
74
+ });
75
+ }));
76
+ });
25
77
  describe("configure", () => {
26
78
  it("should create API methods with the correct baseURL", () => {
27
79
  const baseURL = "https://example.com/api";
28
80
  const api = configure(baseURL);
29
- assert.ok(axiosStub.calledOnce);
30
- assert.deepEqual(axiosStub.firstCall.args[0], { baseURL });
81
+ assert.deepEqual(axiosStub.args, [
82
+ [
83
+ {
84
+ baseURL,
85
+ },
86
+ ],
87
+ ]);
88
+ assert.deepEqual(Object.keys(api), [
89
+ "deviceInfo",
90
+ "setPower",
91
+ "setPowerOff",
92
+ "setPowerOn",
93
+ ]);
94
+ });
95
+ it("should create API methods with the default baseURL", () => {
96
+ const api = configure();
97
+ assert.deepEqual(axiosStub.args, [
98
+ [
99
+ {
100
+ baseURL: API_URL,
101
+ },
102
+ ],
103
+ ]);
31
104
  assert.deepEqual(Object.keys(api), [
32
105
  "deviceInfo",
33
106
  "setPower",
@@ -38,39 +111,58 @@ describe("library", () => {
38
111
  });
39
112
  describe("API Methods", () => {
40
113
  it("should call axios for deviceInfo", () => __awaiter(void 0, void 0, void 0, function* () {
114
+ const expectedDevice = { id: "123", name: "Mock Device" };
115
+ const expectedToken = "mockToken";
41
116
  const mockAxios = {
42
- get: sinon
43
- .stub()
44
- .resolves({ data: { id: "123", name: "Mock Device" } }),
117
+ get: sinon.stub().resolves({ data: expectedDevice }),
45
118
  };
46
119
  axiosStub.returns(mockAxios);
47
120
  const api = configure("https://example.com/api");
48
- const result = yield api.deviceInfo("mockToken", "mockMacAddress");
49
- assert.ok(mockAxios.get.calledOnce);
50
- assert.equal(mockAxios.get.firstCall.args[0], "device/mockMacAddress/info");
51
- assert.deepEqual(mockAxios.get.firstCall.args[1], {
52
- headers: { Authorization: "Bearer mockToken" },
53
- });
54
- assert.deepEqual(result.data, { id: "123", name: "Mock Device" });
55
- }));
56
- it("should call axios for setPowerOn", () => __awaiter(void 0, void 0, void 0, function* () {
57
- const mockAxios = {
58
- put: sinon.stub().resolves({ status: 200 }),
59
- };
60
- axiosStub.returns(mockAxios);
61
- const api = configure("https://example.com/api");
62
- const result = yield api.setPowerOn("mockToken", "mockMacAddress");
63
- assert.ok(mockAxios.put.calledOnce);
64
- assert.equal(mockAxios.put.firstCall.args[0], "mqtt/command");
65
- assert.deepEqual(mockAxios.put.firstCall.args[1], {
66
- mac_address: "mockMacAddress",
67
- name: "power",
68
- value: 1,
69
- });
70
- assert.deepEqual(mockAxios.put.firstCall.args[2], {
71
- headers: { Authorization: "Bearer mockToken" },
72
- });
73
- assert.equal(result.status, 200);
121
+ const result = yield api.deviceInfo(expectedToken, "mockMacAddress");
122
+ assert.deepEqual(mockAxios.get.args, [
123
+ [
124
+ "device/mockMacAddress/info",
125
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
126
+ ],
127
+ ]);
128
+ assert.deepEqual(result.data, expectedDevice);
74
129
  }));
130
+ // Tests for setPowerOn and setPowerOff
131
+ [
132
+ {
133
+ method: "setPowerOn",
134
+ call: (api) => api.setPowerOn("mockToken", "mockMacAddress"),
135
+ expectedValue: 1,
136
+ },
137
+ {
138
+ method: "setPowerOff",
139
+ call: (api) => api.setPowerOff("mockToken", "mockMacAddress"),
140
+ expectedValue: 0,
141
+ },
142
+ ].forEach(({ method, call, expectedValue }) => {
143
+ it(`should call axios for ${method}`, () => __awaiter(void 0, void 0, void 0, function* () {
144
+ const mockAxios = {
145
+ put: sinon.stub().resolves({ status: 200 }),
146
+ };
147
+ axiosStub.returns(mockAxios);
148
+ const api = configure("https://example.com/api");
149
+ // Invoke the method using the mapped call function
150
+ const result = yield call(api);
151
+ assert.deepEqual(mockAxios.put.args, [
152
+ [
153
+ "mqtt/command",
154
+ {
155
+ mac_address: "mockMacAddress",
156
+ name: "power",
157
+ value: expectedValue,
158
+ },
159
+ {
160
+ headers: { Authorization: "Bearer mockToken" },
161
+ },
162
+ ],
163
+ ]);
164
+ assert.equal(result.status, 200);
165
+ }));
166
+ });
75
167
  });
76
168
  });
@@ -0,0 +1,35 @@
1
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import simpleImportSort from "eslint-plugin-simple-import-sort";
3
+ import globals from "globals";
4
+ import tsParser from "@typescript-eslint/parser";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import js from "@eslint/js";
8
+ import { FlatCompat } from "@eslint/eslintrc";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ const compat = new FlatCompat({
14
+ baseDirectory: __dirname,
15
+ recommendedConfig: js.configs.recommended,
16
+ allConfig: js.configs.all,
17
+ });
18
+
19
+ export default [
20
+ ...compat.extends(
21
+ "eslint:recommended",
22
+ "plugin:@typescript-eslint/recommended"
23
+ ),
24
+ {
25
+ plugins: {
26
+ "@typescript-eslint": typescriptEslint,
27
+ "simple-import-sort": simpleImportSort,
28
+ },
29
+ rules: {
30
+ // Sorting imports and exports
31
+ "simple-import-sort/imports": "error",
32
+ "simple-import-sort/exports": "error",
33
+ },
34
+ },
35
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edilkamin",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -8,10 +8,14 @@
8
8
  "scripts": {
9
9
  "cli": "ts-node src/cli.ts",
10
10
  "cli:debug": "node --inspect --require ts-node/register/transpile-only src/cli.ts",
11
- "test": "mocha --require ts-node/register src/*.test.ts",
12
- "test:debug": "mocha --require ts-node/register/transpile-only --inspect src/*.test.ts",
13
- "lint": "prettier --check src docs .github *.md *.json",
14
- "format": "prettier --write src docs .github *.md *.json",
11
+ "test": "nyc mocha --require ts-node/register src/*.test.ts",
12
+ "test:debug": "nyc mocha --require ts-node/register/transpile-only --inspect src/*.test.ts",
13
+ "lint:prettier": "prettier --check src docs .github *.json *.md *.mjs",
14
+ "format:prettier": "prettier --write src docs .github *.json *.md *.mjs",
15
+ "lint:eslint": "eslint src",
16
+ "format:eslint": "eslint --fix src",
17
+ "lint": "yarn lint:prettier && yarn lint:eslint",
18
+ "format": "yarn format:prettier && yarn format:eslint",
15
19
  "build:cjs": "tsc -p tsconfig.cjs.json",
16
20
  "build:esm": "tsc -p tsconfig.esm.json",
17
21
  "build": "npm run build:cjs && npm run build:esm"
@@ -29,15 +33,31 @@
29
33
  "bin": {
30
34
  "edilkamin": "dist/cjs/cli.js"
31
35
  },
36
+ "nyc": {
37
+ "reporter": [
38
+ "html",
39
+ "lcov",
40
+ "text"
41
+ ]
42
+ },
32
43
  "dependencies": {
33
44
  "aws-amplify": "^6.10.0",
34
45
  "axios": "^0.26.0"
35
46
  },
36
47
  "devDependencies": {
37
48
  "@aws-amplify/cli": "^7.6.21",
49
+ "@eslint/eslintrc": "^3.2.0",
50
+ "@eslint/js": "^9.16.0",
38
51
  "@types/mocha": "^10.0.10",
39
52
  "@types/sinon": "^17.0.3",
53
+ "@typescript-eslint/eslint-plugin": "^8.17.0",
54
+ "@typescript-eslint/parser": "^8.17.0",
55
+ "eslint": "^9.16.0",
56
+ "eslint-config-prettier": "^9.1.0",
57
+ "eslint-plugin-prettier": "^5.2.1",
58
+ "eslint-plugin-simple-import-sort": "^12.1.1",
40
59
  "mocha": "^10.8.2",
60
+ "nyc": "^17.1.0",
41
61
  "prettier": "^2.5.1",
42
62
  "sinon": "^19.0.2",
43
63
  "ts-node": "^10.9.1",
package/src/cli.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { signIn, configure } from "./library";
3
2
  import { Command } from "commander";
4
3
  import readline from "readline";
4
+
5
5
  import { version } from "../package.json";
6
+ import { configure, signIn } from "./library";
6
7
 
7
8
  const promptPassword = (): Promise<string> => {
8
9
  const rl = readline.createInterface({
@@ -59,10 +60,11 @@ const createProgram = (): Command => {
59
60
  .requiredOption("-m, --mac <macAddress>", "MAC address of the device")
60
61
  ).action(async (options) => {
61
62
  const { username, password, mac } = options;
63
+ const normalizedMac = mac.replace(/:/g, "");
62
64
  const pwd = password || (await promptPassword());
63
65
  const jwtToken = await signIn(username, pwd);
64
66
  const api = configure(); // Use the default API configuration
65
- const deviceInfo = await api.deviceInfo(jwtToken, mac);
67
+ const deviceInfo = await api.deviceInfo(jwtToken, normalizedMac);
66
68
  console.log("Device Info:", deviceInfo.data);
67
69
  });
68
70
  return program;
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { configure } from "./library";
2
2
 
3
3
  export { API_URL } from "./constants";
4
-
4
+ export { configure, signIn } from "./library";
5
5
  export {
6
6
  CommandsType,
7
7
  DeviceInfoType,
@@ -10,6 +10,4 @@ export {
10
10
  UserParametersType,
11
11
  } from "./types";
12
12
 
13
- export { signIn, configure } from "./library";
14
-
15
13
  export const { deviceInfo, setPower, setPowerOff, setPowerOn } = configure();
@@ -1,7 +1,9 @@
1
1
  import { strict as assert } from "assert";
2
- import sinon from "sinon";
3
2
  import axios from "axios";
4
- import { configure } from "../src/library";
3
+ import sinon from "sinon";
4
+
5
+ import { configure, createAuthService } from "../src/library";
6
+ import { API_URL } from "./constants";
5
7
 
6
8
  describe("library", () => {
7
9
  let axiosStub: sinon.SinonStub;
@@ -10,6 +12,7 @@ describe("library", () => {
10
12
  axiosStub = sinon.stub(axios, "create").returns({
11
13
  get: sinon.stub(),
12
14
  put: sinon.stub(),
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
16
  } as any);
14
17
  });
15
18
 
@@ -17,12 +20,91 @@ describe("library", () => {
17
20
  sinon.restore();
18
21
  });
19
22
 
23
+ describe("signIn", () => {
24
+ it("should sign in and return the JWT token", async () => {
25
+ const expectedUsername = "testuser";
26
+ const expectedPassword = "testpassword";
27
+ const expectedToken = "mockJwtToken";
28
+ const signIn = sinon.stub().resolves({ isSignedIn: true });
29
+ const signOut = sinon.stub();
30
+ const fetchAuthSession = sinon.stub().resolves({
31
+ tokens: {
32
+ accessToken: { toString: () => expectedToken },
33
+ },
34
+ });
35
+ const authStub = {
36
+ signIn,
37
+ signOut,
38
+ fetchAuthSession,
39
+ };
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const authService = createAuthService(authStub as any);
42
+ const token = await authService.signIn(
43
+ expectedUsername,
44
+ expectedPassword
45
+ );
46
+ assert.deepEqual(authStub.signOut.args, [[]]);
47
+ assert.deepEqual(signIn.args, [
48
+ [{ username: expectedUsername, password: expectedPassword }],
49
+ ]);
50
+ assert.equal(token, expectedToken);
51
+ });
52
+
53
+ it("should throw an error if sign-in fails", async () => {
54
+ const expectedUsername = "testuser";
55
+ const expectedPassword = "testpassword";
56
+ const expectedToken = "mockJwtToken";
57
+ const signIn = sinon.stub().resolves({ isSignedIn: false });
58
+ const signOut = sinon.stub();
59
+ const fetchAuthSession = sinon.stub().resolves({
60
+ tokens: {
61
+ accessToken: { toString: () => expectedToken },
62
+ },
63
+ });
64
+ const authStub = {
65
+ signIn,
66
+ signOut,
67
+ fetchAuthSession,
68
+ };
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ const authService = createAuthService(authStub as any);
71
+ await assert.rejects(
72
+ async () => authService.signIn(expectedUsername, expectedPassword),
73
+ {
74
+ name: "AssertionError",
75
+ message: "Sign-in failed",
76
+ }
77
+ );
78
+ });
79
+ });
80
+
20
81
  describe("configure", () => {
21
82
  it("should create API methods with the correct baseURL", () => {
22
83
  const baseURL = "https://example.com/api";
23
84
  const api = configure(baseURL);
24
- assert.ok(axiosStub.calledOnce);
25
- assert.deepEqual(axiosStub.firstCall.args[0], { baseURL });
85
+ assert.deepEqual(axiosStub.args, [
86
+ [
87
+ {
88
+ baseURL,
89
+ },
90
+ ],
91
+ ]);
92
+ assert.deepEqual(Object.keys(api), [
93
+ "deviceInfo",
94
+ "setPower",
95
+ "setPowerOff",
96
+ "setPowerOn",
97
+ ]);
98
+ });
99
+ it("should create API methods with the default baseURL", () => {
100
+ const api = configure();
101
+ assert.deepEqual(axiosStub.args, [
102
+ [
103
+ {
104
+ baseURL: API_URL,
105
+ },
106
+ ],
107
+ ]);
26
108
  assert.deepEqual(Object.keys(api), [
27
109
  "deviceInfo",
28
110
  "setPower",
@@ -34,43 +116,62 @@ describe("library", () => {
34
116
 
35
117
  describe("API Methods", () => {
36
118
  it("should call axios for deviceInfo", async () => {
119
+ const expectedDevice = { id: "123", name: "Mock Device" };
120
+ const expectedToken = "mockToken";
37
121
  const mockAxios = {
38
- get: sinon
39
- .stub()
40
- .resolves({ data: { id: "123", name: "Mock Device" } }),
122
+ get: sinon.stub().resolves({ data: expectedDevice }),
41
123
  };
42
- axiosStub.returns(mockAxios as any);
124
+ axiosStub.returns(mockAxios);
43
125
  const api = configure("https://example.com/api");
44
- const result = await api.deviceInfo("mockToken", "mockMacAddress");
45
- assert.ok(mockAxios.get.calledOnce);
46
- assert.equal(
47
- mockAxios.get.firstCall.args[0],
48
- "device/mockMacAddress/info"
49
- );
50
- assert.deepEqual(mockAxios.get.firstCall.args[1], {
51
- headers: { Authorization: "Bearer mockToken" },
52
- });
53
- assert.deepEqual(result.data, { id: "123", name: "Mock Device" });
126
+ const result = await api.deviceInfo(expectedToken, "mockMacAddress");
127
+ assert.deepEqual(mockAxios.get.args, [
128
+ [
129
+ "device/mockMacAddress/info",
130
+ { headers: { Authorization: `Bearer ${expectedToken}` } },
131
+ ],
132
+ ]);
133
+ assert.deepEqual(result.data, expectedDevice);
54
134
  });
55
135
 
56
- it("should call axios for setPowerOn", async () => {
57
- const mockAxios = {
58
- put: sinon.stub().resolves({ status: 200 }),
59
- };
60
- axiosStub.returns(mockAxios as any);
61
- const api = configure("https://example.com/api");
62
- const result = await api.setPowerOn("mockToken", "mockMacAddress");
63
- assert.ok(mockAxios.put.calledOnce);
64
- assert.equal(mockAxios.put.firstCall.args[0], "mqtt/command");
65
- assert.deepEqual(mockAxios.put.firstCall.args[1], {
66
- mac_address: "mockMacAddress",
67
- name: "power",
68
- value: 1,
69
- });
70
- assert.deepEqual(mockAxios.put.firstCall.args[2], {
71
- headers: { Authorization: "Bearer mockToken" },
136
+ // Tests for setPowerOn and setPowerOff
137
+ [
138
+ {
139
+ method: "setPowerOn",
140
+ call: (api: ReturnType<typeof configure>) =>
141
+ api.setPowerOn("mockToken", "mockMacAddress"),
142
+ expectedValue: 1,
143
+ },
144
+ {
145
+ method: "setPowerOff",
146
+ call: (api: ReturnType<typeof configure>) =>
147
+ api.setPowerOff("mockToken", "mockMacAddress"),
148
+ expectedValue: 0,
149
+ },
150
+ ].forEach(({ method, call, expectedValue }) => {
151
+ it(`should call axios for ${method}`, async () => {
152
+ const mockAxios = {
153
+ put: sinon.stub().resolves({ status: 200 }),
154
+ };
155
+ axiosStub.returns(mockAxios);
156
+ const api = configure("https://example.com/api");
157
+
158
+ // Invoke the method using the mapped call function
159
+ const result = await call(api);
160
+ assert.deepEqual(mockAxios.put.args, [
161
+ [
162
+ "mqtt/command",
163
+ {
164
+ mac_address: "mockMacAddress",
165
+ name: "power",
166
+ value: expectedValue,
167
+ },
168
+ {
169
+ headers: { Authorization: "Bearer mockToken" },
170
+ },
171
+ ],
172
+ ]);
173
+ assert.equal(result.status, 200);
72
174
  });
73
- assert.equal(result.status, 200);
74
175
  });
75
176
  });
76
177
  });
package/src/library.ts CHANGED
@@ -2,35 +2,64 @@ import { strict as assert } from "assert";
2
2
  import { Amplify } from "aws-amplify";
3
3
  import * as amplifyAuth from "aws-amplify/auth";
4
4
  import axios, { AxiosInstance } from "axios";
5
- import { DeviceInfoType } from "./types";
5
+
6
6
  import { API_URL } from "./constants";
7
+ import { DeviceInfoType } from "./types";
7
8
 
8
9
  const amplifyconfiguration = {
9
10
  aws_project_region: "eu-central-1",
10
11
  aws_user_pools_id: "eu-central-1_BYmQ2VBlo",
11
12
  aws_user_pools_web_client_id: "7sc1qltkqobo3ddqsk4542dg2h",
12
13
  };
13
- Amplify.configure(amplifyconfiguration);
14
14
 
15
+ /**
16
+ * Generates headers with a JWT token for authenticated requests.
17
+ * @param {string} jwtToken - The JWT token for authorization.
18
+ * @returns {object} - The headers object with the Authorization field.
19
+ */
15
20
  const headers = (jwtToken: string) => ({ Authorization: `Bearer ${jwtToken}` });
16
21
 
17
22
  /**
18
- * Sign in to return the JWT token.
23
+ * Configures Amplify if not already configured.
24
+ * Ensures the configuration is only applied once.
19
25
  */
20
- const signIn = async (username: string, password: string): Promise<string> => {
21
- // in case the user is already signed in, refs:
22
- // https://github.com/aws-amplify/amplify-js/issues/13813
23
- await amplifyAuth.signOut();
24
- const { isSignedIn } = await amplifyAuth.signIn({
25
- username,
26
- password,
27
- });
28
- assert.ok(isSignedIn);
29
- const { tokens } = await amplifyAuth.fetchAuthSession();
30
- assert.ok(tokens);
31
- return tokens.accessToken.toString();
26
+ const configureAmplify = () => {
27
+ const currentConfig = Amplify.getConfig();
28
+ if (Object.keys(currentConfig).length !== 0) return;
29
+ Amplify.configure(amplifyconfiguration);
32
30
  };
33
31
 
32
+ /**
33
+ * Creates an authentication service with sign-in functionality.
34
+ * @param {typeof amplifyAuth} auth - The authentication module to use.
35
+ * @returns {object} - An object containing authentication-related methods.
36
+ */
37
+ const createAuthService = (auth: typeof amplifyAuth) => {
38
+ /**
39
+ * Signs in a user with the provided credentials.
40
+ * @param {string} username - The username of the user.
41
+ * @param {string} password - The password of the user.
42
+ * @returns {Promise<string>} - The JWT token of the signed-in user.
43
+ * @throws {Error} - If sign-in fails or no tokens are retrieved.
44
+ */
45
+ const signIn = async (
46
+ username: string,
47
+ password: string
48
+ ): Promise<string> => {
49
+ configureAmplify();
50
+ await auth.signOut(); // Ensure the user is signed out first
51
+ const { isSignedIn } = await auth.signIn({ username, password });
52
+ assert.ok(isSignedIn, "Sign-in failed");
53
+ const { tokens } = await auth.fetchAuthSession();
54
+ assert.ok(tokens, "No tokens found");
55
+ return tokens.accessToken.toString();
56
+ };
57
+ return { signIn };
58
+ };
59
+
60
+ // Create the default auth service using amplifyAuth
61
+ const { signIn } = createAuthService(amplifyAuth);
62
+
34
63
  const deviceInfo =
35
64
  (axiosInstance: AxiosInstance) => (jwtToken: string, macAddress: string) =>
36
65
  axiosInstance.get<DeviceInfoType>(`device/${macAddress}/info`, {
@@ -39,6 +68,7 @@ const deviceInfo =
39
68
 
40
69
  const mqttCommand =
41
70
  (axiosInstance: AxiosInstance) =>
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
72
  (jwtToken: string, macAddress: string, payload: any) =>
43
73
  axiosInstance.put(
44
74
  "mqtt/command",
@@ -72,4 +102,4 @@ const configure = (baseURL: string = API_URL) => {
72
102
  };
73
103
  };
74
104
 
75
- export { signIn, configure };
105
+ export { configure, createAuthService, headers, signIn };