diff --git a/CHANGELOG.md b/CHANGELOG.md index 07feaa490..bc70e68ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ Versioning]. ## [Unreleased] -- _No changes yet_ +- Improve alignment between `Shescape` and `Stubscape`. ([#1149]) +- Add a failing `Shescape` stub to the testing module. ([#1149]) ## [2.0.0] - 2023-09-07 @@ -312,6 +313,7 @@ Versioning]. [#1094]: https://github.com/ericcornelissen/shescape/pull/1094 [#1137]: https://github.com/ericcornelissen/shescape/pull/1137 [#1142]: https://github.com/ericcornelissen/shescape/pull/1142 +[#1149]: https://github.com/ericcornelissen/shescape/pull/1149 [552e8ea]: https://github.com/ericcornelissen/shescape/commit/552e8eab56861720b1d4e5474fb65741643358f9 [keep a changelog]: https://keepachangelog.com/en/1.0.0/ [semantic versioning]: https://semver.org/spec/v2.0.0.html diff --git a/docs/testing.md b/docs/testing.md index 7d15408c7..6bc82e055 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -19,11 +19,16 @@ mocking ([for example with Jest][jest-module-mock]). // my-module.test.js import assert from "node:assert"; -import { Shescape as Stubscape } from "shescape/testing"; +import { Shescape as Stubscape, Throwscape } from "shescape/testing"; import { functionUnderTest } from "./my-module.js"; +// Test good conditions const stubscape = new Stubscape(); assert.ok(functionUnderTest(stubscape)); + +// Test bad conditions +const throwscape = new Throwscape(); +assert.ok(functionUnderTest(throwscape)); ``` ### Why Stubs diff --git a/test/integration/testing/commonjs.test.js b/test/integration/testing/commonjs.test.js index 5e09d5ba3..49d425f78 100644 --- a/test/integration/testing/commonjs.test.js +++ b/test/integration/testing/commonjs.test.js @@ -5,15 +5,34 @@ */ import { testProp } from "@fast-check/ava"; +import test from "ava"; import * as fc from "fast-check"; import { arbitrary } from "../_.js"; -import { Shescape as Stubscape } from "shescape/testing"; -import { Shescape as StubscapeCjs } from "../../../testing.cjs"; +import { + injectionStrings, + Shescape as Stubscape, + Throwscape, +} from "shescape/testing"; +import { + injectionStrings as injectionStringsCjs, + Shescape as StubscapeCjs, + Throwscape as ThrowscapeCjs, +} from "../../../testing.cjs"; + +test("injection strings", (t) => { + for (const injectionStringCjs of injectionStringsCjs) { + t.true(injectionStrings.includes(injectionStringCjs)); + } + + for (const injectionString of injectionStrings) { + t.true(injectionStringsCjs.includes(injectionString)); + } +}); testProp( - "escape (esm === cjs)", + "Stubscape#escape (esm === cjs)", [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], (t, arg, options) => { const stubscape = new Stubscape(options); @@ -26,7 +45,7 @@ testProp( ); testProp( - "escapeAll (esm === cjs)", + "Stubscape#escapeAll (esm === cjs)", [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], (t, args, options) => { const stubscape = new Stubscape(options); @@ -39,27 +58,75 @@ testProp( ); testProp( - "quote (esm === cjs)", + "Stubscape#quote (esm === cjs)", [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], (t, arg, options) => { + let resultEsm, resultCjs, erroredEsm, erroredCjs; + const stubscape = new Stubscape(options); const stubscapeCjs = new StubscapeCjs(options); - const resultEsm = stubscape.quote(arg); - const resultCjs = stubscapeCjs.quote(arg); + try { + resultEsm = stubscape.quote(arg); + } catch (_) { + erroredEsm = true; + } + + try { + resultCjs = stubscapeCjs.quote(arg); + } catch (_) { + erroredCjs = true; + } + + t.is(erroredEsm, erroredCjs); t.is(resultEsm, resultCjs); }, ); testProp( - "quoteAll (esm === cjs)", + "Stubscape#quoteAll (esm === cjs)", [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], (t, args, options) => { + let resultEsm, resultCjs, erroredEsm, erroredCjs; + const stubscape = new Stubscape(options); const stubscapeCjs = new StubscapeCjs(options); - const resultEsm = stubscape.quoteAll(args); - const resultCjs = stubscapeCjs.quoteAll(args); + try { + resultEsm = stubscape.quoteAll(args); + } catch (_) { + erroredEsm = true; + } + + try { + resultCjs = stubscapeCjs.quoteAll(args); + } catch (_) { + erroredCjs = true; + } + + t.is(erroredEsm, erroredCjs); t.deepEqual(resultEsm, resultCjs); }, ); + +testProp( + "Throwscape#constructor (esm === cjs)", + [arbitrary.shescapeOptions()], + (t, options) => { + let erroredEsm, erroredCjs; + + try { + new Throwscape(options); + } catch (_) { + erroredEsm = true; + } + + try { + new ThrowscapeCjs(options); + } catch (_) { + erroredCjs = true; + } + + t.deepEqual(erroredEsm, erroredCjs); + }, +); diff --git a/test/integration/testing/functional.test.js b/test/integration/testing/functional.test.js index b3cd09fce..ea6283ded 100644 --- a/test/integration/testing/functional.test.js +++ b/test/integration/testing/functional.test.js @@ -11,7 +11,11 @@ import * as fc from "fast-check"; import { arbitrary } from "../_.js"; import { Shescape } from "shescape"; -import { injectionStrings, Shescape as Stubscape } from "shescape/testing"; +import { + injectionStrings, + Shescape as Stubscape, + Throwscape, +} from "shescape/testing"; test("injection strings", (t) => { t.true(Array.isArray(injectionStrings)); @@ -24,7 +28,7 @@ test("injection strings", (t) => { }); testProp( - "escape (stubscape ~ shescape)", + "Stubscape#escape (stubscape =~ shescape)", [fc.anything(), arbitrary.shescapeOptions()], (t, arg, options) => { let result, stubResult, errored, stubErrored; @@ -56,7 +60,7 @@ testProp( ); testProp( - "escapeAll (stubscape ~ shescape)", + "Stubscape#escapeAll (stubscape =~ shescape)", [fc.anything(), arbitrary.shescapeOptions()], (t, args, options) => { let result, stubResult, errored, stubErrored; @@ -88,11 +92,8 @@ testProp( ); testProp( - "quote with shell (stubscape ~ shescape)", - [ - fc.anything(), - arbitrary.shescapeOptions().filter((options) => options?.shell !== false), - ], + "Stubscape#quote, with shell (stubscape =~ shescape)", + [fc.anything(), arbitrary.shescapeOptions()], (t, arg, options) => { let result, stubResult, errored, stubErrored; @@ -123,11 +124,8 @@ testProp( ); testProp( - "quoteAll with shell (stubscape ~ shescape)", - [ - fc.anything(), - arbitrary.shescapeOptions().filter((options) => options?.shell !== false), - ], + "Stubscape#quoteAll, with shell (stubscape =~ shescape)", + [fc.anything(), arbitrary.shescapeOptions()], (t, args, options) => { let result, stubResult, errored, stubErrored; @@ -156,3 +154,14 @@ testProp( t.is(typeof result, typeof stubResult); }, ); + +testProp( + "Throwscape#constructor", + [arbitrary.shescapeOptions()], + (t, options) => { + t.throws(() => new Throwscape(options), { + instanceOf: Error, + message: "Can't be instantiated", + }); + }, +); diff --git a/test/unit/testing/stubscape.test.js b/test/unit/testing/stubscape.test.js index 5c85ac164..348a90a63 100644 --- a/test/unit/testing/stubscape.test.js +++ b/test/unit/testing/stubscape.test.js @@ -68,7 +68,10 @@ test("escapeAll invalid arguments", (t) => { testProp( "quote valid arguments", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + [ + arbitrary.shescapeArg(), + arbitrary.shescapeOptions().filter((options) => options?.shell !== false), + ], (t, arg, options) => { const stubscape = new Stubscape(options); const result = stubscape.quote(arg); @@ -87,9 +90,26 @@ test("quote invalid arguments", (t) => { } }); +testProp( + "quote without a shell", + [ + arbitrary.shescapeArg(), + arbitrary.shescapeOptions().filter((options) => options?.shell === false), + ], + (t, arg, options) => { + const stubscape = new Stubscape(options); + t.throws(() => stubscape.quote(arg), { + instanceOf: Error, + }); + }, +); + testProp( "quoteAll valid arguments", - [fc.array(arbitrary.shescapeArg()), arbitrary.shescapeOptions()], + [ + fc.array(arbitrary.shescapeArg()), + arbitrary.shescapeOptions().filter((options) => options?.shell !== false), + ], (t, args, options) => { const stubscape = new Stubscape(options); const result = stubscape.quoteAll(args); @@ -100,12 +120,14 @@ testProp( testProp( "quoteAll non-array arguments", - [arbitrary.shescapeArg(), arbitrary.shescapeOptions()], + [ + arbitrary.shescapeArg(), + arbitrary.shescapeOptions().filter((options) => options?.shell !== false), + ], (t, arg, options) => { const stubscape = new Stubscape(options); t.throws(() => stubscape.quoteAll(arg), { instanceOf: TypeError, - message: "args.map is not a function", }); }, ); @@ -120,3 +142,17 @@ test("quoteAll invalid arguments", (t) => { }); } }); + +testProp( + "quoteAll without a shell", + [ + fc.array(arbitrary.shescapeArg(), { minLength: 1 }), + arbitrary.shescapeOptions().filter((options) => options?.shell === false), + ], + (t, args, options) => { + const stubscape = new Stubscape(options); + t.throws(() => stubscape.quoteAll(args), { + instanceOf: Error, + }); + }, +); diff --git a/test/unit/testing/throwscape.test.js b/test/unit/testing/throwscape.test.js new file mode 100644 index 000000000..1f48bee12 --- /dev/null +++ b/test/unit/testing/throwscape.test.js @@ -0,0 +1,17 @@ +/** + * @overview Contains unit tests for the throwing test stub of shescape. + * @license MIT + */ + +import { testProp } from "@fast-check/ava"; + +import { arbitrary } from "./_.js"; + +import { Throwscape } from "../../../testing.js"; + +testProp("throws", [arbitrary.shescapeOptions()], (t, options) => { + t.throws(() => new Throwscape(options), { + instanceOf: Error, + message: "Can't be instantiated", + }); +}); diff --git a/testing.d.ts b/testing.d.ts index 57e7c384c..4396e9166 100644 --- a/testing.d.ts +++ b/testing.d.ts @@ -1,4 +1,4 @@ -import shescape from "shescape"; +import type { Shescape as ShescapeType } from "shescape"; /** * A list of example shell injection strings to test whether or not a function @@ -21,9 +21,10 @@ export const injectionStrings: string[]; * - Errors on non-stringable inputs. * - Converts non-array inputs to single-item arrays where necessary. */ -export const shescape: { - escape: shescape.escape; - escapeAll: shescape.escapeAll; - quote: shescape.quote; - quoteAll: shescape.quoteAll; -}; +export const Shescape: ShescapeType; + +/** + * A test stub of Shescape that can't be instantiated. This can be used to + * simulate a failure to instantiate Shescape in your code. + */ +export const Throwscape: ShescapeType; diff --git a/testing.js b/testing.js index 612a08e76..d0e8ea424 100644 --- a/testing.js +++ b/testing.js @@ -26,17 +26,19 @@ export const injectionStrings = [ ]; /** - * A test stub of Shescape that has the same input-output profile as the real - * shescape implementation. + * An optimistic test stub of Shescape that has the same input-output profile as + * the real Shescape implementation. * * In particular: + * - The constructor never fails. * - Returns a string for all stringable inputs. * - Errors on non-stringable inputs. - * - Converts non-array inputs to single-item arrays where necessary. + * - Errors on non-array inputs where arrays are expected. + * - Errors when trying to quote when `shell: false`. */ export class Shescape { - constructor(_options) { - // Nothing to do. + constructor(options = {}) { + this.shell = options.shell; } escape(arg) { @@ -48,10 +50,24 @@ export class Shescape { } quote(arg) { + if (this.shell === false) { + throw new Error(); + } + return this.escape(arg); } quoteAll(args) { - return this.escapeAll(args); + return args.map((arg) => this.quote(arg)); + } +} + +/** + * A test stub of Shescape that can't be instantiated. This can be used to + * simulate a failure to instantiate Shescape in your code. + */ +export class Throwscape { + constructor(_options) { + throw new Error("Can't be instantiated"); } }