From 751e5e13623804c47d127a3ee7defcbb57561faf Mon Sep 17 00:00:00 2001 From: Ross Stenersen Date: Thu, 9 Nov 2023 11:57:45 -0600 Subject: [PATCH] test: start converting unit tests over to ESM-friendly tests --- jest.config.ts | 4 +- package-lock.json | 124 +++++- package.json | 10 +- src/__tests__/lib/api-helpers.test.ts | 511 +++++++++++----------- src/__tests__/lib/table-generator.test.ts | 62 +-- src/__tests__/lib/user-query.test.ts | 26 +- src/__tests__/lib/validate-util.test.ts | 2 + 7 files changed, 405 insertions(+), 334 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index d9237652..38ef1895 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,7 +4,9 @@ import { defaultsESM as tsjPreset } from 'ts-jest/presets' const config: JestConfigWithTsJest = { testMatch: [ - '**/__tests__/**/*.test.ts', + // TODO: put this back when all unit tests are converted + // '**/__tests__/**/*.test.ts', + '**/__tests__/lib/(a*|t*|u*|v*).test.ts', ], setupFilesAfterEnv: ['jest-extended/all'], collectCoverageFrom: ['src/**/*.ts'], diff --git a/package-lock.json b/package-lock.json index d87e3e1a..b38e5490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "eventsource": "^2.0.2", "express": "^4.18.2", "get-port-please": "^3.1.1", - "inquirer": "^8.2.6", + "inquirer": "^9.2.11", "js-yaml": "^4.1.0", "jszip": "^3.10.1", "lodash.at": "^4.6.0", @@ -42,7 +42,7 @@ "@commitlint/config-conventional": "^17.7.0", "@types/eventsource": "^1.1.12", "@types/express": "^4.17.18", - "@types/inquirer": "^8.2.7", + "@types/inquirer": "^9.0.6", "@types/jest": "^29.5.5", "@types/js-yaml": "^4.0.6", "@types/lodash.at": "^4.6.7", @@ -3127,6 +3127,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@ljharb/through": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", + "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/@manypkg/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", @@ -4013,9 +4024,9 @@ "dev": true }, "node_modules/@types/inquirer": { - "version": "8.2.7", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.7.tgz", - "integrity": "sha512-uICJEaJOf6MsKyyAf8p58+QxTS6dwy91QVfXk1hnQ0rUT+u7KpkeFx5dxQ/oju0BaOKB284brEMBHLpNf4bZDQ==", + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.6.tgz", + "integrity": "sha512-1Go1AAP/yOy3Pth5Xf1DC3nfZ03cJLCPx6E2YnSN/5I3w1jHBVH4170DkZ+JxfmA7c9kL9+bf9z3FRGa4kNAqg==", "dev": true, "dependencies": { "@types/through": "*", @@ -5819,6 +5830,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, "engines": { "node": ">= 10" } @@ -8087,6 +8099,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -8101,6 +8114,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -9133,28 +9147,28 @@ "dev": true }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.11.tgz", + "integrity": "sha512-B2LafrnnhbRzCWfAdOXisUzL89Kg8cVJlYmhqoi3flSiV/TveO+nsXwgKr9h9PIo+J1hz7nBSk6gegRIMBBf7g==", "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", + "@ljharb/through": "^2.3.9", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^5.0.0", "lodash": "^4.17.21", - "mute-stream": "0.0.8", + "mute-stream": "1.0.0", "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^6.0.1" + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.18.0" } }, "node_modules/inquirer/node_modules/ansi-styles": { @@ -9186,6 +9200,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/inquirer/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, "node_modules/inquirer/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -9202,6 +9224,43 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/inquirer/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/figures": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/figures/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inquirer/node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -9236,6 +9295,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/inquirer/node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -9258,6 +9325,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inquirer/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -12226,7 +12301,8 @@ "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true }, "node_modules/napi-build-utils": { "version": "1.0.2", @@ -14040,6 +14116,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -15129,7 +15206,8 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true }, "node_modules/through2": { "version": "4.0.2", diff --git a/package.json b/package.json index 2d5e4152..da82de3e 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "eventsource": "^2.0.2", "express": "^4.18.2", "get-port-please": "^3.1.1", - "inquirer": "^8.2.6", + "inquirer": "^9.2.11", "js-yaml": "^4.1.0", "jszip": "^3.10.1", "lodash.at": "^4.6.0", @@ -83,7 +83,7 @@ "@commitlint/config-conventional": "^17.7.0", "@types/eventsource": "^1.1.12", "@types/express": "^4.17.18", - "@types/inquirer": "^8.2.7", + "@types/inquirer": "^9.0.6", "@types/jest": "^29.5.5", "@types/js-yaml": "^4.0.6", "@types/lodash.at": "^4.6.7", @@ -122,9 +122,9 @@ "watch": "tsc -b -w --sourceMap", "build": "npm run clean && npm run compile && npm run readme", "debug-build": "npm run clean && tsc -b --sourceMap && npm run readme", - "test": "jest", - "test-watch": "jest --watch --reporters=default", - "test-coverage": "jest --coverage=true", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test-watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --reporters=default", + "test-coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage=true", "prepack": "tsc -b && echo TODO:readme", "version": "echo TODO:readme && git add README.md && changeset version && npm i --package-lock-only && npm run build && npm run version -w packages/cli", "release": "npm run build && changeset publish", diff --git a/src/__tests__/lib/api-helpers.test.ts b/src/__tests__/lib/api-helpers.test.ts index fd20dde0..4bdd6b8f 100644 --- a/src/__tests__/lib/api-helpers.test.ts +++ b/src/__tests__/lib/api-helpers.test.ts @@ -1,13 +1,13 @@ +import { jest } from '@jest/globals' + import { CapabilitiesEndpoint, CapabilityNamespace, - LocationsEndpoint, - NoOpAuthenticator, Room, - RoomsEndpoint, SmartThingsClient, } from '@smartthings/core-sdk' +import { SimpleType } from '../test-lib/simple-type.js' import { forAllNamespaces, forAllOrganizations, @@ -16,336 +16,315 @@ import { withLocationAndRoom, withLocationsAndRooms, } from '../../lib/api-helpers.js' -import * as apiHelpers from '../../lib/api-helpers.js' -import { SimpleType } from '../test-lib/simple-type.js' -describe('api-helpers', () => { - let client: SmartThingsClient - - beforeEach(() => { - const urlProvider = { - baseURL: 'https://example.com/api', - authURL: 'https://example.com/auth', - keyApiURL: 'https://example.com/key', +const locations = [ + { locationId: 'uno', name: 'main location' }, + { locationId: 'dos', name: 'vacation home' }, +] +const locationsListMock = jest.fn().mockResolvedValue(locations) + +const roomsByLocationId: Map = new Map([ + ['uno', [{ roomId: 'twelve', name: 'garage' }, { roomId: 'unnamed' }, { name: 'no id' }]], + ['dos', [{ roomId: 'thirteen', name: 'kitchen' }, { roomId: 'fourteen', name: 'living room' }]], +]) +const roomsListMock = jest.fn(async locationId => { + let rooms: Room[] | undefined + if (locationId && roomsByLocationId.has(locationId)) { + rooms = roomsByLocationId.get(locationId) + if (rooms) { + return rooms } - const authenticator = new NoOpAuthenticator() - client = new SmartThingsClient(authenticator, { urlProvider }) + } + throw Error('Request failed with status code 403') +}) - const locations = [ - { locationId: 'uno', name: 'main location' }, - { locationId: 'dos', name: 'vacation home' }, +const client = { + locations: { + list: locationsListMock, + }, + rooms: { + list: roomsListMock, + }, +} as unknown as SmartThingsClient + +describe('withLocations', () => { + it('updates simple object', async () => { + const things = [ + { locationId: 'uno', other: 'field' }, ] - jest.spyOn(LocationsEndpoint.prototype, 'list').mockResolvedValue(locations) - const roomsByLocationId: Map = new Map([ - ['uno', [{ roomId: 'twelve', name: 'garage' }, { roomId: 'unnamed' }, { name: 'no id' }]], - ['dos', [{ roomId: 'thirteen', name: 'kitchen' }, { roomId: 'fourteen', name: 'living room' }]], - ]) - jest.spyOn(RoomsEndpoint.prototype, 'list').mockImplementation(async locationId => { - let rooms: Room[] | undefined - if (locationId && roomsByLocationId.has(locationId)) { - rooms = roomsByLocationId.get(locationId) - if (rooms) { - return rooms - } - } - throw Error('Request failed with status code 403') - }) + const updated = await withLocations(client, things) + + expect(locationsListMock).toHaveBeenCalledTimes(1) + + expect(updated).not.toEqual(things) + expect(updated).toEqual([{ ...things[0], location: 'main location' }]) }) - describe('withLocations', () => { - it('updates simple object', async () => { - const thing = [ - { locationId: 'uno', other: 'field' }, - ] + it('succeeds even with no locationId', async () => { + const things = [ + { locationId: 'uno', other: 'field' }, + { another: 'value' }, + ] - const updated = await withLocations(client, thing) + const updated = await withLocations(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(thing) - expect(updated).toEqual([{ ...thing[0], location: 'main location' }]) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([ + { ...things[0], location: 'main location' }, + { ...things[1], location: '' }, + ]) + }) - it('succeeds even with no locationId', async () => { - const things = [ - { locationId: 'uno', other: 'field' }, - { another: 'value' }, - ] + it('notes bad locationId', async () => { + // The API shouldn't allow bad location ids so this shouldn't happen. + const things = [ + { locationId: 'uno', other: 'field' }, + { locationId: 'invalid-location-id', another: 'value' }, + ] - const updated = await withLocations(client, things) + const updated = await withLocations(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(things) - expect(updated).toEqual([ - { ...things[0], location: 'main location' }, - { ...things[1], location: '' }, - ]) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([ + { ...things[0], location: 'main location' }, + { ...things[1], location: '' }, + ]) + }) +}) - it('notes bad locationId', async () => { - // The API shouldn't allow bad location ids so this shouldn't happen. - const things = [ - { locationId: 'uno', other: 'field' }, - { locationId: 'invalid-location-id', another: 'value' }, - ] +describe('withLocation', () => { + it('updates simple object', async () => { + const thing = { locationId: 'uno', other: 'field' } - const updated = await withLocations(client, things) + const updated = await withLocation(client, thing) - expect(client.locations.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(things) - expect(updated).toEqual([ - { ...things[0], location: 'main location' }, - { ...things[1], location: '' }, - ]) - }) + expect(updated).not.toEqual(thing) + expect(updated).toEqual({ ...thing, location: 'main location' }) }) +}) - describe('withLocation', () => { - const withLocationsSpy = jest.spyOn(apiHelpers, 'withLocations') - - it('proxies to withLocations and indexes return', async () => { - const item: SimpleType & apiHelpers.WithLocation = { - num: 1, - str: 'string', - locationId: 'location-id', - } - const itemWithLocation = { ...item, location: 'Location Name' } +describe('withLocationsAndRooms', () => { + it('updates simple object', async () => { + const thing = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + ] - withLocationsSpy.mockResolvedValueOnce([itemWithLocation]) + const updated = await withLocationsAndRooms(client, thing) - expect(await withLocation(client, item)).toBe(itemWithLocation) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(1) - expect(withLocationsSpy).toHaveBeenCalledTimes(1) - expect(withLocationsSpy).toHaveBeenCalledWith(client, [item]) - }) + expect(updated).not.toEqual(thing) + expect(updated).toEqual([{ ...thing[0], location: 'main location', room: 'garage' }]) }) - describe('withLocationsAndRooms', () => { - it('updates simple object', async () => { - const thing = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - ] + it('succeeds even with no locationId', async () => { + const things = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + { another: 'value', roomId: 'twelve' }, + ] - const updated = await withLocationsAndRooms(client, thing) + const updated = await withLocationsAndRooms(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(thing) - expect(updated).toEqual([{ ...thing[0], location: 'main location', room: 'garage' }]) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([ + { ...things[0], location: 'main location', room: 'garage' }, + { ...things[1], location: '', room: '' }, + ]) + }) - it('succeeds even with no locationId', async () => { - const things = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - { another: 'value', roomId: 'twelve' }, - ] + it('fails with bad locationId', async () => { + // The API shouldn't allow bad location ids so this shouldn't happen. + const things = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + { locationId: 'invalid-location-id', roomId: 'twelve', another: 'value' }, + ] - const updated = await withLocationsAndRooms(client, things) + await expect(withLocationsAndRooms(client, things)) + .rejects.toThrow('Request failed with status code 403') - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(2) + }) - expect(updated).not.toEqual(things) - expect(updated).toEqual([ - { ...things[0], location: 'main location', room: 'garage' }, - { ...things[1], location: '', room: '' }, - ]) - }) + it('succeeds even with no roomId', async () => { + const things = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + { another: 'value', locationId: 'dos' }, + ] - it('fails with bad locationId', async () => { - // The API shouldn't allow bad location ids so this shouldn't happen. - const things = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - { locationId: 'invalid-location-id', roomId: 'twelve', another: 'value' }, - ] + const updated = await withLocationsAndRooms(client, things) - await expect(withLocationsAndRooms(client, things)) - .rejects.toThrow('Request failed with status code 403') + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(2) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(2) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([ + { ...things[0], location: 'main location', room: 'garage' }, + { ...things[1], location: 'vacation home', room: '' }, + ]) + }) - it('succeeds even with no roomId', async () => { - const things = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - { another: 'value', locationId: 'dos' }, - ] + it('handles room with no id', async () => { + // This seems odd but the roomId field is not required in the API. + const thing = [ + { locationId: 'uno', other: 'field' }, + ] - const updated = await withLocationsAndRooms(client, things) + const updated = await withLocationsAndRooms(client, thing) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(2) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(things) - expect(updated).toEqual([ - { ...things[0], location: 'main location', room: 'garage' }, - { ...things[1], location: 'vacation home', room: '' }, - ]) - }) + expect(updated).not.toEqual(thing) + expect(updated).toEqual([{ ...thing[0], location: 'main location', room: '' }]) + }) - it('handles room with no id', async () => { - // This seems odd but the roomId field is not required in the API. - const thing = [ - { locationId: 'uno', other: 'field' }, - ] + it('handles unnamed room', async () => { + const things = [ + { locationId: 'uno', roomId: 'unnamed', other: 'field' }, + ] - const updated = await withLocationsAndRooms(client, thing) + const updated = await withLocationsAndRooms(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(1) - expect(updated).not.toEqual(thing) - expect(updated).toEqual([{ ...thing[0], location: 'main location', room: '' }]) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([{ ...things[0], location: 'main location', room: '' }]) + }) - it('handles unnamed room', async () => { - const thing = [ - { locationId: 'uno', roomId: 'unnamed', other: 'field' }, - ] + it('succeeds even with bad roomId', async () => { + const things = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + { locationId: 'dos', roomId: 'not-a-real-room', another: 'value' }, + ] - const updated = await withLocationsAndRooms(client, thing) + const updated = await withLocationsAndRooms(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(1) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(2) - expect(updated).not.toEqual(thing) - expect(updated).toEqual([{ ...thing[0], location: 'main location', room: '' }]) - }) + expect(updated).not.toEqual(things) + expect(updated).toEqual([ + { ...things[0], location: 'main location', room: 'garage' }, + { ...things[1], location: 'vacation home', room: '' }, + ]) + }) - it('succeeds even with bad roomId', async () => { - const things = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - { locationId: 'dos', roomId: 'not-a-real-room', another: 'value' }, - ] + it('calls rooms only once for each locationId', async () => { + const things = [ + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + { locationId: 'dos', roomId: 'thirteen', other: 'field' }, + { locationId: 'dos', roomId: 'fourteen', other: 'field' }, + { locationId: 'uno', roomId: 'twelve', other: 'field' }, + ] - const updated = await withLocationsAndRooms(client, things) + const updated = await withLocationsAndRooms(client, things) - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(2) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(2) - expect(updated).not.toEqual(things) - expect(updated).toEqual([ - { ...things[0], location: 'main location', room: 'garage' }, - { ...things[1], location: 'vacation home', room: '' }, - ]) - }) - - it('calls rooms only once for each locationId', async () => { - const things = [ - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - { locationId: 'dos', roomId: 'thirteen', other: 'field' }, - { locationId: 'dos', roomId: 'fourteen', other: 'field' }, - { locationId: 'uno', roomId: 'twelve', other: 'field' }, - ] - - const updated = await withLocationsAndRooms(client, things) - - expect(client.locations.list).toHaveBeenCalledTimes(1) - expect(client.rooms.list).toHaveBeenCalledTimes(2) - - expect(updated).not.toEqual(things) - expect(updated.length).toBe(4) - expect(updated).toEqual([ - { ...things[0], location: 'main location', room: 'garage' }, - { ...things[1], location: 'vacation home', room: 'kitchen' }, - { ...things[2], location: 'vacation home', room: 'living room' }, - { ...things[3], location: 'main location', room: 'garage' }, - ]) - }) + expect(updated).not.toEqual(things) + expect(updated.length).toBe(4) + expect(updated).toEqual([ + { ...things[0], location: 'main location', room: 'garage' }, + { ...things[1], location: 'vacation home', room: 'kitchen' }, + { ...things[2], location: 'vacation home', room: 'living room' }, + { ...things[3], location: 'main location', room: 'garage' }, + ]) }) +}) - describe('withLocationAndRoom', () => { - const withLocationsAndRoomsSpy = jest.spyOn(apiHelpers, 'withLocationsAndRooms') - - it('proxies to withLocationsAndRooms and indexes return', async () => { - const item: SimpleType & apiHelpers.WithRoom = { - num: 1, - str: 'string', - locationId: 'location-id', - roomId: 'room-id', - } - const itemWithLocationAndRoom = { ...item, location: 'Location Name', room: 'Room Name' } +describe('withLocationAndRoom', () => { + it('updates simple object', async () => { + const thing = { locationId: 'uno', roomId: 'twelve', other: 'field' } - withLocationsAndRoomsSpy.mockResolvedValueOnce([itemWithLocationAndRoom]) + const updated = await withLocationAndRoom(client, thing) - expect(await withLocationAndRoom(client, item)).toBe(itemWithLocationAndRoom) + expect(locationsListMock).toHaveBeenCalledTimes(1) + expect(roomsListMock).toHaveBeenCalledTimes(1) - expect(withLocationsAndRoomsSpy).toHaveBeenCalledTimes(1) - expect(withLocationsAndRoomsSpy).toHaveBeenCalledWith(client, [item]) - }) + expect(updated).not.toEqual(thing) + expect(updated).toEqual({ ...thing, location: 'main location', room: 'garage' }) }) +}) - describe('forAllOrganizations', () => { - const organizationsListMock = jest.fn() - const org1Client = { - isFor: 'Organization 1', - } as unknown as SmartThingsClient - const org2Client = { - isFor: 'Organization 1', - } as unknown as SmartThingsClient - const cloneMock = jest.fn() - const client = { - organizations: { - list: organizationsListMock, - }, - clone: cloneMock, - } as unknown as SmartThingsClient - - const organization1 = { organizationId: 'organization-1-id', name: 'Organization 1' } - const organization2 = { organizationId: 'organization-2-id', name: 'Organization 2' } - - it('combines multiple results', async () => { - organizationsListMock.mockResolvedValueOnce([organization1, organization2]) - cloneMock.mockReturnValueOnce(org1Client) - cloneMock.mockReturnValueOnce(org2Client) - const query = jest.fn() - .mockResolvedValueOnce([ - { name: 'thing 1' }, - { name: 'thing 2' }, - ]) - .mockResolvedValueOnce([{ name: 'thing 3' }]) - - expect(await forAllOrganizations(client, query)).toStrictEqual([ - { name: 'thing 1', organization: 'Organization 1' }, - { name: 'thing 2', organization: 'Organization 1' }, - { name: 'thing 3', organization: 'Organization 2' }, +describe('forAllOrganizations', () => { + const organizationsListMock = jest.fn() + const org1Client = { + isFor: 'Organization 1', + } as unknown as SmartThingsClient + const org2Client = { + isFor: 'Organization 1', + } as unknown as SmartThingsClient + const cloneMock = jest.fn() + const client = { + organizations: { + list: organizationsListMock, + }, + clone: cloneMock, + } as unknown as SmartThingsClient + + const organization1 = { organizationId: 'organization-1-id', name: 'Organization 1' } + const organization2 = { organizationId: 'organization-2-id', name: 'Organization 2' } + + it('combines multiple results', async () => { + organizationsListMock.mockResolvedValueOnce([organization1, organization2]) + cloneMock.mockReturnValueOnce(org1Client) + cloneMock.mockReturnValueOnce(org2Client) + const query = jest.fn() + .mockResolvedValueOnce([ + { name: 'thing 1' }, + { name: 'thing 2' }, ]) + .mockResolvedValueOnce([{ name: 'thing 3' }]) - expect(organizationsListMock).toHaveBeenCalledTimes(1) - expect(organizationsListMock).toHaveBeenCalledWith() + expect(await forAllOrganizations(client, query)).toStrictEqual([ + { name: 'thing 1', organization: 'Organization 1' }, + { name: 'thing 2', organization: 'Organization 1' }, + { name: 'thing 3', organization: 'Organization 2' }, + ]) - expect(query).toHaveBeenCalledTimes(2) - expect(query).toHaveBeenCalledWith(org1Client, organization1) - expect(query).toHaveBeenCalledWith(org2Client, organization2) - }) + expect(organizationsListMock).toHaveBeenCalledTimes(1) + expect(organizationsListMock).toHaveBeenCalledWith() + + expect(query).toHaveBeenCalledTimes(2) + expect(query).toHaveBeenCalledWith(org1Client, organization1) + expect(query).toHaveBeenCalledWith(org2Client, organization2) }) +}) - describe('forAllNamespaces', () => { - const listNamespacesMock = jest.fn() - const capabilities = { - listNamespaces: listNamespacesMock, - } as unknown as CapabilitiesEndpoint - const client = { capabilities } as SmartThingsClient - const queryMock = jest.fn() as jest.Mock, [CapabilityNamespace]> +describe('forAllNamespaces', () => { + const listNamespacesMock = jest.fn() + const capabilities = { + listNamespaces: listNamespacesMock, + } as unknown as CapabilitiesEndpoint + const client = { capabilities } as SmartThingsClient + const queryMock = jest.fn() as jest.Mock, [CapabilityNamespace]> - it('combines multiple results', async () => { - listNamespacesMock.mockResolvedValueOnce(['namespace1', 'namespace2']) + it('combines multiple results', async () => { + listNamespacesMock.mockResolvedValueOnce(['namespace1', 'namespace2']) - const ns1Items = [{ num: 1, str: 'str1' }] - const ns2Items = [{ num: 2, str: 'str2' }, { num: 3, str: 'str3' }] - queryMock.mockResolvedValueOnce(ns1Items) - queryMock.mockResolvedValueOnce(ns2Items) + const ns1Items = [{ num: 1, str: 'str1' }] + const ns2Items = [{ num: 2, str: 'str2' }, { num: 3, str: 'str3' }] + queryMock.mockResolvedValueOnce(ns1Items) + queryMock.mockResolvedValueOnce(ns2Items) - expect(await forAllNamespaces(client, queryMock)).toStrictEqual([...ns1Items, ...ns2Items]) + expect(await forAllNamespaces(client, queryMock)).toStrictEqual([...ns1Items, ...ns2Items]) - expect(listNamespacesMock).toHaveBeenCalledTimes(1) - expect(listNamespacesMock).toHaveBeenCalledWith() - }) + expect(listNamespacesMock).toHaveBeenCalledTimes(1) + expect(listNamespacesMock).toHaveBeenCalledWith() }) }) diff --git a/src/__tests__/lib/table-generator.test.ts b/src/__tests__/lib/table-generator.test.ts index ad753e6c..c0c1f3f2 100644 --- a/src/__tests__/lib/table-generator.test.ts +++ b/src/__tests__/lib/table-generator.test.ts @@ -1,28 +1,34 @@ -import at from 'lodash.at' +import { jest } from '@jest/globals' + import { URL } from 'url' -import log4js from 'log4js' -import { DefaultTableGenerator, TableFieldDefinition, TableGenerator } from '../../lib/table-generator.js' +import { TableFieldDefinition, TableGenerator } from '../../lib/table-generator.js' const mockDebug = jest.fn() const mockWarn = jest.fn() -jest.mock('log4js', () => ({ - getLogger: jest.fn(() => ({ - debug: mockDebug, - warn: mockWarn, - })), +jest.unstable_mockModule('log4js', () => ({ + default: { + getLogger: jest.fn(() => ({ + debug: mockDebug, + warn: mockWarn, + })), + }, })) -jest.mock('lodash.at', () => { - const actualAt = jest.requireActual('lodash.at') +jest.unstable_mockModule('lodash.at', () => { + const original = jest.requireActual('lodash.at') as (...args: unknown[]) => unknown return { - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(actualAt), + default: jest.fn(original), } }) +const log4js = (await import('log4js')).default +const at = (await import('lodash.at')).default + +// eslint-disable-next-line @typescript-eslint/naming-convention +const { DefaultTableGenerator } = await import('../../lib/table-generator.js') + /** * Quote characters that are special to regular expressions. * This isn't complete but it's good enough for this test. @@ -146,8 +152,8 @@ const basicFieldDefinitions: TableFieldDefinition[] = [ ] describe('table-generator', () => { - const mockAt = jest.mocked(at) - const mockGetLogger = jest.mocked(log4js.getLogger) + const atMock = jest.mocked(at) + const getLoggerMock = jest.mocked(log4js.getLogger) let tableGenerator: TableGenerator @@ -314,39 +320,39 @@ describe('table-generator', () => { }) it('uses empty string for no match', () => { - mockAt.mockReturnValue([]) + atMock.mockReturnValue([]) const output = tableGenerator.buildTableFromList([{} as { obj?: string }], [{ path: 'obj.name' }]) expect(output).toHaveItemValues(['']) - expect(mockGetLogger).toHaveBeenCalledTimes(1) - expect(mockGetLogger).toHaveBeenCalledWith('table-manager') - expect(mockAt).toHaveBeenCalledTimes(1) - expect(mockAt).toHaveBeenCalledWith({}, 'obj.name') + expect(getLoggerMock).toHaveBeenCalledTimes(1) + expect(getLoggerMock).toHaveBeenCalledWith('table-manager') + expect(atMock).toHaveBeenCalledTimes(1) + expect(atMock).toHaveBeenCalledWith({}, 'obj.name') expect(mockDebug).toHaveBeenCalledTimes(1) expect(mockDebug).toHaveBeenCalledWith('did not find match for obj.name in {}') }) it('combines data on multiple matches', () => { - mockAt.mockReturnValue(['one', 'two']) + atMock.mockReturnValue(['one', 'two']) const output = tableGenerator.buildTableFromList([{} as { obj?: string }], [{ path: 'obj.name' }]) expect(output).toHaveItemValues(['one, two']) - expect(mockGetLogger).toHaveBeenCalledTimes(1) - expect(mockGetLogger).toHaveBeenCalledWith('table-manager') - expect(mockAt).toHaveBeenCalledTimes(1) - expect(mockAt).toHaveBeenCalledWith({}, 'obj.name') + expect(getLoggerMock).toHaveBeenCalledTimes(1) + expect(getLoggerMock).toHaveBeenCalledWith('table-manager') + expect(atMock).toHaveBeenCalledTimes(1) + expect(atMock).toHaveBeenCalledWith({}, 'obj.name') expect(mockWarn).toHaveBeenCalledTimes(1) expect(mockWarn).toHaveBeenCalledWith('found more than one match for obj.name in {}') }) it('gets logger only once', () => { tableGenerator.buildTableFromList([{} as { obj?: string }], [{ path: 'obj.name' }]) - expect(mockGetLogger).toHaveBeenCalledTimes(1) - expect(mockGetLogger).toHaveBeenCalledWith('table-manager') + expect(getLoggerMock).toHaveBeenCalledTimes(1) + expect(getLoggerMock).toHaveBeenCalledWith('table-manager') tableGenerator.buildTableFromList([{} as { obj?: string }], [{ path: 'obj.name' }]) - expect(mockGetLogger).toHaveBeenCalledTimes(1) + expect(getLoggerMock).toHaveBeenCalledTimes(1) }) it('includes separators every 4 rows with grouping on', () => { diff --git a/src/__tests__/lib/user-query.test.ts b/src/__tests__/lib/user-query.test.ts index bddf1f94..848c3cb6 100644 --- a/src/__tests__/lib/user-query.test.ts +++ b/src/__tests__/lib/user-query.test.ts @@ -1,21 +1,25 @@ -import inquirer from 'inquirer' +import { jest } from '@jest/globals' -import { +import { ValidateFunction } from '../../lib/user-query' + + +const promptMock = jest.fn() +jest.unstable_mockModule('inquirer', () => ({ + default: { + prompt: promptMock, + }, +})) + +const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => true) + +const { askForBoolean, askForInteger, askForNumber, askForString, askForOptionalString, numberTransformer, - ValidateFunction, -} from '../../lib/user-query.js' - - -jest.mock('inquirer') - -const promptMock = jest.mocked(inquirer.prompt) - -const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => true) +} = await import('../../lib/user-query.js') describe('numberTransformer', () => { diff --git a/src/__tests__/lib/validate-util.test.ts b/src/__tests__/lib/validate-util.test.ts index 280d6a19..b0729bbe 100644 --- a/src/__tests__/lib/validate-util.test.ts +++ b/src/__tests__/lib/validate-util.test.ts @@ -1,3 +1,5 @@ +import { jest } from '@jest/globals' + import { httpsURLValidate, localhostOrHTTPSValidate, stringValidateFn, urlValidate } from '../../lib/validate-util.js'