diff --git a/package-lock.json b/package-lock.json index 9148521c2..a340fc600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5411,13 +5411,13 @@ "dev": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "dev": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -11500,6 +11500,17 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", diff --git a/src/modules/backup/backup.controller.ts b/src/modules/backup/backup.controller.ts index 6599739d3..e1f015815 100644 --- a/src/modules/backup/backup.controller.ts +++ b/src/modules/backup/backup.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Put, UseGuards, Res, Req, InternalServerErrorException } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiConsumes } from '@nestjs/swagger'; + import { BackupService } from './backup.service'; import { AdminGuard } from '../../core/auth/guards/admin.guard'; import { Logger } from '../../core/logger/logger.service'; diff --git a/src/modules/backup/backup.gateway.ts b/src/modules/backup/backup.gateway.ts index 5eee68dd8..608235f77 100644 --- a/src/modules/backup/backup.gateway.ts +++ b/src/modules/backup/backup.gateway.ts @@ -1,6 +1,8 @@ import * as color from 'bash-color'; +import { EventEmitter } from 'events'; import { UseGuards } from '@nestjs/common'; import { WebSocketGateway, SubscribeMessage, WsException } from '@nestjs/websockets'; + import { Logger } from '../../core/logger/logger.service'; import { WsAdminGuard } from '../../core/auth/guards/ws-admin-guard'; import { BackupService } from './backup.service'; @@ -14,9 +16,9 @@ export class BackupGateway { ) { } @SubscribeMessage('do-restore') - async doRestore(client, payload) { + async doRestore(client: EventEmitter) { try { - return await this.backupService.restoreFromBackup(payload, client); + return await this.backupService.restoreFromBackup(client); } catch (e) { this.logger.error(e); client.emit('stdout', '\n\r' + color.red(e.toString()) + '\n\r'); @@ -25,9 +27,9 @@ export class BackupGateway { } @SubscribeMessage('do-restore-hbfx') - async doRestoreHbfx(client, payload) { + async doRestoreHbfx(client: EventEmitter) { try { - return await this.backupService.restoreHbfxBackup(payload, client); + return await this.backupService.restoreHbfxBackup(client); } catch (e) { this.logger.error(e); client.emit('stdout', '\n\r' + color.red(e.toString()) + '\n\r'); diff --git a/src/modules/backup/backup.module.ts b/src/modules/backup/backup.module.ts index 443e35897..5e8ecdf2b 100644 --- a/src/modules/backup/backup.module.ts +++ b/src/modules/backup/backup.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; + import { ConfigModule } from '../../core/config/config.module'; import { LoggerModule } from '../../core/logger/logger.module'; import { BackupService } from './backup.service'; diff --git a/src/modules/backup/backup.service.ts b/src/modules/backup/backup.service.ts index 7e9c23a1d..4371627a9 100644 --- a/src/modules/backup/backup.service.ts +++ b/src/modules/backup/backup.service.ts @@ -4,8 +4,10 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import * as color from 'bash-color'; import * as unzipper from 'unzipper'; +import { EventEmitter } from 'events'; import * as child_process from 'child_process'; import { Injectable, BadRequestException } from '@nestjs/common'; + import { PluginsService } from '../plugins/plugins.service'; import { ConfigService, HomebridgeConfig } from '../../core/config/config.service'; import { Logger } from '../../core/logger/logger.service'; @@ -133,11 +135,13 @@ export class BackupService { /** * Restores the uploaded backup */ - async restoreFromBackup(payload, client) { + async restoreFromBackup(client: EventEmitter) { if (!this.restoreDirectory) { throw new BadRequestException(); } + console.log(this.restoreDirectory); + // check info.json exists if (!await fs.pathExists(path.resolve(this.restoreDirectory, 'info.json'))) { await this.removeRestoreDirectory(); @@ -267,7 +271,7 @@ export class BackupService { /** * Restore .hbfx backup file */ - async restoreHbfxBackup(payload, client) { + async restoreHbfxBackup(client: EventEmitter) { if (!this.restoreDirectory) { throw new BadRequestException(); } diff --git a/test/e2e/backup.e2e-spec.ts b/test/e2e/backup.e2e-spec.ts index 54dddcf3c..002ab22a9 100644 --- a/test/e2e/backup.e2e-spec.ts +++ b/test/e2e/backup.e2e-spec.ts @@ -1,12 +1,17 @@ import * as path from 'path'; import * as fs from 'fs-extra'; +import { EventEmitter } from 'events'; import { ValidationPipe } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; +import * as fastifyMultipart from 'fastify-multipart'; +import * as FormData from 'form-data'; import { AuthModule } from '../../src/core/auth/auth.module'; import { BackupModule } from '../../src/modules/backup/backup.module'; import { BackupService } from '../../src/modules/backup/backup.service'; +import { BackupGateway } from '../../src/modules/backup/backup.gateway'; +import { PluginsService } from '../../src/modules/plugins/plugins.service'; describe('BackupController (e2e)', () => { let app: NestFastifyApplication; @@ -14,16 +19,22 @@ describe('BackupController (e2e)', () => { let authFilePath: string; let secretsFilePath: string; let authorization: string; + let tempBackupPath: string; + let backupService: BackupService; + let backupGateway: BackupGateway; + let pluginsService: PluginsService; let postBackupRestoreRestartFn; beforeAll(async () => { process.env.UIX_BASE_PATH = path.resolve(__dirname, '../../'); process.env.UIX_STORAGE_PATH = path.resolve(__dirname, '../', '.homebridge'); process.env.UIX_CONFIG_PATH = path.resolve(process.env.UIX_STORAGE_PATH, 'config.json'); + process.env.UIX_CUSTOM_PLUGIN_PATH = path.resolve(process.env.UIX_STORAGE_PATH, 'plugins/node_modules'); authFilePath = path.resolve(process.env.UIX_STORAGE_PATH, 'auth.json'); secretsFilePath = path.resolve(process.env.UIX_STORAGE_PATH, '.uix-secrets'); + tempBackupPath = path.resolve(process.env.UIX_STORAGE_PATH, 'backup.tar.gz'); // setup test config await fs.copy(path.resolve(__dirname, '../mocks', 'config.json'), process.env.UIX_CONFIG_PATH); @@ -36,7 +47,15 @@ describe('BackupController (e2e)', () => { imports: [BackupModule, AuthModule], }).compile(); - app = moduleFixture.createNestApplication(new FastifyAdapter()); + const fAdapter = new FastifyAdapter(); + + fAdapter.register(fastifyMultipart, { + limits: { + files: 1, + }, + }); + + app = moduleFixture.createNestApplication(fAdapter); app.useGlobalPipes(new ValidationPipe({ whitelist: true, @@ -47,6 +66,8 @@ describe('BackupController (e2e)', () => { await app.getHttpAdapter().getInstance().ready(); backupService = app.get(BackupService); + backupGateway = app.get(BackupGateway); + pluginsService = app.get(PluginsService); }); beforeEach(async () => { @@ -78,6 +99,70 @@ describe('BackupController (e2e)', () => { expect(res.headers['content-type']).toEqual('application/octet-stream'); }); + it('POST /backup/restore', async () => { + // get a new backup + const downloadBackup = await app.inject({ + method: 'GET', + path: '/backup/download', + headers: { + authorization, + } + }); + + // save the backup to disk + await fs.writeFile(tempBackupPath, downloadBackup.rawPayload); + + // create multi-part form + const payload = new FormData(); + payload.append('backup.tar.gz', await fs.readFile(tempBackupPath)); + + const headers = payload.getHeaders(); + headers.authorization = authorization; + + const res = await app.inject({ + method: 'POST', + path: '/backup/restore', + headers, + payload, + }); + + expect(res.statusCode).toEqual(200); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + // check the backup contains the required files + const restoreDirectory = (backupService as any).restoreDirectory; + const pluginsJson = path.join(restoreDirectory, 'plugins.json'); + const infoJson = path.join(restoreDirectory, 'info.json'); + + expect(await fs.pathExists(pluginsJson)).toEqual(true); + expect(await fs.pathExists(infoJson)).toEqual(true); + + // mark the "homebridge-mock-plugin" dummy plugin as public so we can test the mock install + const installedPlugins = (await fs.readJson(pluginsJson)).map(x => { + x.publicPackage = true; + return x; + }); + await fs.writeJson(pluginsJson, installedPlugins); + + // create some mocks + const client = new EventEmitter(); + + jest.spyOn(client, 'emit'); + + jest.spyOn(pluginsService, 'installPlugin') + .mockImplementation(async () => { + return true; + }); + + // start restore + await backupGateway.doRestore(client); + + expect(client.emit).toBeCalledWith('stdout', expect.stringContaining('Restoring backup')); + expect(client.emit).toBeCalledWith('stdout', expect.stringContaining('Restore Complete')); + expect(pluginsService.installPlugin).toBeCalledWith('homebridge-mock-plugin', client); + }); + it('GET /backup/restart', async () => { const res = await app.inject({ method: 'PUT', diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 15f50e934..fd9608f41 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -10,7 +10,6 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverage": true, "collectCoverageFrom": [ "**/*.ts" ],