From 2244b27a07c2bb16cef077c3d70b949e3df8dd02 Mon Sep 17 00:00:00 2001 From: James Li Date: Wed, 29 May 2024 11:43:55 -0700 Subject: [PATCH] Unit testing + simplify API mapping --- README.md | 3 + jest.config.js | 16 ++++++ package-lock.json | 115 ++++++++++++++++++++++++++++++++++++-- package.json | 6 +- src/api/Method.test.ts | 120 ++++++++++++++++++++++++++++++++++++++++ src/api/Resource.ts | 8 +-- src/dimo.test.ts | 72 ++++++++++++++++++++++++ src/dimo.ts | 24 ++++---- src/graphql/Resource.ts | 8 +-- tsconfig.json | 4 +- 10 files changed, 348 insertions(+), 28 deletions(-) create mode 100644 jest.config.js create mode 100644 src/api/Method.test.ts create mode 100644 src/dimo.test.ts diff --git a/README.md b/README.md index 12123ee..e6b8dd9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ or use [yarn](https://classic.yarnpkg.com/en/package/@dimo-network/dimo-node-sdk yarn add @dimo-network/dimo-node-sdk ``` +## Unit Testing +Run `npm test` or `npm run test` to execute the Jest tests. + ## API Documentation Please visit the DIMO [Developer Documentation](https://docs.dimo.zone/developer-platform) to learn more about building on DIMO and detailed information on the API. diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..9107ba0 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +export default { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest', // Transform TypeScript files + '^.+\\.mjs$': 'babel-jest', // Transform .mjs files using Babel + '^.+\\.js$': 'babel-jest', // Transform JavaScript files + }, + transformIgnorePatterns: ['/node_modules/'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'mjs'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1' // Adjust this if you're using path aliases + } +}; diff --git a/package-lock.json b/package-lock.json index 5660bdc..47b454e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,12 +15,14 @@ }, "devDependencies": { "@types/dotenv-safe": "^8.1.6", + "@types/jest": "^29.5.12", "@types/node": "^20.11.25", "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", "jest": "^29.7.0", - "typescript": "^5.4.2" + "ts-jest": "^29.1.4", + "typescript": "^5.4.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1355,6 +1357,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1852,6 +1864,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -4399,6 +4423,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4462,6 +4492,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -5553,6 +5589,77 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-jest": { + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -5672,9 +5779,9 @@ } }, "node_modules/typescript": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", - "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 37d8cb3..2098763 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dimo-network/dimo-node-sdk", - "version": "1.1.4", + "version": "1.1.5", "description": "DIMO SDK for JavaScript", "main": "dist/index.js", "author": "James Li", @@ -29,12 +29,14 @@ }, "devDependencies": { "@types/dotenv-safe": "^8.1.6", + "@types/jest": "^29.5.12", "@types/node": "^20.11.25", "eslint": "^8.56.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", "jest": "^29.7.0", - "typescript": "^5.4.2" + "ts-jest": "^29.1.4", + "typescript": "^5.4.5" }, "types": "./dist/index.d.ts" } diff --git a/src/api/Method.test.ts b/src/api/Method.test.ts new file mode 100644 index 0000000..398a267 --- /dev/null +++ b/src/api/Method.test.ts @@ -0,0 +1,120 @@ +import axios from 'axios'; +import { Method } from './Method'; // Import the Method function to be tested +import { DimoError } from '../errors'; +import { DimoEnvironment } from '../environments'; + +const PROD = 'Production'; +const DEV = 'Dev'; +const RESOURCE = { + method: 'GET', + path: '', + queryParams: { param1: true }, +}; +const PARAM = { param1: 'value1' }; + +describe('Method Function', () => { + test('Valid API Call - Device Data API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.DeviceData, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.DeviceData, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ code: 200, message: 'Server is up.' }); + expect(prodResponse).toEqual({ code: 200, message: 'Server is up.' }); + }); + + test('Valid API Call - Device Definitions API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.DeviceDefinitions, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.DeviceDefinitions, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual('device definitions api running!'); + expect(prodResponse).toEqual('device definitions api running!'); + }); + + + test('Valid API Call - Devices API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.Devices, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.Devices, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ data: 'Server is up and running' }); + expect(prodResponse).toEqual({ data: 'Server is up and running' }); + }); + + test('Valid API Call - Events API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.Events, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.Events, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ data: 'Server is up and running' }); + expect(prodResponse).toEqual({ data: 'Server is up and running' }); + }); + + test('Valid API Call - Token Exchange API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.TokenExchange, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.TokenExchange, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ data: 'Server is up and running' }); + expect(prodResponse).toEqual({ data: 'Server is up and running' }); + }); + + test('Valid API Call - Users API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.User, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.User, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ data: 'Server is up and running' }); + expect(prodResponse).toEqual({ data: 'Server is up and running' }); + }); + + test('Valid API Call - Valuations API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.Valuations, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.Valuations, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual({ code: 200, message: 'Server is up.' }); + expect(prodResponse).toEqual({ code: 200, message: 'Server is up.' }); + }); + + test('Valid API Call - Vehicle Signal Decoding API Server is up and returning data', async () => { + jest.spyOn(axios, 'request').mockResolvedValue({ data: { key: 'value' } }); + + const devResponse = await Method(RESOURCE, DimoEnvironment.Dev.VehicleSignalDecoding, PARAM, DEV); + const prodResponse = await Method(RESOURCE, DimoEnvironment.Production.VehicleSignalDecoding, PARAM, PROD); + + // Assertion - Check if the response data is returned correctly + expect(devResponse).toEqual('healthy'); + expect(prodResponse).toEqual('healthy'); + }); + + + test('Missing Required Query Parameter - Throws Error', async () => { + // Mock input data with missing required query parameter + const resource = { + method: 'GET', + path: '/example/endpoint', + queryParams: { expectedParam: true }, // Expect expectedParam + }; + const baseUrl = 'https://example.com/api'; + const params = { unexpectedParam: 'value1' }; + + // Call the Method function and expect it to throw an error + await expect(Method(resource, baseUrl, params, DEV)).rejects.toThrowError(DimoError); + await expect(Method(resource, baseUrl, params, PROD)).rejects.toThrowError(DimoError); + }); +}); diff --git a/src/api/Resource.ts b/src/api/Resource.ts index be8c8bc..5c22b0b 100644 --- a/src/api/Resource.ts +++ b/src/api/Resource.ts @@ -6,9 +6,9 @@ export interface Resource { } export class Resource { - protected api: any; - protected resourceName: any; - protected env: any; + public api: any; + public resourceName: any; + public env: any; constructor(api: any, resourceName: string, env: keyof typeof DimoEnvironment ) { this.api = api; @@ -20,7 +20,7 @@ export class Resource { Object.keys(resources).forEach(key => { this[key] = (params: any = {}) => Method( resources[key], // Setup the endpoint resources - this.api[this.resourceName], // Setup the base URL + this.api, // Setup the base URL params, // Pass through the params this.env // Identiy the environment ); diff --git a/src/dimo.test.ts b/src/dimo.test.ts new file mode 100644 index 0000000..dc646f5 --- /dev/null +++ b/src/dimo.test.ts @@ -0,0 +1,72 @@ +import { DIMO } from './dimo'; + +const PROD = 'Production'; +const DEV = 'Dev'; + +const dimo = new DIMO(PROD); +const devDimo = new DIMO(DEV); + +describe('Production Environment', () => { + test('Production resources are initialized with the correct environment', () => { + expect(dimo.auth.env).toBe(PROD); + expect(dimo.devicedata.env).toBe(PROD); + expect(dimo.devicedefinitions.env).toBe(PROD); + expect(dimo.devices.env).toBe(PROD); + expect(dimo.events.env).toBe(PROD); + expect(dimo.identity.env).toBe(PROD); + expect(dimo.telemetry.env).toBe(PROD); + expect(dimo.tokenexchange.env).toBe(PROD); + expect(dimo.trips.env).toBe(PROD); + expect(dimo.user.env).toBe(PROD); + expect(dimo.valuations.env).toBe(PROD); + expect(dimo.vehiclesignaldecoding.env).toBe(PROD); + }); + + test('Production API endpoints are defined', () => { + expect(dimo.auth.api).toBeDefined; + expect(dimo.devicedata.api).toBeDefined; + expect(dimo.devicedefinitions.api).toBeDefined; + expect(dimo.devices.api).toBeDefined; + expect(dimo.events.api).toBeDefined; + expect(dimo.identity.api).toBeDefined; + expect(dimo.telemetry.api).toBeDefined; + expect(dimo.tokenexchange.api).toBeDefined; + expect(dimo.trips.api).toBeDefined; + expect(dimo.user.api).toBeDefined; + expect(dimo.valuations.api).toBeDefined; + expect(dimo.vehiclesignaldecoding.api).toBeDefined; + }); +}); + +describe('Dev Environment', () => { + test('Dev resources are initialized with the correct environment', () => { + expect(devDimo.auth.env).toBe(DEV); + expect(devDimo.devicedata.env).toBe(DEV); + expect(devDimo.devicedefinitions.env).toBe(DEV); + expect(devDimo.devices.env).toBe(DEV); + expect(devDimo.events.env).toBe(DEV); + expect(devDimo.identity.env).toBe(DEV); + expect(devDimo.telemetry.env).toBe(DEV); + expect(devDimo.tokenexchange.env).toBe(DEV); + expect(devDimo.trips.env).toBe(DEV); + expect(devDimo.user.env).toBe(DEV); + expect(devDimo.valuations.env).toBe(DEV); + expect(devDimo.vehiclesignaldecoding.env).toBe(DEV); + }); + + test('Dev API endpoints are defined', () => { + expect(devDimo.auth.api).toBeDefined; + expect(devDimo.devicedata.api).toBeDefined; + expect(devDimo.devicedefinitions.api).toBeDefined; + expect(devDimo.devices.api).toBeDefined; + expect(devDimo.events.api).toBeDefined; + expect(devDimo.identity.api).toBeDefined; + expect(devDimo.telemetry.api).toBeDefined; + expect(devDimo.tokenexchange.api).toBeDefined; + expect(devDimo.trips.api).toBeDefined; + expect(devDimo.user.api).toBeDefined; + expect(devDimo.valuations.api).toBeDefined; + expect(devDimo.vehiclesignaldecoding.api).toBeDefined; + }); +}); + diff --git a/src/dimo.ts b/src/dimo.ts index 7600189..dd6f91a 100644 --- a/src/dimo.ts +++ b/src/dimo.ts @@ -33,22 +33,22 @@ export class DIMO { public vehiclesignaldecoding: VehicleSignalDecoding; constructor(env: keyof typeof DimoEnvironment) { - this.identity = new Identity(DimoEnvironment[env], env); - this.telemetry = new Telemetry(DimoEnvironment[env], env); + this.identity = new Identity(DimoEnvironment[env].Identity, env); + this.telemetry = new Telemetry(DimoEnvironment[env].Telemetry, env); /** * Set up all REST Endpoints */ - this.auth = new Auth(DimoEnvironment[env], env); - this.devicedata = new DeviceData(DimoEnvironment[env], env); - this.devicedefinitions = new DeviceDefinitions(DimoEnvironment[env], env); - this.devices = new Devices(DimoEnvironment[env], env); - this.events = new Events(DimoEnvironment[env], env); - this.tokenexchange = new TokenExchange(DimoEnvironment[env], env); - this.trips = new Trips(DimoEnvironment[env], env); - this.user = new User(DimoEnvironment[env], env); - this.valuations = new Valuations(DimoEnvironment[env], env); - this.vehiclesignaldecoding = new VehicleSignalDecoding(DimoEnvironment[env], env); + this.auth = new Auth(DimoEnvironment[env].Auth, env); + this.devicedata = new DeviceData(DimoEnvironment[env].DeviceData, env); + this.devicedefinitions = new DeviceDefinitions(DimoEnvironment[env].DeviceDefinitions, env); + this.devices = new Devices(DimoEnvironment[env].Devices, env); + this.events = new Events(DimoEnvironment[env].Events, env); + this.tokenexchange = new TokenExchange(DimoEnvironment[env].TokenExchange, env); + this.trips = new Trips(DimoEnvironment[env].Trips, env); + this.user = new User(DimoEnvironment[env].User, env); + this.valuations = new Valuations(DimoEnvironment[env].Valuations, env); + this.vehiclesignaldecoding = new VehicleSignalDecoding(DimoEnvironment[env].VehicleSignalDecoding, env); } // Helper Function diff --git a/src/graphql/Resource.ts b/src/graphql/Resource.ts index 3384dd7..c58067c 100644 --- a/src/graphql/Resource.ts +++ b/src/graphql/Resource.ts @@ -6,9 +6,9 @@ export interface Resource { } export class Resource { - protected api: any; - protected resourceName: any; - protected env: any; + public api: any; + public resourceName: any; + public env: any; constructor(api: any, resourceName: string, env: keyof typeof DimoEnvironment ) { this.api = api; @@ -20,7 +20,7 @@ export class Resource { Object.keys(resources).forEach(key => { this[key] = (params: any = {}) => Query( resources[key], // Setup the endpoint resources - this.api[this.resourceName], // Setup the base URL + this.api, // Setup the base URL params, // Pass through the params ); }); diff --git a/tsconfig.json b/tsconfig.json index cec5dc2..beee6c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,12 +12,12 @@ "strict": true, /* Enable all strict type-checking options. */ "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */ - "types": ["node"] + "types": ["node", "jest"] }, "include": [ "src", "node" ], - "exclude": [], + "exclude": ["node_modules", "**/*.spec.ts"], "ts-node": { "esm": true },