diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 97fe6d783..1cf129a94 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -34,7 +34,7 @@ import { resolveProjectEntries } from '../projects/resolveProjects' import { withLabel } from '../reporters/renderers/utils' import { BaseSequencer } from '../sequencers/BaseSequencer' import { RandomSequencer } from '../sequencers/RandomSequencer' -import { API_TOKEN_FILE, resolveApiToken } from './apiToken' +import { resolveApiToken } from './apiToken' import { PluginHarness } from './pluginHarness' function resolvePath(path: string, root: string) { @@ -1169,7 +1169,6 @@ export async function resolveConfig( rootViteConfig.server.fs.allow.push( ...resolveFsAllow(rootViteConfig.root, rootViteConfig.configFile), ) - rootViteConfig.server.fs.deny.push(API_TOKEN_FILE) // the server has been created, we don't need to override vite.server options const { token, tokenCreated } = resolveApiToken(rootViteConfig.root) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index ef5c2afb5..9b6dd125e 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -8,7 +8,7 @@ import { MetaEnvReplacerPlugin } from './metaEnvReplacer' import { MocksPlugins } from './mocks' import { NormalizeURLPlugin } from './normalizeURL' import { SsrRunnerFixerPlugin } from './ssrRunnerFixer' -import { normalizeUIOptions, resolveTestCacheDir } from './utils' +import { appendApiTokenFsDeny, normalizeUIOptions, resolveTestCacheDir } from './utils' import { VitestCoreResolver } from './vitestResolver' export function VitestCorePlugin(harness: PluginHarness): VitePlugin[] { @@ -23,6 +23,7 @@ export function VitestCorePlugin(harness: PluginHarness): VitePlugin[] { order: 'post', handler(viteConfig) { const root = resolve(viteConfig.test?.root || viteConfig.root || process.cwd()) + appendApiTokenFsDeny(viteConfig) return { base: '/', diff --git a/packages/vitest/src/node/plugins/utils.ts b/packages/vitest/src/node/plugins/utils.ts index 92c2c19fa..3209efafa 100644 --- a/packages/vitest/src/node/plugins/utils.ts +++ b/packages/vitest/src/node/plugins/utils.ts @@ -5,6 +5,7 @@ import { dirname, resolve } from 'pathe' import { searchForWorkspaceRoot, version as viteVersion } from 'vite' import * as vite from 'vite' import { rootDir } from '../../paths' +import { API_TOKEN_FILE } from '../config/apiToken' import { hash } from '../hash' export function resolveTestCacheDir( @@ -127,6 +128,28 @@ export function deleteDefineConfig(viteConfig: ViteConfig): DefineConfigResult { return { defines, scriptDefines } } +// TODO: use Vite's own defaults once they are exported (request upstream) +// copied from Vite 8.3 https://github.com/vitejs/vite/blob/v8.3.4/packages/vite/src/node/server/index.ts#L1254-L1261 +const VITE_DEFAULT_FS_DENY = [ + '.env', + '.env.*', + '*.{crt,pem,key,p12,pfx,cer,der}', + '.npmrc', + '.yarnrc.yml', + '**/.git/**', +] + +// Vite drops its default `server.fs.deny` once any value is set, +// so the defaults are kept explicitly when the user didn't set one. +export function appendApiTokenFsDeny(viteConfig: ViteConfig): void { + viteConfig.server ??= {} + viteConfig.server.fs ??= {} + viteConfig.server.fs.deny = [ + ...(viteConfig.server.fs.deny ?? VITE_DEFAULT_FS_DENY), + API_TOKEN_FILE, + ] +} + export function resolveFsAllow( projectRoot: string, rootConfigFile: string | false | undefined, diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index e8fc4af27..2230dbe56 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -2,7 +2,6 @@ import type * as vite from 'vite' import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' import type { PluginHarness } from '../config/pluginHarness' import { resolve } from 'pathe' -import { API_TOKEN_FILE } from '../config/apiToken' import { ViteConfigPlugin } from './config' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' @@ -10,7 +9,7 @@ import { MetaEnvReplacerPlugin } from './metaEnvReplacer' import { MocksPlugins } from './mocks' import { NormalizeURLPlugin } from './normalizeURL' import { SsrRunnerFixerPlugin } from './ssrRunnerFixer' -import { resolveTestCacheDir } from './utils' +import { appendApiTokenFsDeny, resolveTestCacheDir } from './utils' import { VitestProjectResolver } from './vitestResolver' export function ProjectRootPlugin(projectRoot: string): VitePlugin { @@ -44,6 +43,7 @@ export function WorkspaceVitestPlugin( config(viteConfig) { const testConfig = viteConfig.test || {} const root = testConfig.root || viteConfig.root + appendApiTokenFsDeny(viteConfig) const config: ViteConfig = { base: '/', @@ -57,7 +57,6 @@ export function WorkspaceVitestPlugin( open: false, fs: { allow: globalViteConfig.server.fs.allow, - deny: [API_TOKEN_FILE], }, }, } diff --git a/test/e2e/test/fs-deny.test.ts b/test/e2e/test/fs-deny.test.ts new file mode 100644 index 000000000..cc909d527 --- /dev/null +++ b/test/e2e/test/fs-deny.test.ts @@ -0,0 +1,93 @@ +import type { ViteUserConfig } from 'vitest/config' +import { expect, test } from 'vitest' +import { isFileLoadingAllowed, resolveConfig } from 'vitest/node' + +// checks the root server, an inline project sharing it, and a project +// with its own server (`alias` keeps it from sharing the root server) +async function resolveDeniedFiles(viteConfig: ViteUserConfig = {}) { + const config = await resolveConfig( + { config: false }, + { + ...viteConfig, + test: { + projects: [ + { test: { name: 'shared' } }, + { test: { name: 'own-server', alias: { foo: '/foo' } } }, + ], + }, + }, + ) + expect(config.test.resolvedProjects.map((p) => p.viteConfig === config)).toEqual([true, false]) + + const files = ['.env', 'server.pem', 'secret.txt', 'node_modules/.vitest/.vitest-secret-token'] + const serverConfigs = { + root: config, + ...Object.fromEntries( + config.test.resolvedProjects.map((p) => [p.projectConfig.name, p.viteConfig]), + ), + } + return Object.fromEntries( + Object.entries(serverConfigs).map(([name, serverConfig]) => [ + name, + files.filter((file) => !isFileLoadingAllowed(serverConfig, `${config.root}/${file}`)), + ]), + ) +} + +test('keeps vite default fs.deny and denies the api token file', async () => { + expect(await resolveDeniedFiles()).toMatchInlineSnapshot(` + { + "own-server": [ + ".env", + "server.pem", + "node_modules/.vitest/.vitest-secret-token", + ], + "root": [ + ".env", + "server.pem", + "node_modules/.vitest/.vitest-secret-token", + ], + "shared": [ + ".env", + "server.pem", + "node_modules/.vitest/.vitest-secret-token", + ], + } + `) +}) + +test('user fs.deny replaces vite defaults but still denies the api token file', async () => { + expect(await resolveDeniedFiles({ server: { fs: { deny: ['secret.txt'] } } })) + .toMatchInlineSnapshot(` + { + "own-server": [ + "secret.txt", + "node_modules/.vitest/.vitest-secret-token", + ], + "root": [ + "secret.txt", + "node_modules/.vitest/.vitest-secret-token", + ], + "shared": [ + "secret.txt", + "node_modules/.vitest/.vitest-secret-token", + ], + } + `) +}) + +test('keeps vite default fs.deny', async () => { + const config = await resolveConfig({ config: false }) + // newest Vite default + api token file + expect(config.server.fs.deny).toMatchInlineSnapshot(` + [ + ".env", + ".env.*", + "*.{crt,pem,key,p12,pfx,cer,der}", + ".npmrc", + ".yarnrc.yml", + "**/.git/**", + ".vitest-secret-token", + ] + `) +})