diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d59b189..312a620 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,10 @@ jobs: run: yarn install - name: Linting run: | - npm run lint - - name: Coverage + yarn run lint + - name: Unit tests & coverage run: | - npm run coverage + yarn run coverage + - name: Integration tests + run: | + yarn run test ./tests/integration diff --git a/jest.config.ts b/jest.config.ts index a062b81..051d642 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,13 +1,23 @@ import type { Config } from 'jest'; const config: Config = { + preset: "ts-jest", + extensionsToTreatAsEsm: ['.ts'], + transform: { + "^.+\\.ts?$": [ + "ts-jest", + { + useESM: true, + }, + ], + }, setupFilesAfterEnv: ['./tests/jest.setup.ts'], - transform: { '\\.[jt]s$': ['ts-jest', { tsconfig: { allowJs: true } }] }, moduleNameMapper: { '^(\\.{1,2}/.*)\\.[jt]s$': '$1', }, testEnvironment: "node", coverageDirectory: "./build/coverage", + coverageReporters: ['lcov', 'text-summary'], coverageThreshold: { global: { branches: 0, @@ -16,7 +26,7 @@ const config: Config = { statements: 0 } }, - "collectCoverageFrom": [ + collectCoverageFrom: [ "src/**/*" ] }; diff --git a/package.json b/package.json index b6a12b9..14c4406 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "express-async-handler": "^1.2.0", "glob": "^8.0.1", "node-os-utils": "^1.3.7", - "on-change": "^4.0.1", + "on-change": "^5.0.1", "pino": "^8.17.2", "read-last-lines": "^1.8.0", "reflect-metadata": "^0.1.13", @@ -38,6 +38,7 @@ "@types/node-os-utils": "^1.3.1", "@types/pino": "^7.0.5", "@types/serialport": "^8.0.2", + "@types/supertest": "^6.0.2", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", @@ -50,6 +51,7 @@ "jest-mock-extended": "^3.0.4", "nodemon": "^3.0.2", "pino-pretty": "^10.3.0", + "supertest": "^6.3.4", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" @@ -60,8 +62,8 @@ "scripts": { "compile": "tsc && node dist/index.js --trace-warnings | pino-pretty", "dev": "nodemon", - "test": "jest", - "coverage": "jest --collect-coverage", + "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest", + "coverage": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest ./tests/unit --collect-coverage", "lint": "eslint -c .eslintrc.cjs --ext .ts .", "build": "tsc" } diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..e29a1ab --- /dev/null +++ b/src/app.ts @@ -0,0 +1,157 @@ +import 'dotenv/config'; +import 'reflect-metadata'; +import cors from 'cors'; +import contentTypeMiddleware from './middleware/contentTypeMiddleware.js'; +import express from 'express'; +import {Pimple} from '@timesplinter/pimple'; +import ControllerServiceProvider from './serviceProvider/controllerServiceProvider.js'; +import RepositoryServiceProvider from './serviceProvider/repositoryServiceProvider.js'; +import SerializationServiceProvider from './serviceProvider/serializationServiceProvider.js'; +import FactoryServiceProvider from './serviceProvider/factoryServiceProvider.js'; +import DeviceServiceProvider from "./serviceProvider/deviceServiceProvider.js"; +import SettingsServiceProvider from "./serviceProvider/settingsServiceProvider.js"; +import * as http from 'http' +import SocketServiceProvider from "./serviceProvider/socketServiceProvider.js"; +import {DeviceUpdateData} from "./socket/types"; +import AutomationServiceProvider from "./serviceProvider/automationServiceProvider.js"; +import type Device from "./device/device.js"; +import WebSocketEvent from "./device/webSocketEvent.js"; +import ServerServiceProvider from "./serviceProvider/serverServiceProvider.js"; +import asyncHandler from "express-async-handler" +import AutomationEventType from "./automation/automationEventType.js"; +import DeviceManagerEvent from "./device/deviceManagerEvent.js"; +import LoggerServiceProvider from "./serviceProvider/loggerServiceProvider.js"; +import DeviceDiscriminator from "./serialization/discriminator/deviceDiscriminator.js"; +import ServiceMap from "./serviceMap.js"; + +const container = new Pimple(); +export const app = express(); +const httpServer = http.createServer(app); + +container + .register(new LoggerServiceProvider()) + .register(new ServerServiceProvider(httpServer)) + .register(new SettingsServiceProvider()) + .register(new DeviceServiceProvider()) + .register(new ControllerServiceProvider()) + .register(new SocketServiceProvider()) + .register(new RepositoryServiceProvider()) + .register(new SerializationServiceProvider()) + .register(new AutomationServiceProvider()) + .register(new FactoryServiceProvider()) +; + +export const logger = container.get('logger.default'); +const io = container.get('server.websocket'); +const deviceManager = container.get('device.manager'); +const scriptRuntime = container.get('automation.scriptRuntime'); + +container.get('device.provider.loader').loadFromSettings(); + +// Middlewares +app + .use(cors()) + .use(contentTypeMiddleware) + .use(express.json()) + .use(express.text()) +; + +// Routes +app.get('/health', asyncHandler((req, res) => { + const controller = container.get('controller.health') + return controller.execute(req, res) +})); + +app.get('/devices', (req, res) => { + const controller = container.get('controller.getDevices') + return controller.execute(req, res) +}); + +app.get('/device/:deviceId', (req, res) => { + const controller = container.get('controller.getDevice') + return controller.execute(req, res) +}); + +app.patch('/device/:deviceId', (req, res) => { + const controller = container.get('controller.patchDevice') + return controller.execute(req, res) +}); + +app.get('/automation/scripts', (req, res) => { + const controller = container.get('controller.automation.getScripts') + return controller.execute(req, res) +}); + +app.get('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { + const controller = container.get('controller.automation.getScript') + return controller.execute(req, res) +}); + +app.post('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { + const controller = container.get('controller.automation.createScript') + return controller.execute(req, res) +}); + +app.delete('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { + const controller = container.get('controller.automation.deleteScript') + return controller.execute(req, res) +}); + +app.get('/automation/log', asyncHandler((req, res) => { + const controller = container.get('controller.automation.getLog') + return controller.execute(req, res) +})); + +app.post('/automation/run', (req, res) => { + const controller = container.get('controller.automation.runScript') + return controller.execute(req, res) +}); + +app.get('/automation/stop', (req, res) => { + const controller = container.get('controller.automation.stopScript') + return controller.execute(req, res) +}); + +app.get('/automation/status', (req, res) => { + const controller = container.get('controller.automation.statusScript') + return controller.execute(req, res) +}); + +// Whenever someone connects this gets executed +io.on('connection', socket => { + logger.debug(`Client connected: ${socket.id}`); + + socket.on('disconnect', () => { + logger.debug(`Client disconnected: ${socket.id}`); + }); + + const deviceUpdateHandler = container.get('socket.deviceUpdateHandler'); + + socket.on(WebSocketEvent.deviceUpdateReceived, (data) => deviceUpdateHandler.handle(data as DeviceUpdateData)); +}); + +const serializer = container.get('serializer.classToPlain'); + +const deviceDiscriminator = DeviceDiscriminator.createClassTransformerTypeDiscriminator('type'); + +deviceManager.on(DeviceManagerEvent.deviceConnected, (device: Device) => { + io.emit(WebSocketEvent.deviceConnected, serializer.transform(device, deviceDiscriminator)); + scriptRuntime.runForEvent(DeviceManagerEvent.deviceConnected, device); +}); + +deviceManager.on(DeviceManagerEvent.deviceDisconnected, (device: Device) => { + io.emit(WebSocketEvent.deviceDisconnected, serializer.transform(device, deviceDiscriminator)); + scriptRuntime.runForEvent(DeviceManagerEvent.deviceDisconnected, device); +}); + +deviceManager.on(DeviceManagerEvent.deviceRefreshed, (device: Device) => { + io.emit(WebSocketEvent.deviceRefreshed, serializer.transform(device, deviceDiscriminator)); + scriptRuntime.runForEvent(DeviceManagerEvent.deviceRefreshed, device); +}); + +// Automation events +scriptRuntime.on(AutomationEventType.consoleLog, (data: any) => io.emit(AutomationEventType.consoleLog, data)); + +process.on('uncaughtException', (err: Error) => { + logger.error('Asynchronous error caught.', err); +}); diff --git a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts index 8957e0d..3df6826 100644 --- a/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts +++ b/src/device/protocol/buttplugIo/buttplugIoWebsocketDeviceProvider.ts @@ -56,10 +56,10 @@ export default class ButtplugIoWebsocketDeviceProvider extends DeviceProvider this.connectToServer(); - setInterval(() => { this.connectToServer() }, 3000); + setInterval(() => { this.connectToServer() }, 3000).unref(); if (true === this.autoScan) { - setInterval(() => { this.discoverButtplugIoDevices() }, 60000); + setInterval(() => { this.discoverButtplugIoDevices() }, 60000).unref(); } resolve(); diff --git a/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts b/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts index 6880c95..e9991b8 100644 --- a/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts +++ b/src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts @@ -39,7 +39,7 @@ export default class SlvCtrlPlusSerialDeviceProvider extends DeviceProvider { return new Promise((resolve) => { // Scan for new SlvCtrl+ protocol serial devices every 3 seconds - setInterval(() => { this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) }, 3000); + setInterval(() => { this.discoverSerialDevices().catch((e: Error) => this.logger.error(e.message, e)) }, 3000).unref(); resolve(); }) } diff --git a/src/device/provider/deviceProvider.ts b/src/device/provider/deviceProvider.ts index e0a89fd..7e6270d 100644 --- a/src/device/provider/deviceProvider.ts +++ b/src/device/provider/deviceProvider.ts @@ -44,6 +44,6 @@ export default abstract class DeviceProvider deviceStatusUpdater(); - return setInterval(deviceStatusUpdater, device.getRefreshInterval); + return setInterval(deviceStatusUpdater, device.getRefreshInterval).unref(); } } diff --git a/src/index.ts b/src/index.ts index 1084689..3471ce4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,164 +1,10 @@ import 'dotenv/config'; import 'reflect-metadata'; -import cors from 'cors'; -import contentTypeMiddleware from './middleware/contentTypeMiddleware.js'; -import express from 'express'; -import {Pimple} from '@timesplinter/pimple'; -import ControllerServiceProvider from './serviceProvider/controllerServiceProvider.js'; -import RepositoryServiceProvider from './serviceProvider/repositoryServiceProvider.js'; -import SerializationServiceProvider from './serviceProvider/serializationServiceProvider.js'; -import FactoryServiceProvider from './serviceProvider/factoryServiceProvider.js'; -import DeviceServiceProvider from "./serviceProvider/deviceServiceProvider.js"; -import SettingsServiceProvider from "./serviceProvider/settingsServiceProvider.js"; -import * as http from 'http' -import SocketServiceProvider from "./serviceProvider/socketServiceProvider.js"; -import {DeviceUpdateData} from "./socket/types"; -import AutomationServiceProvider from "./serviceProvider/automationServiceProvider.js"; -import type Device from "./device/device.js"; -import WebSocketEvent from "./device/webSocketEvent.js"; -import ServerServiceProvider from "./serviceProvider/serverServiceProvider.js"; -import asyncHandler from "express-async-handler" -import AutomationEventType from "./automation/automationEventType.js"; -import DeviceManagerEvent from "./device/deviceManagerEvent.js"; -import LoggerServiceProvider from "./serviceProvider/loggerServiceProvider.js"; -import DeviceDiscriminator from "./serialization/discriminator/deviceDiscriminator.js"; -import ServiceMap from "./serviceMap.js"; +import {app,logger} from "./app.js"; const APP_PORT = process.env.PORT; -const container = new Pimple(); -const app = express(); -const httpServer = http.createServer(app); - -container - .register(new LoggerServiceProvider()) - .register(new ServerServiceProvider(httpServer)) - .register(new SettingsServiceProvider()) - .register(new DeviceServiceProvider()) - .register(new ControllerServiceProvider()) - .register(new SocketServiceProvider()) - .register(new RepositoryServiceProvider()) - .register(new SerializationServiceProvider()) - .register(new AutomationServiceProvider()) - .register(new FactoryServiceProvider()) -; - -const logger = container.get('logger.default'); -const io = container.get('server.websocket'); -const deviceManager = container.get('device.manager'); -const scriptRuntime = container.get('automation.scriptRuntime'); - -container.get('device.provider.loader').loadFromSettings(); - -// Middlewares -app - .use(cors()) - .use(contentTypeMiddleware) - .use(express.json()) - .use(express.text()) -; - -// Routes -app.get('/health', asyncHandler((req, res) => { - const controller = container.get('controller.health') - return controller.execute(req, res) -})); - -app.get('/devices', (req, res) => { - const controller = container.get('controller.getDevices') - return controller.execute(req, res) -}); - -app.get('/device/:deviceId', (req, res) => { - const controller = container.get('controller.getDevice') - return controller.execute(req, res) -}); - -app.patch('/device/:deviceId', (req, res) => { - const controller = container.get('controller.patchDevice') - return controller.execute(req, res) -}); - -app.get('/automation/scripts', (req, res) => { - const controller = container.get('controller.automation.getScripts') - return controller.execute(req, res) -}); - -app.get('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { - const controller = container.get('controller.automation.getScript') - return controller.execute(req, res) -}); - -app.post('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { - const controller = container.get('controller.automation.createScript') - return controller.execute(req, res) -}); - -app.delete('/automation/scripts/:fileName([a-z\\d._-]+.js)', (req, res) => { - const controller = container.get('controller.automation.deleteScript') - return controller.execute(req, res) -}); - -app.get('/automation/log', asyncHandler((req, res) => { - const controller = container.get('controller.automation.getLog') - return controller.execute(req, res) -})); - -app.post('/automation/run', (req, res) => { - const controller = container.get('controller.automation.runScript') - return controller.execute(req, res) -}); - -app.get('/automation/stop', (req, res) => { - const controller = container.get('controller.automation.stopScript') - return controller.execute(req, res) -}); - -app.get('/automation/status', (req, res) => { - const controller = container.get('controller.automation.statusScript') - return controller.execute(req, res) -}); - -// Whenever someone connects this gets executed -io.on('connection', socket => { - logger.debug(`Client connected: ${socket.id}`); - - socket.on('disconnect', () => { - logger.debug(`Client disconnected: ${socket.id}`); - }); - - const deviceUpdateHandler = container.get('socket.deviceUpdateHandler'); - - socket.on(WebSocketEvent.deviceUpdateReceived, (data) => deviceUpdateHandler.handle(data as DeviceUpdateData)); -}); - -const serializer = container.get('serializer.classToPlain'); - -const deviceDiscriminator = DeviceDiscriminator.createClassTransformerTypeDiscriminator('type'); - -deviceManager.on(DeviceManagerEvent.deviceConnected, (device: Device) => { - io.emit(WebSocketEvent.deviceConnected, serializer.transform(device, deviceDiscriminator)); - scriptRuntime.runForEvent(DeviceManagerEvent.deviceConnected, device); -}); - -deviceManager.on(DeviceManagerEvent.deviceDisconnected, (device: Device) => { - io.emit(WebSocketEvent.deviceDisconnected, serializer.transform(device, deviceDiscriminator)); - scriptRuntime.runForEvent(DeviceManagerEvent.deviceDisconnected, device); -}); - -deviceManager.on(DeviceManagerEvent.deviceRefreshed, (device: Device) => { - io.emit(WebSocketEvent.deviceRefreshed, serializer.transform(device, deviceDiscriminator)); - scriptRuntime.runForEvent(DeviceManagerEvent.deviceRefreshed, device); -}); - -// Automation events -scriptRuntime.on(AutomationEventType.consoleLog, (data: any) => io.emit(AutomationEventType.consoleLog, data)); - -httpServer.listen(APP_PORT, () => { +app.listen(APP_PORT, () => { logger.info(`Node version: ${process.version}`); logger.info(`SlvCtrl+ server listening on port ${APP_PORT}!`); }); - -process.on('uncaughtException', (err: Error) => { - logger.error('Asynchronous error caught.', err); -}); diff --git a/tests/integration/device.spec.ts b/tests/integration/device.spec.ts new file mode 100644 index 0000000..bc5844a --- /dev/null +++ b/tests/integration/device.spec.ts @@ -0,0 +1,12 @@ +import request from "supertest" +import {app} from "../../src/app.js"; + +describe('Device controller', () => { + it('returns list of active devices', async () => { + const response = await request(app) + .get("/devices"); + + expect(response.status).toBe(200); + expect(response.body).toEqual({"count": 0, "items": []}); + }); +}); diff --git a/tests/integration/health.spec.ts b/tests/integration/health.spec.ts new file mode 100644 index 0000000..c1d0a23 --- /dev/null +++ b/tests/integration/health.spec.ts @@ -0,0 +1,13 @@ +import request from "supertest" +import {app} from "../../src/app.js"; + +describe('Health controller', () => { + it('returns health status', async () => { + const response = await request(app) + .get("/health"); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty("process"); + expect(response.body).toHaveProperty("system"); + }); +}); diff --git a/tests/unit/settings/settingsManager.spec.ts b/tests/unit/settings/settingsManager.spec.ts new file mode 100644 index 0000000..eaa4843 --- /dev/null +++ b/tests/unit/settings/settingsManager.spec.ts @@ -0,0 +1,22 @@ +import ConnectedDeviceRepository from "../../../src/repository/connectedDeviceRepository.js"; +import { mock } from "jest-mock-extended"; +import DeviceManager from "../../../src/device/deviceManager.js"; +import SettingsManager from "../../../src/settings/settingsManager.js"; +import PlainToClassSerializer from "../../../src/serialization/plainToClassSerializer.js"; +import ClassToPlainSerializer from "../../../src/serialization/classToPlainSerializer.js"; +import Logger from "../../../src/logging/Logger.js"; + +describe('settingsManager', () => { + it('can be instantiated', async () => { + const plainToClassSerializerMock = mock(); + const classToPlainSerializerMock = mock(); + const logger = mock(); + + const deviceRepository = new SettingsManager( + 'foo', + plainToClassSerializerMock, + classToPlainSerializerMock, + logger + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 42a1d91..c113829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1559,6 +1559,11 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== +"@types/cookiejar@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== + "@types/cors@^2.8.10", "@types/cors@^2.8.12": version "2.8.17" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" @@ -1638,6 +1643,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/methods@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== + "@types/mime@*": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" @@ -1723,6 +1733,23 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/superagent@^8.1.0": + version "8.1.3" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.3.tgz#6222a466e89eac9c84ad8de11870d92097e6554a" + integrity sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw== + dependencies: + "@types/cookiejar" "^2.1.5" + "@types/methods" "^1.1.4" + "@types/node" "*" + +"@types/supertest@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.2.tgz#2af1c466456aaf82c7c6106c6b5cbd73a5e86588" + integrity sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg== + dependencies: + "@types/methods" "^1.1.4" + "@types/superagent" "^8.1.0" + "@types/uuid@^8.3.0": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -1984,6 +2011,16 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -2333,6 +2370,13 @@ colorette@^2.0.7: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -2348,6 +2392,11 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +component-emitter@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2385,6 +2434,11 @@ cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + core-js-compat@^3.31.0, core-js-compat@^3.33.1: version "3.34.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.34.0.tgz#61a4931a13c52f8f08d924522bba65f8c94a5f17" @@ -2484,6 +2538,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -2499,6 +2558,14 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -3029,6 +3096,25 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3243,6 +3329,11 @@ help-me@^5.0.0: resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -4133,7 +4224,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -4151,7 +4242,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -4163,6 +4254,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -4332,10 +4428,10 @@ object.getownpropertydescriptors@^2.0.3: es-abstract "^1.22.1" safe-array-concat "^1.0.0" -on-change@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/on-change/-/on-change-4.0.2.tgz#838129790f09dc2ed04284944bda6e82b92c10b8" - integrity sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA== +on-change@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/on-change/-/on-change-5.0.1.tgz#ced60d262211eee41043e7479515b4875d1744ef" + integrity sha512-n7THCP7RkyReRSLkJb8kUWoNsxUIBxTkIp3JKno+sEz6o/9AJ3w3P9fzQkITEkMwyTKJjZciF3v/pVoouxZZMg== on-exit-leak-free@^2.1.0: version "2.1.2" @@ -4631,6 +4727,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -4852,7 +4955,7 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4: +semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -5156,6 +5259,30 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +superagent@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.4: + version "6.3.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.4.tgz#2145c250570c2ea5d337db3552dbfb78a2286218" + integrity sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw== + dependencies: + methods "^1.1.2" + superagent "^8.1.2" + supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"