From 002ca6f18fe02b77bd5219b018499b3b916cce71 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Sat, 14 Oct 2023 14:59:42 +0200 Subject: [PATCH 1/6] ci(action): apply step-security recommended policies --- .github/workflows/codeql.yml | 67 +++++++++++++++++++++++++ .github/workflows/lint_pr_title.yml | 5 ++ .github/workflows/release.yml | 5 ++ .github/workflows/scorecards.yml | 76 +++++++++++++++++++++++++++++ .github/workflows/test.yml | 8 +++ 5 files changed, 161 insertions(+) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/scorecards.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..47f043f4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,67 @@ +name: 'CodeQL' + +on: + push: + branches: ['main'] + pull_request: + # The branches below must be a subset of the branches above + branches: ['main'] + schedule: + - cron: '0 0 * * 1' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['typescript'] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + + - name: Checkout repository + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + with: + category: '/language:${{matrix.language}}' diff --git a/.github/workflows/lint_pr_title.yml b/.github/workflows/lint_pr_title.yml index 08809495..a4777ea3 100644 --- a/.github/workflows/lint_pr_title.yml +++ b/.github/workflows/lint_pr_title.yml @@ -15,6 +15,11 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5 id: lint_pr_title env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31bdfbe7..0b97fcd8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,11 @@ jobs: node-version: 20.x steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + - name: Checkout project uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 00000000..de17fefa --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,76 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '20 7 * * 2' + push: + branches: ['main'] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + contents: read + actions: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + + - name: 'Checkout code' + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # v2.0.6 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: 'Upload artifact' + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: 'Upload to code-scanning' + uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70d39cf7..bafe479c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,9 @@ on: - beta - '+([0-9])?(.{+([0-9]),x}).x' +permissions: + contents: read + jobs: lint-and-test: runs-on: ${{ matrix.os }} @@ -17,6 +20,11 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: + - name: Harden Runner + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + - name: Checkout project uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Use Node.js ${{ matrix.node }} From 884176e917dc7dc6ec464ba8ebc11164aa152700 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Mon, 23 Oct 2023 13:51:32 +0200 Subject: [PATCH 2/6] test: add fuzz testing --- .github/workflows/test.yml | 2 + package-lock.json | 76 +++++++++++++++++++++ package.json | 2 + tests/cron.fuzz.ts | 116 +++++++++++++++++++++++++++++++++ tests/helpers/is_cron_error.ts | 27 ++++++++ 5 files changed, 223 insertions(+) create mode 100644 tests/cron.fuzz.ts create mode 100644 tests/helpers/is_cron_error.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bafe479c..bc99c76e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,5 @@ jobs: run: npm run lint - name: Run tests run: npm run test + - name: Run fuzz tests + run: npm run test:fuzz diff --git a/package-lock.json b/package-lock.json index e47b5dd7..8fabbf46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@commitlint/cli": "18.0.0", + "@fast-check/jest": "1.7.3", "@insurgentlab/commitlint-config": "18.1.3", "@insurgentlab/conventional-changelog-preset": "7.0.0", "@semantic-release/changelog": "6.0.3", @@ -1061,6 +1062,59 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-check/jest": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@fast-check/jest/-/jest-1.7.3.tgz", + "integrity": "sha512-6NcpYIIUnLwEdEfPhijYT5mnFPiQNP/isC+os+P+rV8qHRzUxRNx8WyPTOx+oVkBMm1+XSn00ZqfD3ANfciTZQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "fast-check": "^3.0.0" + }, + "peerDependencies": { + "@fast-check/worker": "~0.0.7", + "@jest/expect": ">=28.0.0", + "@jest/globals": ">=25.5.2" + }, + "peerDependenciesMeta": { + "@fast-check/worker": { + "optional": true + }, + "@jest/expect": { + "optional": true + } + } + }, + "node_modules/@fast-check/worker": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@fast-check/worker/-/worker-0.0.9.tgz", + "integrity": "sha512-Hdp+24K41OAk++Q/dnOTBljjqvnH8Jvxvq3FeqIp2kHGWwM1FL1Hpx8mn+mkaNlfEaa4oxk1fiC/xBr3pXvQ2Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "optional": true, + "peer": true, + "dependencies": { + "fast-check": "^3.4.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -4642,6 +4696,28 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-check": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.13.1.tgz", + "integrity": "sha512-Xp00tFuWd83i8rbG/4wU54qU+yINjQha7bXH2N4ARNTkyOimzHtUBJ5+htpdXk7RMaCOD/j2jxSjEt9u9ZPNeQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 919d8293..f87dfef5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lint:fix": "npm run lint:eslint -- --fix && npm run lint:prettier -- --write", "test": "jest --coverage", "test:watch": "jest --watch --coverage", + "test:fuzz": "jest --testRegex='(\\.|/)fuzz\\.ts$' --coverage=false --testTimeout=120000", "prepare": "husky install", "release": "semantic-release" }, @@ -28,6 +29,7 @@ }, "devDependencies": { "@commitlint/cli": "18.0.0", + "@fast-check/jest": "1.7.3", "@insurgentlab/commitlint-config": "18.1.3", "@insurgentlab/conventional-changelog-preset": "7.0.0", "@semantic-release/changelog": "6.0.3", diff --git a/tests/cron.fuzz.ts b/tests/cron.fuzz.ts new file mode 100644 index 00000000..c238371a --- /dev/null +++ b/tests/cron.fuzz.ts @@ -0,0 +1,116 @@ +/* eslint-disable jest/no-standalone-expect */ +import { fc, test } from '@fast-check/jest'; +import { CronJob } from '../src'; +import { isCronError } from './helpers/is_cron_error'; + +/** + * fuzzing might result in an infinite loop in our code, so Jest will simply timeout. + * experimental worker implementation of ```@fast-check/jest``` could help detect that issue + * that would be better as it would also give the counter-example that causes the bug + * but since it is still experimental, simply uncomment the log line in testCronJob + * function, so you can see the input causing the infinite loop. + */ +function testCronJob( + { + cronTime, + start, + timeZone, + runOnInit, + utcOffset, + unrefTimeout, + tzOrOffset + }: { + cronTime: string; + start: boolean; + timeZone: string; + runOnInit: boolean; + utcOffset: number; + unrefTimeout: boolean; + tzOrOffset: boolean; + }, + checkError: (err: unknown) => boolean +) { + // console.debug(cronTime, '|', timeZone, '|', utcOffset); + try { + const job = new CronJob( + cronTime, + function () {}, + null, + start, + (tzOrOffset ? timeZone : null) as typeof tzOrOffset extends true + ? string + : null, + null, + runOnInit, + (tzOrOffset ? null : utcOffset) as typeof tzOrOffset extends true + ? null + : number, + unrefTimeout + ); + + expect(job.running).toBe(start); + job.stop(); + expect(job.running).toBe(false); + + expect(job.cronTime.source).toBe(cronTime); + } catch (error) { + const isOk = checkError(error); + if (!isOk) console.error(error); + expect(isOk).toBe(true); + } +} + +test.prop( + { + cronTime: fc.stringMatching(/^(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ){5,6}$/), + start: fc.boolean(), + timeZone: fc.stringMatching( + /^((?:Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])|(Africa\/Abidjan|Asia\/Singapore|Australia\/Sydney|CET|EST|Europe\/Paris|America\/New_York))$/ + ), + runOnInit: fc.boolean(), + utcOffset: fc.integer(), + unrefTimeout: fc.boolean(), + tzOrOffset: fc.boolean() + }, + { numRuns: 250_000 } +)( + 'CronJob should behave as expected and not error unexpectedly (with matching inputs)', + params => testCronJob(params, isCronError) +); + +test.prop( + { + cronTime: fc.string(), + start: fc.boolean(), + timeZone: fc.string(), + runOnInit: fc.boolean(), + utcOffset: fc.integer(), + unrefTimeout: fc.boolean(), + tzOrOffset: fc.boolean() + }, + { numRuns: 250_000 } +)( + 'CronJob should behave as expected and not error unexpectedly (with random inputs)', + params => testCronJob(params, isCronError) +); + +test.prop( + { + cronTime: fc.anything(), + start: fc.anything(), + timeZone: fc.anything(), + runOnInit: fc.anything(), + utcOffset: fc.anything(), + unrefTimeout: fc.anything(), + tzOrOffset: fc.boolean() + }, + { numRuns: 250_000 } +)( + 'CronJob should behave as expected and not error unexpectedly (with anything inputs)', + params => + testCronJob( + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + params as any, + err => err instanceof TypeError || isCronError(err) + ) +); diff --git a/tests/helpers/is_cron_error.ts b/tests/helpers/is_cron_error.ts new file mode 100644 index 00000000..b08f61f3 --- /dev/null +++ b/tests/helpers/is_cron_error.ts @@ -0,0 +1,27 @@ +import { ExclusiveParametersError } from '../../src/errors'; + +export function isCronError(error: unknown): boolean { + return ( + error instanceof Error && + (error instanceof ExclusiveParametersError || + error.message === 'time must be an instance of CronTime.' || + error.message === 'Invalid timezone.' || + error.message === 'ERROR: You specified an invalid UTC offset.' || + error.message === 'WARNING: Date in past. Will never be fired.' || + error.message === 'ERROR: You specified an invalid date.' || + error.message.startsWith( + 'ERROR: This DST checking related function assumes the input DateTime' + ) || + error.message.startsWith( + 'ERROR: This DST checking related function assumes the forward jump starting hour' + ) || + error.message.startsWith('Unknown alias:') || + error.message === 'Too few fields' || + error.message === 'Too many fields' || + error.message.endsWith('has an invalid wildcard expression') || + error.message.endsWith('has a step of zero') || + error.message.endsWith('has an invalid range') || + error.message.endsWith('is out of range') || + error.message.endsWith('cannot be parsed')) + ); +} From b28732990e4f87b911a10d8d4de28f80821133da Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Mon, 23 Oct 2023 16:30:12 +0200 Subject: [PATCH 3/6] chore(scripts): fix fuzz test jest command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f87dfef5..96b9abad 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint:fix": "npm run lint:eslint -- --fix && npm run lint:prettier -- --write", "test": "jest --coverage", "test:watch": "jest --watch --coverage", - "test:fuzz": "jest --testRegex='(\\.|/)fuzz\\.ts$' --coverage=false --testTimeout=120000", + "test:fuzz": "jest --testMatch='**/*.fuzz.ts' --coverage=false --testTimeout=120000", "prepare": "husky install", "release": "semantic-release" }, From f9c473ee39b7a4ca88715e4aa6fea78296221936 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Mon, 23 Oct 2023 19:23:25 +0200 Subject: [PATCH 4/6] test: reduce number of fuzzing runs --- tests/cron.fuzz.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cron.fuzz.ts b/tests/cron.fuzz.ts index c238371a..ce630fd0 100644 --- a/tests/cron.fuzz.ts +++ b/tests/cron.fuzz.ts @@ -72,7 +72,7 @@ test.prop( unrefTimeout: fc.boolean(), tzOrOffset: fc.boolean() }, - { numRuns: 250_000 } + { numRuns: 100_000 } )( 'CronJob should behave as expected and not error unexpectedly (with matching inputs)', params => testCronJob(params, isCronError) @@ -88,7 +88,7 @@ test.prop( unrefTimeout: fc.boolean(), tzOrOffset: fc.boolean() }, - { numRuns: 250_000 } + { numRuns: 100_000 } )( 'CronJob should behave as expected and not error unexpectedly (with random inputs)', params => testCronJob(params, isCronError) @@ -104,7 +104,7 @@ test.prop( unrefTimeout: fc.anything(), tzOrOffset: fc.boolean() }, - { numRuns: 250_000 } + { numRuns: 100_000 } )( 'CronJob should behave as expected and not error unexpectedly (with anything inputs)', params => From 033403019fe7826cdfd365df9cabcbdca616c94b Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Mon, 23 Oct 2023 20:43:52 +0200 Subject: [PATCH 5/6] docs: add SECURITY.md --- .github/SECURITY.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..fb2e12d2 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report it in a private conversation with one or more of our maintainers [on Discord](https://discord.gg/yyKns29zch). + +Please encrypt your message to us using our PGP key. The key fingerprint is: + +``` +A656 0650 74D2 6C7D CF6E D0F4 0784 3C69 92BF C9FA +``` + +The key is available from [keyserver.ubuntu.com](https://keyserver.ubuntu.com/pks/lookup?search=0xA656065074D26C7DCF6ED0F407843C6992BFC9FA&fingerprint=on&op=index). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +Please get in touch and give the project contributors a chance to resolve the vulnerability and issue a new release prior to any public exposure; this helps protect the project's users and provides them with a chance to upgrade and/or update in order to protect their applications. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +`cron` follows the principle of [Coordinated Vulnerability Disclosure](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html#responsible-or-coordinated-disclosure). From 6b36b67ea9b16c698948acb0cd4fd679ae207271 Mon Sep 17 00:00:00 2001 From: Pierre Cavin Date: Thu, 26 Oct 2023 15:43:59 +0200 Subject: [PATCH 6/6] refactor: use custom CronError --- src/errors.ts | 4 +++- src/job.ts | 4 ++-- src/time.ts | 34 ++++++++++++++++++---------------- tests/cron.fuzz.ts | 15 ++++++++++----- tests/helpers/is_cron_error.ts | 27 --------------------------- 5 files changed, 33 insertions(+), 51 deletions(-) delete mode 100644 tests/helpers/is_cron_error.ts diff --git a/src/errors.ts b/src/errors.ts index 9f07d5bb..798e2d43 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,6 @@ -export class ExclusiveParametersError extends Error { +export class CronError extends Error {} + +export class ExclusiveParametersError extends CronError { constructor(param1: string, param2: string) { super(`You can't specify both ${param1} and ${param2}`); } diff --git a/src/job.ts b/src/job.ts index 68d7c349..1201d653 100644 --- a/src/job.ts +++ b/src/job.ts @@ -1,5 +1,5 @@ import { spawn } from 'child_process'; -import { ExclusiveParametersError } from './errors'; +import { CronError, ExclusiveParametersError } from './errors'; import { CronTime } from './time'; import { CronCallback, @@ -172,7 +172,7 @@ export class CronJob { setTime(time: CronTime) { if (!(time instanceof CronTime)) { - throw new Error('time must be an instance of CronTime.'); + throw new CronError('time must be an instance of CronTime.'); } const wasRunning = this.running; this.stop(); diff --git a/src/time.ts b/src/time.ts index 2a35b38a..9814c630 100644 --- a/src/time.ts +++ b/src/time.ts @@ -12,7 +12,7 @@ import { TIME_UNITS_LEN, TIME_UNITS_MAP } from './constants'; -import { ExclusiveParametersError } from './errors'; +import { CronError, ExclusiveParametersError } from './errors'; import { CronJobParams, DayOfMonthRange, @@ -59,7 +59,7 @@ export class CronTime { if (timeZone) { const dt = DateTime.fromObject({}, { zone: timeZone }); if (!dt.isValid) { - throw new Error('Invalid timezone.'); + throw new CronError('Invalid timezone.'); } this.timeZone = timeZone; @@ -157,13 +157,13 @@ export class CronTime { date = date.setZone(utcZone); if (!date.isValid) { - throw new Error('ERROR: You specified an invalid UTC offset.'); + throw new CronError('ERROR: You specified an invalid UTC offset.'); } } if (this.realDate) { if (DateTime.local() > date) { - throw new Error('WARNING: Date in past. Will never be fired.'); + throw new CronError('WARNING: Date in past. Will never be fired.'); } return date; @@ -240,7 +240,7 @@ export class CronTime { } if (!date.isValid) { - throw new Error('ERROR: You specified an invalid date.'); + throw new CronError('ERROR: You specified an invalid date.'); } /** @@ -258,7 +258,7 @@ export class CronTime { // hard stop if the current date is after the maximum match interval if (date > maxMatch) { - throw new Error( + throw new CronError( `Something went wrong. No execution date was found in the next 8 years. Please provide the following string if you would like to help debug: Time Zone: ${ @@ -433,7 +433,7 @@ export class CronTime { let iteration = 0; do { if (++iteration > iterationLimit) { - throw new Error( + throw new CronError( `ERROR: This DST checking related function assumes the input DateTime (${ date.toISO() ?? date.toMillis() }) is within 24 hours of a DST jump.` @@ -583,7 +583,7 @@ export class CronTime { endMinute: number ) { if (startHour >= endHour) { - throw new Error( + throw new CronError( `ERROR: This DST checking related function assumes the forward jump starting hour (${startHour}) is less than the end hour (${endHour})` ); } @@ -707,18 +707,18 @@ export class CronTime { return ALIASES[alias as keyof typeof ALIASES].toString(); } - throw new Error(`Unknown alias: ${alias}`); + throw new CronError(`Unknown alias: ${alias}`); }); const units = source.trim().split(/\s+/); // seconds are optional if (units.length < TIME_UNITS_LEN - 1) { - throw new Error('Too few fields'); + throw new CronError('Too few fields'); } if (units.length > TIME_UNITS_LEN) { - throw new Error('Too many fields'); + throw new CronError('Too many fields'); } const unitsLen = units.length; @@ -756,7 +756,9 @@ export class CronTime { fields.forEach(field => { const wildcardIndex = field.indexOf('*'); if (wildcardIndex !== -1 && wildcardIndex !== 0) { - throw new Error(`Field (${field}) has an invalid wildcard expression`); + throw new CronError( + `Field (${field}) has an invalid wildcard expression` + ); } }); @@ -776,11 +778,11 @@ export class CronTime { const wasStepDefined = mStep !== undefined; const step = parseInt(mStep ?? '1', 10); if (step === 0) { - throw new Error(`Field (${unit}) has a step of zero`); + throw new CronError(`Field (${unit}) has a step of zero`); } if (upper !== undefined && lower > upper) { - throw new Error(`Field (${unit}) has an invalid range`); + throw new CronError(`Field (${unit}) has an invalid range`); } const isOutOfRange = @@ -789,7 +791,7 @@ export class CronTime { (upper === undefined && lower > high); if (isOutOfRange) { - throw new Error(`Field value (${value}) is out of range`); + throw new CronError(`Field value (${value}) is out of range`); } // Positive integer higher than constraints[0] @@ -820,7 +822,7 @@ export class CronTime { delete typeObj[7]; } } else { - throw new Error(`Field (${unit}) cannot be parsed`); + throw new CronError(`Field (${unit}) cannot be parsed`); } } } diff --git a/tests/cron.fuzz.ts b/tests/cron.fuzz.ts index ce630fd0..e23f3f7a 100644 --- a/tests/cron.fuzz.ts +++ b/tests/cron.fuzz.ts @@ -1,7 +1,7 @@ /* eslint-disable jest/no-standalone-expect */ import { fc, test } from '@fast-check/jest'; import { CronJob } from '../src'; -import { isCronError } from './helpers/is_cron_error'; +import { CronError } from '../src/errors'; /** * fuzzing might result in an infinite loop in our code, so Jest will simply timeout. @@ -55,7 +55,12 @@ function testCronJob( expect(job.cronTime.source).toBe(cronTime); } catch (error) { const isOk = checkError(error); - if (!isOk) console.error(error); + if (!isOk) { + console.error(error); + console.error( + 'Make sure the relevant code is using an instance of CronError (or derived) when throwing.' + ); + } expect(isOk).toBe(true); } } @@ -75,7 +80,7 @@ test.prop( { numRuns: 100_000 } )( 'CronJob should behave as expected and not error unexpectedly (with matching inputs)', - params => testCronJob(params, isCronError) + params => testCronJob(params, err => err instanceof CronError) ); test.prop( @@ -91,7 +96,7 @@ test.prop( { numRuns: 100_000 } )( 'CronJob should behave as expected and not error unexpectedly (with random inputs)', - params => testCronJob(params, isCronError) + params => testCronJob(params, err => err instanceof CronError) ); test.prop( @@ -111,6 +116,6 @@ test.prop( testCronJob( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument params as any, - err => err instanceof TypeError || isCronError(err) + err => err instanceof CronError || err instanceof TypeError ) ); diff --git a/tests/helpers/is_cron_error.ts b/tests/helpers/is_cron_error.ts deleted file mode 100644 index b08f61f3..00000000 --- a/tests/helpers/is_cron_error.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ExclusiveParametersError } from '../../src/errors'; - -export function isCronError(error: unknown): boolean { - return ( - error instanceof Error && - (error instanceof ExclusiveParametersError || - error.message === 'time must be an instance of CronTime.' || - error.message === 'Invalid timezone.' || - error.message === 'ERROR: You specified an invalid UTC offset.' || - error.message === 'WARNING: Date in past. Will never be fired.' || - error.message === 'ERROR: You specified an invalid date.' || - error.message.startsWith( - 'ERROR: This DST checking related function assumes the input DateTime' - ) || - error.message.startsWith( - 'ERROR: This DST checking related function assumes the forward jump starting hour' - ) || - error.message.startsWith('Unknown alias:') || - error.message === 'Too few fields' || - error.message === 'Too many fields' || - error.message.endsWith('has an invalid wildcard expression') || - error.message.endsWith('has a step of zero') || - error.message.endsWith('has an invalid range') || - error.message.endsWith('is out of range') || - error.message.endsWith('cannot be parsed')) - ); -}