diff --git a/__tests__/bin/vip-app-deploy-validate.js b/__tests__/bin/vip-app-deploy-validate.js new file mode 100644 index 000000000..dd44336c8 --- /dev/null +++ b/__tests__/bin/vip-app-deploy-validate.js @@ -0,0 +1,127 @@ +import { appDeployValidateCmd } from '../../src/bin/vip-app-deploy-validate'; +import { getFileMeta } from '../../src/lib/client-file-uploader'; +import * as exit from '../../src/lib/cli/exit'; +import { validateFile } from '../../src/lib/custom-deploy/custom-deploy'; +import { + validateName, + validateZipFile, + validateTarFile, +} from '../../src/lib/validations/custom-deploy'; + +jest.mock( '../../src/lib/client-file-uploader', () => ( { + ...jest.requireActual( '../../src/lib/client-file-uploader' ), + getFileMeta: jest + .fn() + .mockResolvedValue( { fileName: '/vip/skeleton.zip', basename: 'skeleton.zip' } ), +} ) ); + +jest.mock( '../../src/lib/custom-deploy/custom-deploy', () => ( { + validateFile: jest.fn(), +} ) ); + +jest.mock( '../../src/lib/validations/custom-deploy', () => { + return { + ...jest.requireActual( '../../src/lib/validations/custom-deploy' ), + validateZipFile: jest.fn(), + validateTarFile: jest.fn(), + }; +} ); + +jest.mock( '../../src/lib/cli/command', () => { + const commandMock = { + argv: () => commandMock, + examples: () => commandMock, + option: () => commandMock, + command: () => commandMock, + }; + return jest.fn( () => commandMock ); +} ); + +const exitSpy = jest.spyOn( exit, 'withError' ); +jest.spyOn( process, 'exit' ).mockImplementation( () => {} ); +jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + +const opts = { + app: { + id: 1, + organization: { + id: 2, + }, + }, + env: { + id: 3, + type: 'develop', + }, + force: true, +}; + +describe( 'vip-app-deploy-validate', () => { + describe( 'validateName', () => { + beforeEach( async () => { + exitSpy.mockClear(); + } ); + + it.each( [ '!client-mu-plugins', '..vip-go-skeleton', '*test' ] )( + 'fails if the file has has invalid characters for directories', + basename => { + validateName( basename, true ); + expect( exitSpy ).toHaveBeenCalledWith( + `Filename ${ basename } contains disallowed characters: [!:*?"<>|\'/^\\.\\.]+` + ); + } + ); + + it.each( [ 'client-mu-plugins', 'vip-go-skeleton/', '._vip-go-skeleton' ] )( + 'passes if the file has has valid characters for directories', + basename => { + validateName( basename, true ); + expect( exitSpy ).not.toHaveBeenCalled(); + } + ); + + it.each( [ 'client-mu-/plugins.php', 'vip-?config.php' ] )( + 'fails if the file has has invalid characters for non-directories', + basename => { + validateName( basename, false ); + expect( exitSpy ).toHaveBeenCalledWith( + `Filename ${ basename } contains disallowed characters: [!/:*?"<>|\'/^\\.\\.]+` + ); + } + ); + + it.each( [ 'client-mu-plugins.php', 'vip-config.php' ] )( + 'passes if the file has has valid characters for non-directories', + basename => { + validateName( basename, false ); + expect( exitSpy ).not.toHaveBeenCalled(); + } + ); + } ); + + describe( 'appDeployValidateCmd', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should call expected functions (zip)', async () => { + const args = [ '/vip/skeleton.zip' ]; + getFileMeta.mockResolvedValue( { fileName: '/vip/skeleton.zip', basename: 'skeleton.zip' } ); + + await appDeployValidateCmd( args, opts ); + expect( validateFile ).toHaveBeenCalledTimes( 1 ); + expect( validateZipFile ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should call expected functions (tar)', async () => { + const args = [ '/vip/foo.tar.gz' ]; + getFileMeta.mockResolvedValue( { + fileName: '/vip/foo.tar.gz', + basename: 'foo.tar.gz', + } ); + + await appDeployValidateCmd( args, opts ); + expect( validateFile ).toHaveBeenCalledTimes( 1 ); + expect( validateTarFile ).toHaveBeenCalledTimes( 1 ); + } ); + } ); +} ); diff --git a/__tests__/bin/vip-app-deploy.js b/__tests__/bin/vip-app-deploy.js index d6d2c085c..70e33a0ea 100644 --- a/__tests__/bin/vip-app-deploy.js +++ b/__tests__/bin/vip-app-deploy.js @@ -36,6 +36,7 @@ jest.mock( '../../src/lib/cli/command', () => { argv: () => commandMock, examples: () => commandMock, option: () => commandMock, + command: () => commandMock, }; return jest.fn( () => commandMock ); } ); @@ -70,7 +71,7 @@ describe( 'vip-app-deploy', () => { async basename => { validateFilename( basename ); expect( exitSpy ).toHaveBeenCalledWith( - 'Error: The characters used in the name of a file for custom deploys are limited to [0-9,a-z,A-Z,-,_,.]' + `Filename ${ basename } contains disallowed characters: [0-9,a-z,A-Z,-,_,.]` ); } ); diff --git a/src/lib/custom-deploy/custom-deploy.ts b/src/lib/custom-deploy/custom-deploy.ts index dbf8ea6fc..354ef7f69 100644 --- a/src/lib/custom-deploy/custom-deploy.ts +++ b/src/lib/custom-deploy/custom-deploy.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import gql from 'graphql-tag'; -import debugLib from 'debug'; import API from '../../lib/api'; import * as exit from '../../lib/cli/exit'; diff --git a/src/lib/validations/custom-deploy.ts b/src/lib/validations/custom-deploy.ts index 4b8713ced..debd137de 100644 --- a/src/lib/validations/custom-deploy.ts +++ b/src/lib/validations/custom-deploy.ts @@ -2,12 +2,9 @@ import path from 'path'; import AdmZip from 'adm-zip'; import { constants } from 'node:fs'; import * as tar from 'tar'; -import debugLib from 'debug'; import * as exit from '../../lib/cli/exit'; -const debug = debugLib( '@automattic/vip:bin:vip-lib-deploy-validate' ); - interface TarEntry { path: string; type: string; @@ -20,7 +17,7 @@ const errorMessages = { singleRootDir: 'The compressed file must contain a single root directory!', invalidExt: 'Invalid file extension. Please provide a .zip, .tar.gz, or a .tgz file.', invalidChars: ( filename: string, invalidChars: string ) => - `Filename "${ filename }" contains disallowed characters: ${ invalidChars }`, + `Filename ${ filename } contains disallowed characters: ${ invalidChars }`, }; const symlinkIgnorePattern = /\/node_modules\/[^/]+\/\.bin\//; const macosxDir = '__MACOSX'; @@ -52,7 +49,7 @@ export function validateFilename( filename: string ) { const re = /^[a-z0-9\-_.]+$/i; if ( ! re.test( filename ) ) { - exit.withError( errorMessages.invalidChars( filename, re.toString() ) ); + exit.withError( errorMessages.invalidChars( filename, '[0-9,a-z,A-Z,-,_,.]' ) ); } } @@ -62,13 +59,13 @@ export function validateFilename( filename: string ) { * @param {string} name The name of the file * @param {bool} isDirectory Whether the file is a directory */ -function validateName( name: string, isDirectory: boolean ) { +export function validateName( name: string, isDirectory: boolean ) { if ( name.startsWith( '._' ) ) { return; } const invalidCharsPattern = isDirectory ? /[!\:*?"<>|']|^\.\..*$/ : /[!\/:*?"<>|']|^\.\..*$/; - const errorMessage = errorMessages.invalidChars( name, invalidCharsPattern.toString() ); + const errorMessage = errorMessages.invalidChars( name, isDirectory ? '[!:*?"<>|\'/^\\.\\.]+' : '[!/:*?"<>|\'/^\\.\\.]+' ); if ( invalidCharsPattern.test( name ) ) { exit.withError( errorMessage ); }