diff --git a/.circleci/config.yml b/.circleci/config.yml index 43c1695..7d63c07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,11 +2,21 @@ version: 2.1 jobs: build: docker: - # Node 24.18 (engines/.nvmrc) — matching browsers image. - - image: cimg/node:24.18-browsers + # Node 24.19 (engines/.nvmrc) — matching browsers image. + - image: cimg/node:24.19.0-browsers working_directory: ~/repo steps: - checkout + - run: + name: PR base branch verification (main requires dev head) + command: | + TARGET="${CIRCLE_TARGET_BRANCH:-${GITHUB_BASE_REF:-}}" + SOURCE="${CIRCLE_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}" + if [ -n "$TARGET" ] && [ "$TARGET" = "main" ] && [ "$SOURCE" != "dev" ]; then + echo "FAIL: PRs targeting main must originate from dev branch (got: $SOURCE)." >&2 + exit 1 + fi + echo "OK: PR base/head branch verification passed (Target: ${TARGET:-default}, Source: $SOURCE)" - run: name: Test install command: 'npm install --ignore-scripts' diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 0000000..53a07c5 --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,15 @@ +{ + "threshold": 5, + "reporters": ["console"], + "absolute": true, + "gitignore": true, + "format": ["typescript", "tsx", "javascript", "jsx"], + "ignore": [ + "**/*.spec.tsx", + "**/*.spec.ts", + "**/*.test.*", + "**/node_modules/**", + "**/build/**", + "**/coverage/**" + ] +} diff --git a/.nvmrc b/.nvmrc index ca5c350..60ade1a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.18.0 +24.19.0 diff --git a/AGENTS.md b/AGENTS.md index a5e43d9..d8e76d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,25 @@ # Repository Guidelines for WebJamSocketCluster + +## Cross-AI hard rules + +The cross-AI hard rules that bind every agent on every surface are NOT duplicated here. They live +in exactly one file: `docs/cross-ai-rules.md` in the **`web-jam-tools` repository**, which normally +sits alongside this repository — `../web-jam-tools/docs/cross-ai-rules.md`, and on Josh's laptop +`/home/joshua/WebJamApps/web-jam-tools/docs/cross-ai-rules.md`. + +Read that file before acting. If you cannot find it, STOP and say so — do not proceed without the +rules and do not reconstruct them from memory or from this file. + ## TypeScript & Type Safety - **No `any`**: `@typescript-eslint/no-explicit-any` is set to `'error'`. Do not disable this rule or use `: any` or `as any`. - **Shared Domain Types**: Centralized types for socket connections, streams, payloads, and domain objects live in `src/types/index.ts`. - **Mongoose Generic Facade**: Model facades extend `Facade` defined in `src/lib/facade.ts`. When typing generic model methods, use double type assertions (e.g. `(await ... as unknown) as T[]`) to satisfy Mongoose generic method signatures without using `any`. - **SocketCluster Mocks**: When mocking `AGServer` or `IClient` in tests, cast stub objects using `as unknown as socketClusterServer.AGServer` or `as unknown as IClient`. Ensure `receiver.next()` mocks return `{ value?: T; done?: boolean }`. + +## Memory & Security Audits +- **Snyk Failures & Resolution via `npm audit fix`**: PR checks may report failure on `security/snyk` due to transitive dependency vulnerabilities. Running `npm audit fix` updates `package-lock.json` with non-breaking patches to resolve these vulnerabilities. Always run local tests afterwards to verify the test suite remains 100% green before committing and pushing `package-lock.json` to the PR branch. + +## System & Environment +- **Runtime:** Node.js v24.19.0 +- **Node Engine Version Bumps:** When bumping Node.js in `package.json` `engines.node`, run `npm install --package-lock-only --ignore-scripts` (or `npm install --ignore-scripts`) to update `package-lock.json` root engine definition without waiting on `postinstall` scripts so both files are committed together. diff --git a/eslint.config.mjs b/eslint.config.mjs index 8cad0c1..8ddbb93 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,6 +3,8 @@ import tseslint from 'typescript-eslint'; import nodePlugin from 'eslint-plugin-n'; import securityPlugin from 'eslint-plugin-security'; import sonarjs from 'eslint-plugin-sonarjs'; +import unicorn from 'eslint-plugin-unicorn'; +import promise from 'eslint-plugin-promise'; import globals from 'globals'; export default tseslint.config( @@ -24,6 +26,25 @@ export default tseslint.config( nodePlugin.configs['flat/recommended-module'], securityPlugin.configs.recommended, sonarjs.configs.recommended, + promise.configs['flat/recommended'], + { + plugins: { + unicorn, + }, + rules: { + 'unicorn/prefer-node-protocol': 'warn', + 'unicorn/no-unreadable-array-destructuring': 'warn', + 'unicorn/no-useless-promise-resolve-reject': 'off', + 'unicorn/prefer-array-find': 'warn', + 'unicorn/prefer-array-flat-map': 'warn', + 'unicorn/prefer-includes': 'warn', + 'unicorn/prefer-string-starts-ends-with': 'warn', + 'unicorn/prefer-date-now': 'warn', + 'unicorn/no-typeof-undefined': 'warn', + 'unicorn/no-invalid-remove-event-listener': 'warn', + 'promise/no-return-wrap': 'off', + }, + }, { files: ['**/*.ts'], extends: [...tseslint.configs.recommendedTypeChecked], @@ -73,6 +94,8 @@ export default tseslint.config( files: ['test/**/*.ts'], rules: { 'sonarjs/no-nested-functions': 'off', + 'promise/always-return': 'off', + 'promise/catch-or-return': 'off', }, }, ); diff --git a/package-lock.json b/package-lock.json index 140794b..110d351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webjamsocketserver", - "version": "3.0.15", + "version": "3.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webjamsocketserver", - "version": "3.0.15", + "version": "3.0.23", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -46,14 +46,17 @@ "cross-env": "^10.1.0", "eslint": "^10.2.1", "eslint-plugin-n": "^18.1.0", + "eslint-plugin-promise": "^7.3.0", "eslint-plugin-security": "^4.0.0", "eslint-plugin-sonarjs": "^4.0.3", + "eslint-plugin-unicorn": "^73.0.0", "globals": "^17.5.0", + "jscpd": "^5.0.16", "typescript-eslint": "^8.59.1", "vitest": "^4.1.9" }, "engines": { - "node": "24.18.0" + "node": "24.19.0" } }, "node_modules/@babel/helper-string-parser": { @@ -240,6 +243,20 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@eslint/css-tree": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-4.0.5.tgz", + "integrity": "sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.29.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/js": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", @@ -1729,6 +1746,19 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", + "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1802,9 +1832,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -1813,6 +1843,40 @@ "node": "20 || >=22" } }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bson": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", @@ -1903,6 +1967,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1926,6 +2011,29 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -2038,6 +2146,19 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2063,6 +2184,23 @@ "node": ">=6.6.0" } }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2169,6 +2307,19 @@ "node": ">= 0.8" } }, + "node_modules/detect-indent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-7.0.2.tgz", + "integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2235,6 +2386,13 @@ "async-stream-emitter": "^7.0.1" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.415", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", + "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -2265,6 +2423,19 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2482,6 +2653,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-promise": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", + "integrity": "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/eslint-plugin-security": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.1.tgz", @@ -2523,6 +2713,44 @@ "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/eslint-plugin-unicorn": { + "version": "73.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-73.0.0.tgz", + "integrity": "sha512-V0YatLe9nkGhXEXKe2Qljb1EY0sJHwDV0HUF1NKFwtsHh/fU7qGHDgv+6fchzZcgU2/7noHo2gdjnmo0P2uDPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@eslint/css-tree": "^4.0.4", + "browserslist": "^4.28.4", + "change-case": "^5.4.4", + "ci-info": "^4.4.0", + "core-js-compat": "^3.49.0", + "detect-indent": "^7.0.2", + "entities": "^4.5.0", + "find-up-simple": "^1.0.1", + "globals": "^17.7.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "is-identifier": "^1.1.0", + "pluralize": "^8.0.0", + "quote-js-string": "^0.1.0", + "regjsparser": "^0.13.2", + "reserved-identifiers": "^1.2.0", + "semver": "^7.8.5", + "strip-indent": "^4.1.1", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=10.4" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2850,6 +3078,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -2913,6 +3154,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -3184,6 +3438,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/identifier-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/identifier-regex/-/identifier-regex-1.1.0.tgz", + "integrity": "sha512-SLX4H/vtcYlYnL7XqnuJKHU7Z8517TgsW9nmQiGOgMCjQ8V/deLYu6bEmbGoXe7WMMhc9+EUGyFFneHja8KabA==", + "dev": true, + "license": "MIT", + "dependencies": { + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3230,6 +3500,19 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3245,6 +3528,35 @@ "node": ">= 0.10" } }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-builtin-module/node_modules/builtin-modules": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.3.0.tgz", + "integrity": "sha512-hMQUl2bUFG339QygPM97E+mc8OY1IAchORZxm4a/frcYwKzozMzRVDBwHW0NjOqGElLm2O37AVQE8ikxlZHrMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3268,6 +3580,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-identifier/-/is-identifier-1.1.0.tgz", + "integrity": "sha512-NhOds0mDx9lJu+1lBRO0xbwFo5nobA7GCk/0e5xjr6+6XugX985+0OyGX35BNrTkPAsdLcIKg02HUQJOK8D8kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "identifier-regex": "^1.1.0", + "super-regex": "^1.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -3367,6 +3696,133 @@ "dev": true, "license": "MIT" }, + "node_modules/jscpd": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.0.16.tgz", + "integrity": "sha512-TiQ4zKtKeldep6UswXFHjVCDhVdLBaJyQcZjhCSzVOmKpT6HBj0jUZiphP1vK1X3VSSuzwcfifJVNpsOIiwRCg==", + "dev": true, + "license": "MIT", + "bin": { + "jscpd": "run-jscpd.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "jscpd-darwin-arm64": "5.0.16", + "jscpd-darwin-x64": "5.0.16", + "jscpd-linux-arm64-gnu": "5.0.16", + "jscpd-linux-x64-gnu": "5.0.16", + "jscpd-linux-x64-musl": "5.0.16", + "jscpd-windows-x64-msvc": "5.0.16" + } + }, + "node_modules/jscpd-darwin-arm64": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.0.16.tgz", + "integrity": "sha512-Tu6OAg3Rp6m7LCZqOtPViJxYGTKXTLrI/xMyjEKe79N9L7GpbI3XhgotGDpoE+PjLRbgJgGhRKbkKK18FPySmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-darwin-x64": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.0.16.tgz", + "integrity": "sha512-3btQ1aG8K7+9rvJjyaRJRcSBD0DvHgZOUVlecjnyO99FYzMvdFREq67UIl5CAXFeYf5EY8TSorCMZQxL6TBD0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/jscpd-linux-arm64-gnu": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.0.16.tgz", + "integrity": "sha512-L1F9CNHxRPqGJCKgGRA4gtZcESACFkS2lgfPgWn9fG+4CkiewLcYyy/oxrafJJL1/PPTQ24sbLuzPaKUxJuvuw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-gnu": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.0.16.tgz", + "integrity": "sha512-BruP+sAr0+6QsEQZvOkgcS+xR23y4s1hLA0xfkcO1zFFXdnmQdJ4w23/U8VlPwqO2dFRaAEseYEXjHs5oQQGFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-linux-x64-musl": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.0.16.tgz", + "integrity": "sha512-T36RtMnF695Y1/T3cLEhgvspHdKoiF+oxK/lklGJWriLieImZolSOjGXsecW2bp7MNMMGlqLLRzr2RNgnwy+Hg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/jscpd-windows-x64-msvc": { + "version": "5.0.16", + "resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.0.16.tgz", + "integrity": "sha512-BcrY18r6uje+TjgBKdFZJz6qfAAh9QT24KZYZk+jk6tuIKOYIm6oRMeQEu3Tzr/len9ZethN2h1bKmv+EfHcGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3882,6 +4338,37 @@ "source-map-js": "^1.2.1" } }, + "node_modules/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-asynchronous/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -3919,6 +4406,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.29.0.tgz", + "integrity": "sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -4170,6 +4664,16 @@ "node": ">= 0.6" } }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4253,6 +4757,22 @@ "node": ">= 0.8.0" } }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4285,6 +4805,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4373,6 +4906,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/postcss": { "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", @@ -4441,9 +4984,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -4456,6 +4999,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quote-js-string": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/quote-js-string/-/quote-js-string-0.1.0.tgz", + "integrity": "sha512-Y3NoRtprEEZQD8RfxMCfS0ZTqc4e+i18OrXEXAvpM6TfC/3y+0L5rNbZiSnbBBEkDfFzbpd8o+cE8q3/anjMGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/quote-js-string?sponsor=1" + } + }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -4542,6 +5098,32 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -5122,6 +5704,37 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -5158,6 +5771,22 @@ "xtend": "~4.0.1" } }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5361,6 +5990,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5586,6 +6246,13 @@ } } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index c4e064b..0176797 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "webjamsocketserver", "description": "Uses latest version of socketcluster-server", - "version": "3.0.15", + "version": "3.0.23", "license": "MIT", "type": "module", "main": "build/src/index.js", "engines": { - "node": "24.18.0" + "node": "24.19.0" }, "contributors": [ { @@ -34,7 +34,7 @@ "scripts": { "rmrf": "rimraf build && rimraf JaMmusic && rimraf coverage", "dev": "rimraf build && tsc && concurrently --raw \"npm:ts-start\" \"npm:ts-watch\"", - "test": "eslint . && npm run typecheck && rimraf coverage && npm run test:unit", + "test": "eslint . && npm run typecheck && npm run jscpd && rimraf coverage && npm run test:unit", "build:front": "./postinstallJaM.sh && rimraf ./JaMmusic/src && rimraf ./JaMmusic/test", "build:prod": "(cd JaMmusic || exit; npm start -- build)", "cleaninstall": "rimraf yarn.lock && rimraf package-lock.json && rimraf node_modules && npm cache clean --force && npm install", @@ -43,10 +43,12 @@ "ts-start": "cross-env DEBUG=WebJamSocketServer:* node --watch --trace-warnings build/src/index.js", "ts-watch": "tsc -w", "typecheck": "tsc --noEmit", + "lint": "eslint .", "test:lint": "eslint . --fix", "test:local": "eslint . --fix && npm run test:unit && npm run cc", "test:unit": "vitest run", - "test:prod": "rm -rf node_modules build && npm install --omit=dev && node scripts/smoke-start.mjs" + "test:prod": "rm -rf node_modules build && npm install --omit=dev && node scripts/smoke-start.mjs", + "jscpd": "jscpd src" }, "dependencies": { "@types/debug": "^4.1.7", @@ -89,7 +91,10 @@ "eslint-plugin-sonarjs": "^4.0.3", "globals": "^17.5.0", "typescript-eslint": "^8.59.1", - "vitest": "^4.1.9" + "vitest": "^4.1.9", + "eslint-plugin-promise": "^7.3.0", + "eslint-plugin-unicorn": "^73.0.0", + "jscpd": "^5.0.16" }, "overrides": { "socketcluster-client": { diff --git a/scripts/smoke-prod-socket.mjs b/scripts/smoke-prod-socket.mjs index ad7d661..c5ff90e 100644 --- a/scripts/smoke-prod-socket.mjs +++ b/scripts/smoke-prod-socket.mjs @@ -26,7 +26,7 @@ // bind address. Dialling by the hostname 'localhost' leaves address // selection to Node's resolver, and that has bitten this exact test once // already: confirmed empirically against the actual CI image -// (cimg/node:24.18-browsers) that Node 24's *default* `dns.lookup` +// (cimg/node:24.19.0-browsers) that Node 24's *default* `dns.lookup` // ('localhost') resolves `::1` first. If a given environment's IPv6 // loopback is present but not actually routable (a known class of container // networking quirk we could not force-reproduce locally, but which matches diff --git a/src/AgController/utils.ts b/src/AgController/utils.ts index ffdbc71..31f7109 100644 --- a/src/AgController/utils.ts +++ b/src/AgController/utils.ts @@ -74,7 +74,7 @@ function assertCanCreateGig( } return; } - if (!goodRoles || !user.userType || goodRoles.indexOf(user.userType) === -1) { + if (!goodRoles || !user.userType || !goodRoles.includes(user.userType)) { throw new Error('Not allowed to create new gig'); } } diff --git a/src/app/appUtils.ts b/src/app/appUtils.ts index a158e28..fd58396 100644 --- a/src/app/appUtils.ts +++ b/src/app/appUtils.ts @@ -1,6 +1,6 @@ import Debug from 'debug'; import type { Express, Request, Response } from 'express'; -import type http from 'http'; +import type http from 'node:http'; import type { ISocketConsumer } from '../types/index.js'; interface RequestData { diff --git a/src/model/gig/gig-controller.ts b/src/model/gig/gig-controller.ts index 34c08c7..993a0fe 100644 --- a/src/model/gig/gig-controller.ts +++ b/src/model/gig/gig-controller.ts @@ -6,15 +6,25 @@ import type { IGig, SortOrder } from '../../types/index.js'; // entirely, so treating undefined/null as the default artist keeps the live // JaMmusic site working on both sides of the wj-prod -> web-jam-data Mongo // repoint (before: no artist field at all; after: artist:"josh"). +// Widened (#276) to also match `jammusic` so the live calendar survives the +// web-jam-back#1058 re-tagging migration before DEFAULT_ARTIST is narrowed. export const DEFAULT_ARTIST = 'josh'; class GigController extends Controller { // Scope gigs to an artist. The default artist also matches legacy docs - // that have no `artist` field (or it's null) — see DEFAULT_ARTIST above. + // that have no `artist` field (or it's null) — see DEFAULT_ARTIST above — + // and widened (#276) to match the incoming `jammusic` slug. // Non-default artists (e.g. "tim") match exactly. async getAllByArtistSort(artist: string, sort: SortOrder): Promise { const query = artist === DEFAULT_ARTIST - ? { $or: [{ artist: DEFAULT_ARTIST }, { artist: { $exists: false } }, { artist: null }] } + ? { + $or: [ + { artist: DEFAULT_ARTIST }, + { artist: 'jammusic' }, + { artist: { $exists: false } }, + { artist: null }, + ], + } : { artist }; return this.getAllSort(sort, query); } diff --git a/test/AgController/index.spec.ts b/test/AgController/index.spec.ts index 8604850..5f09ee9 100644 --- a/test/AgController/index.spec.ts +++ b/test/AgController/index.spec.ts @@ -177,16 +177,20 @@ describe('AgControler', () => { }); it('gets all tours', async () => { const agController = new AgController(aStub); + const transmitMock = vi.fn(); const cStub: IClient = { socket: { id: '123', listener: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ done: true, value: '1000' }) }) }), - transmit: () => { }, + transmit: transmitMock, receiver: () => ({ createConsumer: () => ({ next: () => Promise.resolve({ value: 123, done: true }) }) }), }, }; agController.gigController.getAllByArtistSort = vi.fn(() => Promise.resolve([])); r = await agController.sendGigs(cStub); + expect(agController.gigController.getAllByArtistSort).toHaveBeenCalledWith('josh', { datetime: -1 }); + expect(transmitMock).toHaveBeenCalledWith('allGigs', []); + expect(transmitMock).toHaveBeenCalledWith('allTours', []); expect(r).toBe('sent gigs'); }); it('gets gigs for a non-default artist on a scoped channel', async () => { diff --git a/test/gig/gig-controller.test.ts b/test/gig/gig-controller.test.ts index f3329c3..7aba8d9 100644 --- a/test/gig/gig-controller.test.ts +++ b/test/gig/gig-controller.test.ts @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; -import controller from '../../src/model/gig/gig-controller.js'; +import controller, { DEFAULT_ARTIST } from '../../src/model/gig/gig-controller.js'; +import type { IGig, QueryFilter } from '../../src/types/index.js'; describe('GigController', () => { const testId = new mongoose.Types.ObjectId(); @@ -46,6 +47,83 @@ describe('GigController', () => { controller.model.findByIdAndUpdate = vi.fn(() => Promise.resolve(null)); await expect(controller.findByIdAndUpdate(testId, {})).rejects.toThrow('Id Not Found'); }); + describe('getAllByArtistSort', () => { + const gigJosh = { _id: '1', venue: 'Josh Gig', artist: 'josh' }; + const gigJammusic = { _id: '2', venue: 'JaMmusic Gig', artist: 'jammusic' }; + const gigMissingArtist = { _id: '3', venue: 'Missing Artist Gig' }; + const gigNullArtist = { _id: '4', venue: 'Null Artist Gig', artist: null }; + const gigTim = { _id: '5', venue: 'Tim Gig', artist: 'tim' }; + const allSampleGigs = [gigJosh, gigJammusic, gigMissingArtist, gigNullArtist, gigTim]; + + const matchQuery = (query: Record, doc: Record): boolean => { + if (Array.isArray(query.$or)) { + return query.$or.some((clause: Record) => { + if ('artist' in clause) { + const target = clause.artist; + if (target && typeof target === 'object' && '$exists' in (target as Record)) { + return !('artist' in doc); + } + return doc.artist === target; + } + return false; + }); + } + if ('artist' in query) { + return doc.artist === query.artist; + } + return false; + }; + + beforeEach(() => { + controller.model.findSort = vi.fn((query: QueryFilter) => { + const matched = allSampleGigs.filter((g) => matchQuery(query as Record, g)); + return Promise.resolve(matched as unknown as IGig[]); + }); + }); + + it('builds widened $or query for the default artist including jammusic', async () => { + const findSortMock = vi.fn(() => Promise.resolve([] as IGig[])); + controller.model.findSort = findSortMock; + await controller.getAllByArtistSort(DEFAULT_ARTIST, { datetime: -1 }); + expect(findSortMock).toHaveBeenCalledWith( + { + $or: [ + { artist: DEFAULT_ARTIST }, + { artist: 'jammusic' }, + { artist: { $exists: false } }, + { artist: null }, + ], + }, + { datetime: -1 }, + ); + }); + + it('matches josh, jammusic, missing artist, and null for default artist, and excludes tim', async () => { + const results = await controller.getAllByArtistSort(DEFAULT_ARTIST, { datetime: -1 }); + expect(results).toContain(gigJosh); + expect(results).toContain(gigJammusic); + expect(results).toContain(gigMissingArtist); + expect(results).toContain(gigNullArtist); + expect(results).not.toContain(gigTim); + expect(results).toHaveLength(4); + }); + + it('specifically matches a gig carrying artist: "jammusic" under the default filter', async () => { + const results = await controller.getAllByArtistSort(DEFAULT_ARTIST, { datetime: -1 }); + const jammusicGig = results.find((g) => g.artist === 'jammusic'); + expect(jammusicGig).toBeDefined(); + expect(jammusicGig?.venue).toBe('JaMmusic Gig'); + }); + + it('builds exact match query for non-default artist and returns only matching gigs', async () => { + const results = await controller.getAllByArtistSort('tim', { datetime: -1 }); + expect(results).toEqual([gigTim]); + expect(results).not.toContain(gigJosh); + expect(results).not.toContain(gigJammusic); + expect(results).not.toContain(gigMissingArtist); + expect(results).not.toContain(gigNullArtist); + }); + }); it('should wait unit tests finish before exiting', async () => { const delay = (ms: number) => new Promise((resolve) => { setTimeout(() => resolve(true), ms); }); await delay(1000);