From 9ab72027c96928a310595ef5e335a7d79670950b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Weslley=20Ara=C3=BAjo?= <46850407+wellwelwel@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:45:46 -0300 Subject: [PATCH] fix(assert): `Map`, `Set` and `Symbol` aren't parsed (#405) * fix(assert): `Map`, `Set` and `Symbol` aren't parsed * chore: improve coverage * ci: fix Deno imports * ci: fix sep * ci: fix sep * ci: fix sep --- package.json | 4 +- src/helpers/parse-assertion.ts | 17 +- src/modules/describe.ts | 3 + src/modules/it.ts | 3 + src/modules/list-files.ts | 163 +++++++----------- src/modules/test.ts | 3 + src/polyfills/fs.ts | 35 ++-- src/polyfills/object.ts | 6 +- src/services/run-tests.ts | 4 +- test/docker/deno/latest.Dockerfile | 2 +- test/docker/playground/deno/Dockerfile | 2 +- ...ype.test.ts => assert-result-type.test.ts} | 62 ++++--- test/unit/map-tests.test.ts | 4 +- test/unit/sanitize-paths.test.ts | 48 ++++++ 14 files changed, 212 insertions(+), 144 deletions(-) rename test/unit/{assert.result-type.test.ts => assert-result-type.test.ts} (77%) create mode 100644 test/unit/sanitize-paths.test.ts diff --git a/package.json b/package.json index 2013f3d8..0040d8d5 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "test:deno:parallel": "tsx src/bin/index.ts --platform=\"deno\" --deno-allow=\"all\" --deno-cjs --parallel --include=\"ci/test/unit,ci/test/integration,ci/test/e2e\"", "test:c8:sequential": "c8 tsx src/bin/index.ts --include=\"test/unit,test/integration,test/e2e\"", "test:c8:parallel": "c8 tsx src/bin/index.ts --parallel --include=\"test/unit,test/integration,test/e2e\"", - "test:c8:sequential:options": "c8 tsx src/bin/index.ts --fast-fail --debug --exclude=\".bak\" --kill-port=\"4000\" --kill-range=\"4000-4001\" --include=\"test/unit,test/integration,test/e2e\"", - "test:c8:parallel:options": "c8 tsx src/bin/index.ts --parallel --concurrency=\"4\" --fast-fail --debug --exclude=\".bak\" --kill-port=\"4000\" --kill-range=\"4000-4001\" --include=\"test/unit,test/integration,test/e2e\"", + "test:c8:sequential:options": "c8 tsx src/bin/index.ts --platform=\"node\" --fast-fail --debug --exclude=\".bak\" --kill-port=\"4000\" --kill-range=\"4000-4001\" --include=\"test/unit,test/integration,test/e2e\" --filter=\".test.|.spec.\"", + "test:c8:parallel:options": "c8 tsx src/bin/index.ts --parallel --concurrency=\"4\" --platform=\"node\" --fast-fail --debug --exclude=\".bak\" --kill-port=\"4000\" --kill-range=\"4000-4001\" --include=\"test/unit,test/integration,test/e2e\" --filter=\".test.|.spec.\"", "test:ci": "tsx ./test/ci.test.ts", "test:ci:node": "FILTER='node-' npm run test:ci", "test:ci:bun": "FILTER='bun-' npm run test:ci", diff --git a/src/helpers/parse-assertion.ts b/src/helpers/parse-assertion.ts index a03b7445..3fce2e95 100644 --- a/src/helpers/parse-assertion.ts +++ b/src/helpers/parse-assertion.ts @@ -19,16 +19,25 @@ const cwd = process.cwd(); export const parseResultType = (type?: unknown): string => { const recurse = (value: unknown): unknown => { - if (typeof value === 'undefined') return 'undefined'; - if ( + typeof value === 'undefined' || typeof value === 'function' || typeof value === 'bigint' || + typeof value === 'symbol' || value instanceof RegExp ) return String(value); if (Array.isArray(value)) return value.map(recurse); + if (value instanceof Set) return Array.from(value).map(recurse); + /* c8 ignore start */ + if (value instanceof Map) + return recurse( + !nodeVersion || nodeVersion >= 12 + ? Object.fromEntries(value) + : fromEntries(value) + ); + /* c8 ignore stop */ /* c8 ignore start */ if (value !== null && typeof value === 'object') { @@ -66,7 +75,9 @@ export const parseAssertion = async ( try { if (typeof each.before.cb === 'function' && each.before.assert) { const beforeResult = each.before.cb(); + /* c8 ignore next */ if (beforeResult instanceof Promise) await beforeResult; + /* c8 ignore next */ } const cbResult = cb(); @@ -74,7 +85,9 @@ export const parseAssertion = async ( if (typeof each.after.cb === 'function' && each.after.assert) { const afterResult = each.after.cb(); + /* c8 ignore next */ if (afterResult instanceof Promise) await afterResult; + /* c8 ignore next */ } if (typeof options.message === 'string') { diff --git a/src/modules/describe.ts b/src/modules/describe.ts index f7fd540e..efe9fcd4 100644 --- a/src/modules/describe.ts +++ b/src/modules/describe.ts @@ -1,3 +1,4 @@ +/* c8 ignore next */ import process from 'node:process'; import { format, backgroundColor } from '../helpers/format.js'; import { write } from '../helpers/logs.js'; @@ -6,6 +7,7 @@ import { indentation } from '../configs/indentation.js'; /* c8 ignore next */ import type { DescribeOptions } from '../@types/describe.js'; +/* c8 ignore start */ /** * On **Poku**, `describe` also can be used just as a pretty `console.log` to title your test suites in the terminal. */ @@ -21,6 +23,7 @@ export async function describe( arg1: string | (() => unknown | Promise), arg2?: (() => unknown | Promise) | DescribeOptions ): Promise { + /* c8 ignore stop */ let title: string | undefined; let cb: (() => unknown | Promise) | undefined; let options: DescribeOptions | undefined; diff --git a/src/modules/it.ts b/src/modules/it.ts index ddfd9559..9ace47cd 100644 --- a/src/modules/it.ts +++ b/src/modules/it.ts @@ -1,3 +1,4 @@ +/* c8 ignore next */ import process from 'node:process'; /* c8 ignore next */ import { each } from '../configs/each.js'; @@ -6,6 +7,7 @@ import { indentation } from '../configs/indentation.js'; import { format } from '../helpers/format.js'; import { write } from '../helpers/logs.js'; +/* c8 ignore start */ export async function it( message: string, cb: () => Promise @@ -19,6 +21,7 @@ export async function it( (() => unknown | Promise)?, ] ): Promise { + /* c8 ignore stop */ let message: string | undefined; let cb: () => unknown | Promise; diff --git a/src/modules/list-files.ts b/src/modules/list-files.ts index 097db0e6..aaf15e26 100644 --- a/src/modules/list-files.ts +++ b/src/modules/list-files.ts @@ -1,6 +1,7 @@ +/* c8 ignore next */ import process from 'node:process'; -import { readdir, stat as fsStat } from 'node:fs'; import { sep, join } from 'node:path'; +import { readdir, stat as fsStat } from '../polyfills/fs.js'; /* c8 ignore next */ import type { Configs } from '../@types/list-files.js'; @@ -10,121 +11,79 @@ export const sanitizePath = (input: string, ensureTarget?: boolean): string => { .replace(/(\.\.(\/|\\|$))+/g, '') // ensure the current path level .replace(/[<>|^?*]+/g, ''); // removing unusual path characters - return ensureTarget ? sanitizedPath.replace(/^[/\\]/, './') : sanitizedPath; + // Preventing absolute path access + return ensureTarget + ? sanitizedPath.replace(/^[/\\]/, `.${sep}`) + : sanitizedPath; }; +/* c8 ignore start */ +export const isFile = async (fullPath: string) => + (await fsStat(fullPath)).isFile(); +/* c8 ignore stop */ + +/* c8 ignore start */ export const escapeRegExp = (string: string) => string.replace(/[.*{}[\]\\]/g, '\\$&'); +/* c8 ignore stop */ -export const isFile = (fullPath: string): Promise => { - return new Promise((resolve, reject) => { - fsStat(fullPath, (err, stats) => { - /* c8 ignore next */ - if (err) return reject(err); - - resolve(stats.isFile()); - }); - }); -}; - +/* c8 ignore start */ const envFilter = process.env.FILTER?.trim() ? new RegExp(escapeRegExp(process.env.FILTER), 'i') : undefined; +/* c8 ignore stop */ -const getAllFiles = ( +export const getAllFiles = async ( dirPath: string, files: string[] = [], - configs?: Configs, - callback?: (err: NodeJS.ErrnoException | null, result?: string[]) => void -) => { - readdir(sanitizePath(dirPath), (err, currentFiles) => { - if (err) return callback?.(err); - - const defaultRegExp = /\.(test|spec)\./i; - const filter: RegExp = - (envFilter - ? envFilter - : configs?.filter instanceof RegExp - ? configs.filter - : defaultRegExp) || defaultRegExp; - - const exclude: Configs['exclude'] = configs?.exclude - ? Array.isArray(configs.exclude) - ? /* c8 ignore next */ - configs.exclude - : [configs.exclude] - : undefined; - - let pending = currentFiles.length; - if (!pending) return callback?.(null, files); - - currentFiles.forEach((file) => { + configs?: Configs +): Promise => { + const currentFiles = await readdir(sanitizePath(dirPath)); + const defaultRegExp = /\.(test|spec)\./i; + /* c8 ignore start */ + const filter: RegExp = + (envFilter + ? envFilter + : configs?.filter instanceof RegExp + ? configs.filter + : defaultRegExp) || defaultRegExp; + + const exclude: Configs['exclude'] = configs?.exclude + ? Array.isArray(configs.exclude) + ? configs.exclude + : [configs.exclude] + : undefined; + /* c8 ignore stop */ + + await Promise.all( + currentFiles.map(async (file) => { const fullPath = join(dirPath, file); - - fsStat(fullPath, (err, stat) => { - /* c8 ignore start */ - if (err) { - if (!--pending) callback?.(null, files); - return; - } - /* c8 ignore stop */ - - if ( - fullPath.indexOf('node_modules') !== -1 || - fullPath.indexOf('.git') === 0 - ) { - /* c8 ignore start */ - if (!--pending) callback?.(null, files); - return; - /* c8 ignore stop */ - } - - if (exclude) { - for (let i = 0; i < exclude.length; i++) { - if (exclude[i].test(fullPath)) { - /* c8 ignore start */ - if (!--pending) callback?.(null, files); - return; - /* c8 ignore stop */ - } - } - } - - if (filter.test(fullPath)) { - files.push(fullPath); - - /* c8 ignore start */ - if (!--pending) callback?.(null, files); - return; - /* c8 ignore stop */ + const stat = await fsStat(fullPath); + + /* c8 ignore start */ + if ( + fullPath.indexOf('node_modules') !== -1 || + fullPath.indexOf('.git') === 0 + ) + return; + /* c8 ignore stop */ + + if (exclude) { + for (let i = 0; i < exclude.length; i++) { + /* c8 ignore next */ + if (exclude[i].test(fullPath)) return; } + } - if (stat.isDirectory()) { - getAllFiles(fullPath, files, configs, (err) => { - /* c8 ignore start */ - if (err) { - if (!--pending) callback?.(err, files); - return; - } - /* c8 ignore stop */ + if (filter.test(fullPath)) return files.push(fullPath); + if (stat.isDirectory()) await getAllFiles(fullPath, files, configs); + }) + ); - if (!--pending) callback?.(null, files); - }); - /* c8 ignore next */ - } else if (!--pending) callback?.(null, files); - }); - }); - }); + return files; }; -export const listFiles = ( - targetDir: string, - configs?: Configs -): Promise => - new Promise((resolve, reject) => { - getAllFiles(sanitizePath(targetDir), [], configs, (err, result) => { - if (err) return reject(err); - - resolve(result!); - }); - }); +/* c8 ignore start */ +export const listFiles = async (targetDir: string, configs?: Configs) => + await getAllFiles(sanitizePath(targetDir), [], configs); +/* c8 ignore stop */ diff --git a/src/modules/test.ts b/src/modules/test.ts index 1bd2a050..f0d6c7a2 100644 --- a/src/modules/test.ts +++ b/src/modules/test.ts @@ -1,3 +1,4 @@ +/* c8 ignore next */ import process from 'node:process'; /* c8 ignore next */ import { each } from '../configs/each.js'; @@ -6,6 +7,7 @@ import { indentation } from '../configs/indentation.js'; import { format } from '../helpers/format.js'; import { write } from '../helpers/logs.js'; +/* c8 ignore start */ export async function test( message: string, cb: () => Promise @@ -19,6 +21,7 @@ export async function test( (() => unknown | Promise)?, ] ): Promise { + /* c8 ignore stop */ let message: string | undefined; let cb: () => unknown | Promise; diff --git a/src/polyfills/fs.ts b/src/polyfills/fs.ts index df4e107a..bdb09151 100644 --- a/src/polyfills/fs.ts +++ b/src/polyfills/fs.ts @@ -8,22 +8,37 @@ import { type Stats, } from 'node:fs'; -export const readdir = ( +export function readdir(path: string): Promise; +export function readdir( path: string, options: { withFileTypes: true } -): Promise => - new Promise((resolve, reject) => { - nodeReaddir(path, options, (err, entries) => { - if (err) reject(err); - else resolve(entries); +): Promise; +export function readdir( + path: string, + options?: { withFileTypes?: boolean } +): Promise { + return new Promise((resolve, reject) => { + if (options?.withFileTypes) { + nodeReaddir(path, { withFileTypes: true }, (err, entries) => { + if (err) return reject(err); + resolve(entries); + }); + + return; + } + + nodeReaddir(path, (err, files) => { + if (err) return reject(err); + resolve(files); }); }); +} export const stat = (path: string): Promise => { return new Promise((resolve, reject) => { nodeStat(path, (err, stats) => { - if (err) reject(err); - else resolve(stats); + if (err) return reject(err); + resolve(stats); }); }); }; @@ -34,8 +49,8 @@ export const readFile = ( ): Promise => new Promise((resolve, reject) => { nodeReadFile(path, encoding, (err, data) => { - if (err) reject(err); - else resolve(data); + if (err) return reject(err); + resolve(data); }); }); diff --git a/src/polyfills/object.ts b/src/polyfills/object.ts index b7d7d368..da3e31e2 100644 --- a/src/polyfills/object.ts +++ b/src/polyfills/object.ts @@ -13,9 +13,11 @@ export const entries = (obj: { [key: string]: any }): [string, unknown][] => { }; export const fromEntries = ( - entries: [string, unknown][] + entries: [string, unknown][] | Map ): Record => { - return entries.reduce( + const mappedEntries = entries instanceof Map ? Array.from(entries) : entries; + + return mappedEntries.reduce( (acc, [key, value]) => { acc[key] = value; return acc; diff --git a/src/services/run-tests.ts b/src/services/run-tests.ts index a6ba3309..bd062e82 100644 --- a/src/services/run-tests.ts +++ b/src/services/run-tests.ts @@ -1,6 +1,6 @@ import process from 'node:process'; import { EOL } from 'node:os'; -import { join, relative } from 'node:path'; +import { join, relative, sep } from 'node:path'; import { runner } from '../helpers/runner.js'; import { indentation } from '../configs/indentation.js'; import { @@ -42,7 +42,7 @@ export const runTests = async ( if (showLogs) { hr(); write( - `${format.bold(isFile ? 'File:' : 'Directory:')} ${format.underline(`./${currentDir}`)}${EOL}` + `${format.bold(isFile ? 'File:' : 'Directory:')} ${format.underline(`.${sep}${currentDir}`)}${EOL}` ); } diff --git a/test/docker/deno/latest.Dockerfile b/test/docker/deno/latest.Dockerfile index 04c161df..abe0358c 100644 --- a/test/docker/deno/latest.Dockerfile +++ b/test/docker/deno/latest.Dockerfile @@ -9,6 +9,6 @@ COPY ./tools ./tools COPY ./fixtures ./fixtures RUN apk add lsof -RUN deno run --allow-read --allow-write --allow-env --allow-run tools/compatibility/deno.ts +RUN deno run --allow-read --allow-write --allow-env --allow-run --unstable-sloppy-imports tools/compatibility/deno.ts CMD ["deno", "run", "--allow-read", "--allow-write", "--allow-hrtime", "--allow-env", "--allow-run", "test/run.test.ts"] diff --git a/test/docker/playground/deno/Dockerfile b/test/docker/playground/deno/Dockerfile index a118e606..bdbdfc28 100644 --- a/test/docker/playground/deno/Dockerfile +++ b/test/docker/playground/deno/Dockerfile @@ -7,7 +7,7 @@ COPY ./test ./test COPY ./tools ./tools COPY ./fixtures ./fixtures -RUN deno run --allow-read --allow-write --allow-env --allow-run tools/compatibility/deno.ts +RUN deno run --allow-read --allow-write --allow-env --allow-run --unstable-sloppy-imports tools/compatibility/deno.ts # deno run --allow-read --allow-net --allow-env --allow-run test/ CMD ["tail", "-f", "/dev/null"] diff --git a/test/unit/assert.result-type.test.ts b/test/unit/assert-result-type.test.ts similarity index 77% rename from test/unit/assert.result-type.test.ts rename to test/unit/assert-result-type.test.ts index c0c1387b..eb64f199 100644 --- a/test/unit/assert.result-type.test.ts +++ b/test/unit/assert-result-type.test.ts @@ -79,7 +79,6 @@ test(async () => { ), 'Function (Params)' ); - assert.deepStrictEqual( parseResultType({ a: true }), `{ @@ -100,25 +99,25 @@ test(async () => { }`, 'Object (Complex)' ); -}); -assert.deepStrictEqual( - parseResultType([1]), - `[ + + assert.deepStrictEqual( + parseResultType([1]), + `[ 1 ]`, - 'Array' -); -assert.deepStrictEqual(parseResultType([]), `[]`, 'Array (Empty)'); -assert.deepStrictEqual( - parseResultType([ - 1, - true, - undefined, - /123/gm, - { a: { b: [{ c: undefined }] } }, - [[[[/[^0-9]/]]]], - ]), - `[ + 'Array' + ); + assert.deepStrictEqual(parseResultType([]), `[]`, 'Array (Empty)'); + assert.deepStrictEqual( + parseResultType([ + 1, + true, + undefined, + /123/gm, + { a: { b: [{ c: undefined }] } }, + [[[[/[^0-9]/]]]], + ]), + `[ 1, true, "undefined", @@ -142,5 +141,28 @@ assert.deepStrictEqual( ] ] ]`, - 'Array Complex' -); + 'Array Complex' + ); + assert.deepStrictEqual( + parseResultType(new Map([['key', 'value']])), + `{ + "key": "value" +}`, + 'Map' + ); + assert.deepStrictEqual( + parseResultType(new Set([1, 2, 3, 3])), + `[ + 1, + 2, + 3 +]`, + 'Set' + ); + assert.deepStrictEqual(parseResultType(Symbol()), 'Symbol()', 'Symbol'); + assert.deepStrictEqual( + parseResultType(Symbol.for('key')), + 'Symbol(key)', + 'Symbol.for' + ); +}); diff --git a/test/unit/map-tests.test.ts b/test/unit/map-tests.test.ts index f71408e6..240e9496 100644 --- a/test/unit/map-tests.test.ts +++ b/test/unit/map-tests.test.ts @@ -52,7 +52,7 @@ describe('mapTests', async () => { ], ]); - assert.deepStrictEqual(Array.from(importMap), Array.from(expected)); + assert.deepStrictEqual(importMap, expected); }); await it('should map single test file correctly', async () => { @@ -66,6 +66,6 @@ describe('mapTests', async () => { ], ]); - assert.deepStrictEqual(Array.from(importMap), Array.from(expected)); + assert.deepStrictEqual(importMap, expected); }); }); diff --git a/test/unit/sanitize-paths.test.ts b/test/unit/sanitize-paths.test.ts new file mode 100644 index 00000000..fa8182a9 --- /dev/null +++ b/test/unit/sanitize-paths.test.ts @@ -0,0 +1,48 @@ +import { sep } from 'node:path'; +import { assert } from '../../src/modules/assert.js'; +import { sanitizePath } from '../../src/modules/list-files.js'; +import { test } from '../../src/modules/test.js'; + +test('Sanitize paths', () => { + assert.strictEqual( + sanitizePath('path//to///file'), + `path${sep}to${sep}file`, + 'should replace multiple slashes with the OS-specific separator' + ); + + assert.strictEqual( + sanitizePath('path/../../file'), + `path${sep}file`, + 'should remove access to parent directories' + ); + + assert.strictEqual( + sanitizePath('path/to/?'), + `path${sep}to${sep}file`, + 'should remove unusual path characters' + ); + + assert.strictEqual( + sanitizePath('/absolute/path', true), + `.${sep}absolute${sep}path`, + 'should prevent absolute path access when ensureTarget is true' + ); + + assert.strictEqual( + sanitizePath('/absolute/path'), + `${sep}absolute${sep}path`, + 'should allow absolute path access when ensureTarget is false' + ); + + assert.strictEqual( + sanitizePath('./relative/path'), + `.${sep}relative${sep}path`, + 'should keep relative path unchanged' + ); + + assert.strictEqual( + sanitizePath('path\\to\\file'), + `path${sep}to${sep}file`, + 'should replace backslashes with the OS-specific separator' + ); +});