From 5f817428cae42c88aac1d751239d1d937501ef93 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 12 Feb 2020 22:48:21 +0100 Subject: [PATCH] fix: verify `rootDir` and all `roots` are directories (#9569) --- CHANGELOG.md | 1 + e2e/__tests__/existentRoots.test.ts | 76 +++++++++++++++++++ .../src/__tests__/normalize.test.js | 12 ++- packages/jest-config/src/normalize.ts | 40 ++++++++++ .../src/__tests__/SearchSource.test.ts | 15 ++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 e2e/__tests__/existentRoots.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5f6e37a23f..eff713ac0b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `[jest-config]` Treat `setupFilesAfterEnv` like `setupFiles` when normalizing configs against presets ([#9495](https://github.com/facebook/jest/pull/9495)) - `[jest-config]` Support `.mjs` config files on Windows as well ([#9558](https://github.com/facebook/jest/pull/9558)) +- `[jest-config]` Verify `rootDir` and all `roots` are directories ([#9569](https://github.com/facebook/jest/pull/9569)) - `[jest-cli]` Set `coverageProvider` correctly when provided in config ([#9562](https://github.com/facebook/jest/pull/9562)) - `[jest-config]` Ensure pattern of `replacePosixSep` is a string ([#9546]https://github.com/facebook/jest/pull/9546) - `[jest-matcher-utils]` Fix diff highlight of symbol-keyed object. ([#9499](https://github.com/facebook/jest/pull/9499)) diff --git a/e2e/__tests__/existentRoots.test.ts b/e2e/__tests__/existentRoots.test.ts new file mode 100644 index 000000000000..358e28323c24 --- /dev/null +++ b/e2e/__tests__/existentRoots.test.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import {tmpdir} from 'os'; +import {skipSuiteOnWindows} from '@jest/test-utils'; +import {cleanup, writeFiles} from '../Utils'; +import runJest from '../runJest'; + +const DIR = path.resolve(tmpdir(), 'existent-roots'); + +beforeEach(() => cleanup(DIR)); +afterAll(() => cleanup(DIR)); + +skipSuiteOnWindows(); + +function writeConfig(rootDir: string, roots?: Array) { + writeFiles(DIR, { + 'jest.config.js': ` + module.exports = ${JSON.stringify({rootDir, roots}, null, 2)}; + `, + 'package.json': '{}', + }); +} + +test('error when rootDir does not exist', () => { + const fakeRootDir = path.join(DIR, 'foobar'); + writeConfig(fakeRootDir); + + const {exitCode, stderr} = runJest(DIR); + + expect(exitCode).toBe(1); + expect(stderr).toContain( + `Directory ${fakeRootDir} in the rootDir option was not found.`, + ); +}); + +test('error when rootDir is a file', () => { + const fakeRootDir = path.join(DIR, 'jest.config.js'); + writeConfig(fakeRootDir); + + const {exitCode, stderr} = runJest(DIR); + + expect(exitCode).toBe(1); + expect(stderr).toContain( + `${fakeRootDir} in the rootDir option is not a directory.`, + ); +}); + +test('error when roots directory does not exist', () => { + const fakeRootDir = path.join(DIR, 'foobar'); + writeConfig(DIR, ['', fakeRootDir]); + + const {exitCode, stderr} = runJest(DIR); + + expect(exitCode).toBe(1); + expect(stderr).toContain( + `Directory ${fakeRootDir} in the roots[1] option was not found.`, + ); +}); + +test('error when roots is a file', () => { + const fakeRootDir = path.join(DIR, 'jest.config.js'); + writeConfig(DIR, ['', fakeRootDir]); + + const {exitCode, stderr} = runJest(DIR); + + expect(exitCode).toBe(1); + expect(stderr).toContain( + `${fakeRootDir} in the roots[1] option is not a directory.`, + ); +}); diff --git a/packages/jest-config/src/__tests__/normalize.test.js b/packages/jest-config/src/__tests__/normalize.test.js index ce238fe2afd8..1d6f786ee06e 100644 --- a/packages/jest-config/src/__tests__/normalize.test.js +++ b/packages/jest-config/src/__tests__/normalize.test.js @@ -16,7 +16,17 @@ import {DEFAULT_JS_PATTERN} from '../constants'; const DEFAULT_CSS_PATTERN = '^.+\\.(css)$'; -jest.mock('jest-resolve').mock('path', () => jest.requireActual('path').posix); +jest + .mock('jest-resolve') + .mock('path', () => jest.requireActual('path').posix) + .mock('fs', () => { + const realFs = jest.requireActual('fs'); + + return { + ...realFs, + statSync: () => ({isDirectory: () => true}), + }; + }); let root; let expectedPathFooBar; diff --git a/packages/jest-config/src/normalize.ts b/packages/jest-config/src/normalize.ts index ea72cdef7f85..dafd3f0232e2 100644 --- a/packages/jest-config/src/normalize.ts +++ b/packages/jest-config/src/normalize.ts @@ -6,6 +6,7 @@ */ import {createHash} from 'crypto'; +import {statSync} from 'fs'; import * as path from 'path'; import {sync as glob} from 'glob'; import {Config} from '@jest/types'; @@ -46,6 +47,39 @@ type AllOptions = Config.ProjectConfig & Config.GlobalConfig; const createConfigError = (message: string) => new ValidationError(ERROR, message, DOCUMENTATION_NOTE); +function verifyDirectoryExists(path: Config.Path, key: string) { + try { + const rootStat = statSync(path); + + if (!rootStat.isDirectory()) { + throw createConfigError( + ` ${chalk.bold(path)} in the ${chalk.bold( + key, + )} option is not a directory.`, + ); + } + } catch (err) { + if (err instanceof ValidationError) { + throw err; + } + + if (err.code === 'ENOENT') { + throw createConfigError( + ` Directory ${chalk.bold(path)} in the ${chalk.bold( + key, + )} option was not found.`, + ); + } + + // Not sure in which cases `statSync` can throw, so let's just show the underlying error to the user + throw createConfigError( + ` Got an error trying to find ${chalk.bold(path)} in the ${chalk.bold( + key, + )} option.\n\n Error was: ${err.message}`, + ); + } +} + // TS 3.5 forces us to split these into 2 const mergeModuleNameMapperWithPreset = ( options: Config.InitialOptionsWithRootDir, @@ -366,6 +400,8 @@ const normalizeRootDir = ( // ignored } + verifyDirectoryExists(options.rootDir, 'rootDir'); + return { ...options, rootDir: options.rootDir, @@ -918,6 +954,10 @@ export default function normalize( return newOptions; }, newOptions); + newOptions.roots.forEach((root, i) => { + verifyDirectoryExists(root, `roots[${i}]`); + }); + try { // try to resolve windows short paths, ignoring errors (permission errors, mostly) newOptions.cwd = realpath(process.cwd()); diff --git a/packages/jest-core/src/__tests__/SearchSource.test.ts b/packages/jest-core/src/__tests__/SearchSource.test.ts index acfb1d6f0bcc..a3a8d05a4067 100644 --- a/packages/jest-core/src/__tests__/SearchSource.test.ts +++ b/packages/jest-core/src/__tests__/SearchSource.test.ts @@ -15,6 +15,21 @@ import SearchSource, {SearchResult} from '../SearchSource'; jest.setTimeout(15000); +jest.mock('fs', () => { + const realFs = jest.requireActual('fs'); + + return { + ...realFs, + statSync: path => { + if (path === '/foo/bar/prefix') { + return {isDirectory: () => true}; + } + + return realFs.statSync(path); + }, + }; +}); + const rootDir = path.resolve(__dirname, 'test_root'); const testRegex = path.sep + '__testtests__' + path.sep; const testMatch = ['**/__testtests__/**/*'];