From 6898e3142c07768e0d3028d3fafc089cba0fa41c Mon Sep 17 00:00:00 2001 From: pieartsy Date: Tue, 24 Feb 2026 09:16:55 -0500 Subject: [PATCH 1/4] Create CONTRIBUTING.md --- CONTRIBUTING.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d7ef9b7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +Thanks for being interested in contributing to PluralFlux! I really can't do this by myself, nor do I want to! + +This is a guide for code contributions only. If you're looking to contribute _money_, please go to my [sponsorship page](https://github.com/sponsors/pieartsy)! + +## Disclaimer +The PluralFlux team is endogenic-friendly. Even if you disagree with this, keep discourse takes to yourself. If you can't be civil about it, please do not contribute. Other bigotry (transphobia, racism, ableism, fatphobia, etc) will not be tolerated either. + +## Resources: +Not too many right now, but I'm hoping to get a wiki up. +- [Issues tracker](https://github.com/pieartsy/PluralFlux/issues) +- [Pluralflux Support server](https://fluxer.gg/WaO6qGdU) where you can contact me (there's a #contributing channel for contributors) +- You can also reach me @pieartsy on Discord (or anywhere, really) if/when Fluxer is down. + +## Requirements +- [Fluxer.js](https://fluxerjs.blstmo.com/) +- Docker +- Node version 25.3.8 + +## Submitting changes +- Submit a pull request to this repository and explain your code and changes. +- We squash-merge commits, but keep to the [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) structure for your PR titles. Conventions are not necessary for commits themselves, but try to keep them readable anyway. +- Branches should target one specific issue in the Issue Tracker and try not to touch other features. Link to the issue in your PR. +- All commits will undergo PR review, at minimum by the main dev right now. If you can't explain or defend your code, it may be rejected. + +## Standards +- Docstrings are *mandatory*, following the standards in [JSDoc](https://michaelcurrin.github.io/dev-cheatsheets/cheatsheets/javascript/general/jsdoc.html). +- Comments are encouraged for confusing code. Prioritize readability (for example, just write an if/else instead of chaining ternaries). +- Reusable message replies should go in the enums file so we don't have to hunt them down to change wording. +- We use [jest](https://jestjs.io/) for testing. Please write unit tests and ideally integration tests for your code. Shoot for 60% coverage at minimum. Check that other features that touch your changes don't break. + +### LLM usage +**Do *not* insert code that has been LLM/GenAI generated.** All code you submit must be handwritten by yourself. This includes writing tests. Vibe coding is especially **not** allowed. Please disclose if you've used any AI for any other reasons, such as rubber-ducking or figuring out bugs or something. The main dev is somewhat more open to these uses because of search engines enshittifying--but frequent LLM usage is heavily discouraged due to the ethical concerns as well as damage to critical thinking skills. Only turn to LLMs if scouring search engines, Stack Overflow, and your friends list has not worked. From dc0de4b092b449113358dd6e969f8ae199d3801a Mon Sep 17 00:00:00 2001 From: pieartsy Date: Tue, 24 Feb 2026 09:23:07 -0500 Subject: [PATCH 2/4] Update README.md added LLM note --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8904f7f..228306b 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,7 @@ All commands are prefixed by `pf;`. Currently only a few are implemented. - `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.** ## Upcoming -- [ ] More than one proxy possible per member (including on import) -- [ ] File attachments -- [ ] React with x to delete message -- [ ] System tag at the end of messages -- [ ] Optionally keep proxy tag in message -- [ ] Autoproxy front +Check for, and add, feature requests in the [Issues tracker](https://github.com/pieartsy/PluralFlux/issues). + +## LLM note +I do **not** use LLMs or other GenAI to generate code, nor do I ever plan to. _Very_ rarely, I ask questions of LLMs to troubleshoot bugs after search engines/StackOverflow/friends' knowledge has failed me, but that should lessen even more over time. As well, I used the Docker "Gordon" LLM to fix the many errors in my initial docker compose, but now that I have a devops person helping me, that should never happen again. From 428310dfade3aa918d38c0254b200950736f24fa Mon Sep 17 00:00:00 2001 From: pieartsy Date: Tue, 24 Feb 2026 09:30:27 -0500 Subject: [PATCH 3/4] Delete .github/workflows/node.js.yml (#20) Since we're moving to local DevOps, this is no longer required. Also it was broken. --- .github/workflows/node.js.yml | 39 ----------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 .github/workflows/node.js.yml diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index e158f80..0000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - -name: Node.js CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [25.3.0] - - steps: - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Run tests - run: npm test - working-directory: tests - - - name: Tests failed - if: failure() - run: exit 1 - - - name: Tests passed - run: npm run build --if-present - working-directory: src From 7fead5e3d76b02ab617c6b89d7d0d66ee6f90141 Mon Sep 17 00:00:00 2001 From: pieartsy Date: Tue, 24 Feb 2026 12:42:23 -0500 Subject: [PATCH 4/4] feat: Add unit tests (#21) Tests are mostly complete (though some are failing -- to be fixed in the async/await refactor. Also refactored, fixed bugs found while testing, and added ability to type `pf;m` instead of `pf;member`. --------- Co-authored-by: Aster Fialla --- .dockerignore | 2 +- .env.jest | 2 + README.md | 2 +- package-lock.json | 304 +++++----- package.json | 7 +- src/bot.js | 69 ++- src/commands.js | 122 ++-- src/enums.js | 9 +- src/helpers/importHelper.js | 6 +- src/helpers/memberHelper.js | 595 ++++++++----------- src/helpers/messageHelper.js | 11 +- src/helpers/utils.js | 29 + tests/bot.test.js | 322 +++++++++++ tests/commands.test.js | 183 ++++++ tests/helpers/importHelper.test.js | 100 ++++ tests/helpers/memberHelper.test.js | 865 ++++++++++++++++++---------- tests/helpers/messageHelper.test.js | 10 +- tests/helpers/utils.test.js | 19 + 18 files changed, 1774 insertions(+), 883 deletions(-) create mode 100644 .env.jest create mode 100644 src/helpers/utils.js create mode 100644 tests/bot.test.js create mode 100644 tests/commands.test.js create mode 100644 tests/helpers/importHelper.test.js create mode 100644 tests/helpers/utils.test.js diff --git a/.dockerignore b/.dockerignore index cd967fc..60b99a7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ **/.dockerignore -**/.env +.env.jest **/.git **/.gitignore **/.project diff --git a/.env.jest b/.env.jest new file mode 100644 index 0000000..c130787 --- /dev/null +++ b/.env.jest @@ -0,0 +1,2 @@ +FLUXER_BOT_TOKEN=jest-fluxer-bot-token +POSTGRES_PASSWORD=jest-postgres-password \ No newline at end of file diff --git a/README.md b/README.md index 228306b..b63e6bf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ All commands are prefixed by `pf;`. Currently only a few are implemented. 1. Pass in a direct remote image URL, for example: `pf;member jane propic `. You can upload images on sites like . 2. Upload an attachment directly. **NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended. - - `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.** + - `proxy` Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy A{text}` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.** ## Upcoming Check for, and add, feature requests in the [Issues tracker](https://github.com/pieartsy/PluralFlux/issues). diff --git a/package-lock.json b/package-lock.json index 4f0d6e7..c056221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,8 @@ "name": "pluralflux", "version": "1.0.0", "dependencies": { - "@fluxerjs/core": "^1.1.5", + "@fluxerjs/core": "^1.2.2", "dotenv": "^17.3.1", - "node-fetch": "^3.3.2", "pg": "^8.18.0", "pg-hstore": "^2.3.4", "pm2": "^6.0.14", @@ -21,8 +20,11 @@ "@babel/core": "^7.29.0", "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/preset-env": "^7.29.0", + "@fetch-mock/jest": "^0.2.20", "babel-jest": "^30.2.0", - "jest": "^30.2.0" + "fetch-mock": "^12.6.0", + "jest": "^30.2.0", + "jest-fetch-mock": "^3.0.3" } }, "node_modules/@babel/code-frame": { @@ -1854,97 +1856,90 @@ "tslib": "^2.4.0" } }, - "node_modules/@fluxerjs/builders": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/builders/-/builders-1.1.5.tgz", - "integrity": "sha512-E7O+uf/DwgrCNkbxgo9/0FzhmP3a2ZH6SBJX9R+yUSsa07u5CdelrIatlp27UyoeBgO5HhXGMdncxa1geyf1tg==", - "license": "Apache-2.0", + "node_modules/@fetch-mock/jest": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@fetch-mock/jest/-/jest-0.2.20.tgz", + "integrity": "sha512-DGX2bhBInodaWPMV3+UZ530aVM3wDj16sAPjFzkrwb0JwNWIQK07CNbYprQ3Tmd2ixDJeaNx2E0aNb+hRb8FFA==", + "dev": true, + "license": "MIT", "dependencies": { - "@fluxerjs/types": "1.1.5", - "@fluxerjs/util": "1.1.5" + "fetch-mock": "^12.6.0" + }, + "engines": { + "node": ">=18.11.0" + }, + "peerDependencies": { + "@jest/globals": "*", + "jest": "*" } }, - "node_modules/@fluxerjs/builders/node_modules/@fluxerjs/types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz", - "integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==", + "node_modules/@fluxerjs/builders": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/builders/-/builders-1.2.2.tgz", + "integrity": "sha512-647yuM44Fs+Cf/MKo01XRWoVb7RoFdSOBHebFiTLLCTO/tqz+dgZ4wgVsDNu9O/BB/oZ2tjAp9jDTxvXMXkUbA==", + "license": "Apache-2.0", + "dependencies": { + "@fluxerjs/types": "1.2.2", + "@fluxerjs/util": "1.2.2" + } + }, + "node_modules/@fluxerjs/collection": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/collection/-/collection-1.2.2.tgz", + "integrity": "sha512-ryytiFh38gELqOLHBtL6Tz4f7/7DIu9L2CMTOqNYoer7E1ef8ukU7YiyANASWHgTf34hcGMC0tdi0dcFFFKkKQ==", "license": "Apache-2.0" }, "node_modules/@fluxerjs/core": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/core/-/core-1.1.5.tgz", - "integrity": "sha512-fklZePpz5SUZ0XQ9xU4CtKjtxUSnBrwRINKOXrEK6F8GWZgYotAKxc3Qkd07HRLP37EvOsuEl03GBnD+mq+LAQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/core/-/core-1.2.2.tgz", + "integrity": "sha512-P+c1jveYqwWGT6vafw+U1WmDPJ0Brp+aNa52Qp2vSsEQ3FLNUg+/25PhyrcPP0x2Mf7zoa1Wv9zGLlkHlKb9Ag==", "license": "Apache-2.0", "dependencies": { - "@fluxerjs/builders": "1.1.5", - "@fluxerjs/collection": "1.1.5", - "@fluxerjs/rest": "1.1.5", - "@fluxerjs/types": "1.1.5", - "@fluxerjs/util": "1.1.5", - "@fluxerjs/ws": "1.1.5" + "@fluxerjs/builders": "1.2.2", + "@fluxerjs/collection": "1.2.2", + "@fluxerjs/rest": "1.2.2", + "@fluxerjs/types": "1.2.2", + "@fluxerjs/util": "1.2.2", + "@fluxerjs/ws": "1.2.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@fluxerjs/core/node_modules/@fluxerjs/collection": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/collection/-/collection-1.1.5.tgz", - "integrity": "sha512-ms72WuFzLtnl8wYM5kAJh4vQovE8K8m0yyqjKxPFHUaTyXIJXAKPR+HmUOvWGj/vYKOdy7Pnz/+QuVho4ayCHQ==", - "license": "Apache-2.0" - }, - "node_modules/@fluxerjs/core/node_modules/@fluxerjs/types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz", - "integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==", - "license": "Apache-2.0" - }, "node_modules/@fluxerjs/rest": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/rest/-/rest-1.1.5.tgz", - "integrity": "sha512-LwksYkjSEEYYb8B/L7xPbrHjFrX8naivOY8aQ/ZjkZrO5pdmGYM4QPAcF8sgTZxZJaQdDHoxXr5ivDr459pApg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/rest/-/rest-1.2.2.tgz", + "integrity": "sha512-Y96A2wQJ8akiAPpiyQm2WWBfNvrQBGIxIuwFCTuXkWg/g+0D6hYrVB7VHxQlnBeDUlj4T+fSJiqSoAYQ3pwrCQ==", "license": "Apache-2.0", "dependencies": { - "@fluxerjs/types": "1.1.5" + "@fluxerjs/types": "1.2.2" } }, - "node_modules/@fluxerjs/rest/node_modules/@fluxerjs/types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz", - "integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==", + "node_modules/@fluxerjs/types": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.2.2.tgz", + "integrity": "sha512-z3AcTxVF2iY/D0XR8xGIcx+c6LY6eNLWR0uO46xNGmEqpm5guE3joDz/EN8DfAZXuap/ludgqX6EA8dLADIeMg==", "license": "Apache-2.0" }, "node_modules/@fluxerjs/util": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/util/-/util-1.1.5.tgz", - "integrity": "sha512-W3XfAXZ3wHQ7++MxK7sL/s8vfT2x9GFAu6e7rSkDOnAvLx4Uq5E7yXEuLRnvYEOejNd9xP960Ut/RXX6Zlvl9g==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/util/-/util-1.2.2.tgz", + "integrity": "sha512-H0c6rKufJJQsz7cloZSK1EqqbRCaxEIF2LdJ4cIRiGPczQTleD2xAIOQ2NvBZxnFvwx+OeetZs79b42bsjcjHQ==", "license": "Apache-2.0", "dependencies": { - "@fluxerjs/types": "1.1.5" + "@fluxerjs/types": "1.2.2" } }, - "node_modules/@fluxerjs/util/node_modules/@fluxerjs/types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz", - "integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==", - "license": "Apache-2.0" - }, "node_modules/@fluxerjs/ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-jzMugI6N/ZopI3MPwgaDQ09qUe4fm0L2LR5YAxekuVMImkx0eKSDpTtl/rEGXvLQjvDbMDrzvNJ17ku8we/Udg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@fluxerjs/ws/-/ws-1.2.2.tgz", + "integrity": "sha512-twg1wYRYo4DlEHsVHSuUhSAKGK2qTmHM2PV3LNSdE0HdGQIB1EFviSuo0rWDpCb1sVTwIzBVMtthrlxyPfVtKA==", "license": "Apache-2.0", "dependencies": { - "@fluxerjs/types": "1.1.5", + "@fluxerjs/types": "1.2.2", "ws": "^8.18.0" } }, - "node_modules/@fluxerjs/ws/node_modules/@fluxerjs/types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@fluxerjs/types/-/types-1.1.5.tgz", - "integrity": "sha512-YSRt3E6eHDJLrMK+9eNC3ZRkIZjRzWmXM2Ro+6CSnYuF5c8PSuLUmTM6HGKBA3z8qCSv4whB8ewwr9/x9WxGhA==", - "license": "Apache-2.0" - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2143,6 +2138,7 @@ "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -2813,6 +2809,13 @@ "@types/ms": "*" } }, + "node_modules/@types/glob-to-regexp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz", + "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3855,6 +3858,16 @@ "integrity": "sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==", "license": "MIT" }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3947,6 +3960,16 @@ "node": ">= 14" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4206,27 +4229,20 @@ "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/fetch-mock": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz", + "integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==", + "dev": true, "license": "MIT", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "@types/glob-to-regexp": "^0.4.4", + "dequal": "^2.0.3", + "glob-to-regexp": "^0.4.1", + "regexparam": "^3.0.0" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=18.11.0" } }, "node_modules/fill-range": { @@ -4292,18 +4308,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4437,6 +4441,13 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4781,6 +4792,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -5012,6 +5024,17 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-haste-map": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", @@ -5699,51 +5722,25 @@ "node": ">= 0.4.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-fetch/node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-int64": { @@ -6393,6 +6390,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/promptly": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", @@ -6504,6 +6508,16 @@ "node": ">=4" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/regexpu-core": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", @@ -7233,6 +7247,13 @@ "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", "license": "MIT" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7470,13 +7491,22 @@ "makeerror": "1.0.12" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/which": { diff --git a/package.json b/package.json index c8f1a20..157809e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "private": true, "dependencies": { - "@fluxerjs/core": "^1.1.5", + "@fluxerjs/core": "^1.2.2", "dotenv": "^17.3.1", "pg": "^8.18.0", "pg-hstore": "^2.3.4", @@ -21,8 +21,11 @@ "@babel/core": "^7.29.0", "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@babel/preset-env": "^7.29.0", + "@fetch-mock/jest": "^0.2.20", "babel-jest": "^30.2.0", - "jest": "^30.2.0" + "fetch-mock": "^12.6.0", + "jest": "^30.2.0", + "jest-fetch-mock": "^3.0.3" }, "scripts": { "test": "jest" diff --git a/src/bot.js b/src/bot.js index 3a89b31..08a07af 100644 --- a/src/bot.js +++ b/src/bot.js @@ -1,46 +1,63 @@ -import { Client, Events } from '@fluxerjs/core'; +import { Client, Events, Message } from '@fluxerjs/core'; import { messageHelper } from "./helpers/messageHelper.js"; import {enums} from "./enums.js"; import {commands} from "./commands.js"; import {webhookHelper} from "./helpers/webhookHelper.js"; -import * as env from 'dotenv'; +import env from 'dotenv'; +import {utils} from "./helpers/utils.js"; -env.config(); +env.config({path: './.env'}); -const token = process.env.FLUXER_BOT_TOKEN; +export const token = process.env.FLUXER_BOT_TOKEN; if (!token) { console.error("Missing FLUXER_BOT_TOKEN environment variable."); process.exit(1); } -const client = new Client({ intents: 0 }); +export const client = new Client({ intents: 0 }); client.on(Events.MessageCreate, async (message) => { - try { - // Ignore bots and messages without content - if (message.author.bot || !message.content) return; + await handleMessageCreate(message); +}); +/** + * Calls functions based off the contents of a message object. + * + * @async + * @param {Message} message - The message object + * + **/ +export const handleMessageCreate = async function(message) { + try { // Parse command and arguments const content = message.content.trim(); + // Ignore bots and messages without content + if (message.author.bot || content.length === 0) return; // If message doesn't start with the bot prefix, it could still be a message with a proxy tag. If it's not, return. if (!content.startsWith(messageHelper.prefix)) { - await webhookHelper.sendMessageAsMember(client, message, content).catch((e) => { + await webhookHelper.sendMessageAsMember(client, message).catch((e) => { throw e }); return; } const commandName = content.slice(messageHelper.prefix.length).split(" ")[0]; + // If there's no command name (ie just the prefix) if (!commandName) return await message.reply(enums.help.SHORT_DESC_PLURALFLUX); const args = messageHelper.parseCommandArgs(content, commandName); - const command = commands.get(commandName); + let command = commands.commandsMap.get(commandName) + if (!command) { + const commandFromAlias = commands.aliasesMap.get(commandName); + command = commandFromAlias ? commands.commandsMap.get(commandFromAlias.command) : null; + } + if (command) { - await command.execute(message, client, args).catch(e => { + await command.execute(message, args).catch(e => { throw e }); } @@ -52,7 +69,7 @@ client.on(Events.MessageCreate, async (message) => { console.error(error); // return await message.reply(error.message); } -}); +} client.on(Events.Ready, () => { console.log(`Logged in as ${client.user?.username}`); @@ -61,27 +78,23 @@ client.on(Events.Ready, () => { let guildCount = 0; client.on(Events.GuildCreate, () => { guildCount++; - callback(); + debouncePrintGuilds(); }); function printGuilds() { console.log(`Serving ${client.guilds.size} guild(s)`); } -const callback = Debounce(printGuilds, 2000); +const debouncePrintGuilds = utils.debounce(printGuilds, 2000); +export const debounceLogin = utils.debounce(client.login, 60000); -function Debounce(func, delay) { - let timeout = null; - return function (...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), delay); - }; -} +(async () => { + try { -try { - await client.login(token); - // await db.check_connection(); -} catch (err) { - console.error('Login failed:', err); - process.exit(1); -} \ No newline at end of file + await client.login(token); + // await db.check_connection(); + } catch (err) { + console.error('Login failed:', err); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/src/commands.js b/src/commands.js index 5a7edc1..428a526 100644 --- a/src/commands.js +++ b/src/commands.js @@ -4,33 +4,53 @@ import {memberHelper} from "./helpers/memberHelper.js"; import {EmbedBuilder} from "@fluxerjs/core"; import {importHelper} from "./helpers/importHelper.js"; -const cmds = new Map(); +const cmds = { + commandsMap: new Map(), + aliasesMap: new Map() +}; -cmds.set('member', { +cmds.aliasesMap.set('m', {command: 'member'}) + +cmds.commandsMap.set('member', { description: enums.help.SHORT_DESC_MEMBER, - async execute(message, client, args) { - const authorFull = `${message.author.username}#${message.author.discriminator}` - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - const attachmentExpires = message.attachments.size > 0 ? message.attachments.first().expires_at : null; - const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(async (e) =>{await message.reply(e.message);}); - if (typeof reply === 'string') { - return await message.reply(reply); - } - else if (reply instanceof EmbedBuilder) { - await message.reply({embeds: [reply.toJSON()]}) - } - else if (typeof reply === 'object') { - const errorsText = reply.errors.length > 0 ? reply.errors.join('\n- ') : null; - return await message.reply({content: `${reply.success} ${errorsText ? "\nThese errors occurred:\n" + errorsText : ""}`, embeds: [reply.embed.toJSON()]}) - } - + async execute(message, args) { + await cmds.memberCommand(message, args) } }) -cmds.set('help', { +/** + * Calls the member-related functions. + * + * @async + * @param {Message} message - The message object + * @param {string[]} args - The parsed arguments + * + **/ +cmds.memberCommand = async function(message, args) { + const authorFull = `${message.author.username}#${message.author.discriminator}` + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + const attachmentExpires = message.attachments.size > 0 ? message.attachments.first().expires_at : null; + + const reply = await memberHelper.parseMemberCommand(message.author.id, authorFull, args, attachmentUrl, attachmentExpires).catch(async (e) =>{console.error(e); await message.reply(e.message);}); + + if (typeof reply === 'string') { + await message.reply(reply); + } + else if (reply instanceof EmbedBuilder) { + await message.reply({embeds: [reply]}) + } + else if (typeof reply === 'object') { + const errorsText = reply.errors.length > 0 ? reply.errors.join('\n- ') : null; + return await message.reply({content: `${reply.success} ${errorsText ? `\n\n${enums.err.ERRORS_OCCURRED}\n` + errorsText : ""}`, embeds: [reply.embed]}) + } + +} + + +cmds.commandsMap.set('help', { description: enums.help.SHORT_DESC_HELP, async execute(message) { - const fields = [...cmds.entries()].map(([name, cmd]) => ({ + const fields = [...cmds.commandsMap.entries()].map(([name, cmd]) => ({ name: `${messageHelper.prefix}${name}`, value: cmd.description, inline: true, @@ -43,36 +63,48 @@ cmds.set('help', { .setFooter({ text: `Prefix: ${messageHelper.prefix}` }) .setTimestamp(); - await message.reply({ embeds: [embed.toJSON()] }); + await message.reply({ embeds: [embed] }); }, }) -cmds.set('import', { +cmds.commandsMap.set('import', { description: enums.help.SHORT_DESC_IMPORT, - async execute(message, client, args) { - const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; - if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) { - return await message.reply(enums.help.IMPORT); - } - return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => { - await message.reply(successfullyAdded); - }).catch(async (error) => { - if (error instanceof AggregateError) { - // errors.message can be a list of successfully added members, or say that none were successful. - let errorsText = `${error.message}.\nThese errors occurred:\n${error.errors.join('\n')}`; - - await message.reply(errorsText).catch(async () => { - const returnedBuffer = messageHelper.returnBufferFromText(errorsText); - await message.reply({content: returnedBuffer.text, files: [{ name: 'text.pdf', data: returnedBuffer.file }] - }) - }); - } - // If just one error was returned. - else { - return await message.reply(error.message); - } - }) + async execute(message, args) { + await cmds.importCommand(message, args); } }) +/** + * Calls the import-related functions. + * + * @async + * @param {Message} message - The message object + * @param {string[]} args - The parsed arguments + * + **/ +cmds.importCommand = async function(message, args) { + const attachmentUrl = message.attachments.size > 0 ? message.attachments.first().url : null; + if ((message.content.includes('--help') || (args[0] === '' && args.length === 1)) && !attachmentUrl ) { + return await message.reply(enums.help.IMPORT); + } + return await importHelper.pluralKitImport(message.author.id, attachmentUrl).then(async (successfullyAdded) => { + await message.reply(successfullyAdded); + }).catch(async (error) => { + if (error instanceof AggregateError) { + // errors.message can be a list of successfully added members, or say that none were successful. + let errorsText = `${error.message}.\n\n${enums.err.ERRORS_OCCURRED}\n${error.errors.join('\n')}`; + + await message.reply(errorsText).catch(async () => { + const returnedBuffer = messageHelper.returnBufferFromText(errorsText); + await message.reply({content: returnedBuffer.text, files: [{ name: 'text.txt', data: returnedBuffer.file }] + }) + }); + } + // If just one error was returned. + else { + return await message.reply(error.message); + } + }) +} + export const commands = cmds; \ No newline at end of file diff --git a/src/enums.js b/src/enums.js index 9375001..9c35204 100644 --- a/src/enums.js +++ b/src/enums.js @@ -7,11 +7,12 @@ helperEnums.err = { ADD_ERROR: "Error adding member.", MEMBER_EXISTS: "A member with that name already exists. Please pick a unique name.", USER_NO_MEMBERS: "You have no members created.", + NAME_REQUIRED: "You must set a unique name for the member for them to save.", DISPLAY_NAME_TOO_LONG: "The maximum length of a display name is 32 characters.", PROXY_EXISTS: "A duplicate proxy already exists for one of your members. Please pick a new one, or change the old one first.", NO_SUCH_COMMAND: "No such command exists.", PROPIC_FAILS_REQUIREMENTS: "Profile picture must be in JPG, PNG, or WEBP format and less than 10MB.", - PROPIC_CANNOT_LOAD: "Profile picture could not be loaded from URL.", + PROPIC_CANNOT_LOAD: "Profile picture could not be loaded. Are you sure this is a valid URL? (Try visiting the link to make sure!)", NO_WEBHOOKS_ALLOWED: "Channel does not support webhooks.", NOT_IN_SERVER: "You can only proxy in a server.", NO_MESSAGE_SENT_WITH_PROXY: 'Proxied message has no content.', @@ -19,7 +20,7 @@ helperEnums.err = { NO_PROXY_WRAPPER: "You need at least one proxy tag surrounding 'text', either before or after.\nCorrect usage examples: `pf;member jane proxy J:text`, `pf;member jane [text]`", NOT_JSON_FILE: "Please attach a valid JSON file.", NO_MEMBERS_IMPORTED: 'No members were imported.', - IMPORT_ERROR: "Please see attached file for logs on the member import process.", + ERRORS_OCCURRED: "These errors occurred:", COMMAND_NOT_RECOGNIZED: "Command not recognized. Try typing `pf;help` for command list.", SET_TO_NULL: "It has been set to null instead." } @@ -36,9 +37,9 @@ helperEnums.help = { LIST: "Lists members in the system. **Currently only lists the first 25.**", NAME: "Updates the name for a specific member based on their current name, for ex: `pf;member john name jane`. The member name should ideally be short so you can write other commands with it easily.", DISPLAY_NAME: "Updates the display name for a specific member based on their name, for example: `pf;member jane displayname \"Jane Doe | ze/hir\"`.This can be up to 32 characters long. If it has spaces, __put it in quotes__.", - PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy [text]` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**", + PROXY: "Updates the proxy tag for a specific member based on their name. The proxy must be formatted with the tags surrounding the word 'text', for example: `pf;member jane proxy Jane:text` or `pf;member amal proxy A{text}` This is so the bot can detect what the proxy tags are. **Only one proxy can be set per member currently.**", PROPIC: "Updates the profile picture for the member. Must be in JPG, PNG, or WEBP format and less than 10MB. The two options are:\n1. Pass in a direct remote image URL, for example: `pf;member jane propic https://cdn.pixabay.com/photo/2020/05/02/02/54/animal-5119676_1280.jpg`. You can upload images on sites like https://imgbb.com/.\n2. Upload an attachment directly.\n\n**NOTE:** Fluxer does not save your attachments forever, so option #1 is recommended.", - IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**" + IMPORT: "Imports from PluralKit using the JSON file provided by their export command. Importing from other proxy bots is TBD. `pf;import` and attach your JSON file to the message. This will only save the fields that are present in the bot currently, not anything else like birthdays or system handles (yet?). **Only one proxy can be set per member currently.**\n\n**PRO TIP**: For privacy reasons, try DMing the bot with this command and your JSON file--it should still work the same." } helperEnums.misc = { diff --git a/src/helpers/importHelper.js b/src/helpers/importHelper.js index 6cb2b15..68cd6cb 100644 --- a/src/helpers/importHelper.js +++ b/src/helpers/importHelper.js @@ -8,11 +8,11 @@ const ih = {}; * * @async * @param {string} authorId - The author of the message - * @param {string} attachmentUrl - The attached JSON url. + * @param {string | null} [attachmentUrl] - The attached JSON url. * @returns {string} A successful addition of all members. * @throws {Error} When the member exists, or creating a member doesn't work. */ -ih.pluralKitImport = async function (authorId, attachmentUrl) { +ih.pluralKitImport = async function (authorId, attachmentUrl= null) { if (!attachmentUrl) { throw new Error(enums.err.NOT_JSON_FILE); } @@ -32,7 +32,7 @@ ih.pluralKitImport = async function (authorId, attachmentUrl) { errors.push(e.message); }); } - const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : enums.err.NO_MEMBERS_IMPORTED; + const aggregatedText = addedMembers.length > 0 ? `Successfully added members: ${addedMembers.join(', ')}` : `${enums.err.NO_MEMBERS_IMPORTED}`; if (errors.length > 0) { throw new AggregateError(errors, aggregatedText); } diff --git a/src/helpers/memberHelper.js b/src/helpers/memberHelper.js index d8dfd37..9c0c671 100644 --- a/src/helpers/memberHelper.js +++ b/src/helpers/memberHelper.js @@ -1,45 +1,151 @@ import {database} from '../database.js'; import {enums} from "../enums.js"; -import {EmptyResultError, Op} from "sequelize"; +import {Op} from "sequelize"; import {EmbedBuilder} from "@fluxerjs/core"; +import {utils} from "./utils.js"; const mh = {}; -// Has an empty "command" to parse the help message properly -const commandList = ['--help', 'new', 'remove', 'name', 'list', 'displayName', 'proxy', 'propic', '']; +const commandList = ['new', 'remove', 'name', 'list', 'displayname', 'proxy', 'propic']; +const newAndRemoveCommands = ['new', 'remove']; /** - * Parses through the subcommands that come after "pf;member" and calls functions accordingly. + * Parses through the subcommands that come after "pf;member" to identify member name, command, and associated values. * * @async * @param {string} authorId - The id of the message author * @param {string} authorFull - The username and discriminator of the message author * @param {string[]} args - The message arguments - * @param {string | null} attachmentUrl - The message attachment url. - * @param {string | null} attachmentExpiration - The message attachment expiration (if uploaded via Fluxer) + * @param {string | null} [attachmentUrl] - The attachment URL, if any + * @param {string | null} [attachmentExpiration] - The attachment expiry date, if any * @returns {Promise} A success message. * @returns {Promise } A list of 25 members as an embed. - * @returns {Promise<{EmbedBuilder, [], string}>} A member info embed + info/errors. + * @returns {Promise } A list of member commands and descriptions. + * @returns {Promise<{EmbedBuilder, string[], string}>} A member info embed + info/errors. * @throws {Error} */ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUrl = null, attachmentExpiration = null) { - let member; + let memberName, command, isHelp = false; // checks whether command is in list, otherwise assumes it's a name - if (!commandList.includes(args[0]) && !args[1]) { - member = await mh.getMemberInfo(authorId, args[0]); + + // ex: pf;member remove, pf;member remove --help + // ex: pf;member, pf;member --help + if (args.length === 0 || args[0] === '--help' || args[0] === '') { + return mh.getMemberCommandInfo(); } - switch (args[0]) { - case '--help': - case '': - return mh.getMemberCommandInfo(); + // ex: pf;member remove somePerson + if (commandList.includes(args[0])) { + command = args[0]; + if (args[1]) { + memberName = args[1]; + } + } + // ex: pf;member somePerson propic + else if (args[1] && commandList.includes(args[1])) { + command = args[1]; + memberName = args[0]; + } + // ex: pf;member somePerson + else if (!commandList.includes(args[0]) && !args[1]) { + memberName = args[0]; + } + if (args[1] === "--help" || command && (memberName === "--help" || !memberName && command !== 'list')) { + isHelp = true; + } + + return await mh.memberArgumentHandler(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration) +} + +/** + * Parses through the command, argument, and values and calls appropriate functions based on their presence or absence. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} authorFull - The username and discriminator of the message author + * @param {boolean} isHelp - Whether this is a help command or not + * @param {string | null} [command] - The command name + * @param {string | null} [memberName] - The member name + * @param {string[]} [args] - The message arguments + * @param {string | null} [attachmentUrl] - The attachment URL, if any + * @param {string | null} [attachmentExpiration] - The attachment expiry date, if any + * @returns {Promise} A success message. + * @returns {Promise } A list of 25 members as an embed. + * @returns {Promise } A list of member commands and descriptions. + * @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors. + * @throws {Error} + */ +mh.memberArgumentHandler = async function(authorId, authorFull, isHelp, command = null, memberName = null, args = [], attachmentUrl = null, attachmentExpiration = null) { + if (!command && !memberName && !isHelp) { + throw new Error(enums.err.COMMAND_NOT_RECOGNIZED); + } + else if (isHelp) { + return mh.sendHelpEnum(command); + } + else if (command === "list") { + return await mh.getAllMembersInfo(authorId, authorFull); + } + else if (!memberName && !isHelp) { + throw new Error(enums.err.NO_MEMBER); + } + + // remove memberName and command from values to reduce confusion + const values = args.slice(2); + + // ex: pf;member blah blah + if (command && memberName && (values.length > 0 || newAndRemoveCommands.includes(command) || attachmentUrl)) { + return await mh.memberCommandHandler(authorId, command, memberName, values, attachmentUrl, attachmentExpiration).catch((e) => {throw e}); + } + else if (memberName && values.length === 0) { + return await mh.sendCurrentValue(authorId, memberName, command).catch((e) => {throw e}); + } +} + +/** + * Sends the current value of a field based on the command. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} memberName - The name of the member + * @param {string | null} [command] - The command being called to query a value. + * @returns {Promise} A success message. + * @returns {Promise } A list of 25 members as an embed. + * @returns {Promise } A list of member commands and descriptions. + * @returns {Promise<{EmbedBuilder, string[], string}>} A member info embed + info/errors. + */ +mh.sendCurrentValue = async function(authorId, memberName, command= null) { + const member = await mh.getMemberByName(authorId, memberName).then((m) => { + if (!m) throw new Error(enums.err.NO_MEMBER); + return m; + }); + + if (!command) { + return mh.getMemberInfo(member); + } + + switch (command) { + case 'name': + return `The name of ${member.name} is \"${member.name}\" but you probably already knew that!`; + case 'displayname': + return member.displayname ? `The display name for ${member.name} is \"${member.displayname}\".` : `Display name ${enums.err.NO_VALUE}`; + case 'proxy': + return member.proxy ? `The proxy for ${member.name} is \"${member.proxy}\".` : `Proxy ${enums.err.NO_VALUE}`; + case 'propic': + return member.propic ? `The profile picture for ${member.name} is \"${member.propic}\".` : `Propic ${enums.err.NO_VALUE}`; + } +} + +/** + * Sends the help text associated with a command. + * + * @param {string} command - The command being called. + * @returns {string} - The help text associated with a command. + */ +mh.sendHelpEnum = function(command) { + switch (command) { case 'new': - return await mh.addNewMember(authorId, args, attachmentUrl).catch((e) => { - throw e - }); + return enums.help.NEW; case 'remove': - return await mh.removeMember(authorId, args).catch((e) => { - throw e - }); + return enums.help.REMOVE; case 'name': return enums.help.NAME; case 'displayname': @@ -49,35 +155,40 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr case 'propic': return enums.help.PROPIC; case 'list': - if (args[1] && args[1] === "--help") { - return enums.help.LIST; - } - return await mh.getAllMembersInfo(authorId, authorFull).catch((e) => { - throw e - }); + return enums.help.LIST; } - switch (args[1]) { +} + +/** + * Handles the commands that need to call other update/edit commands. + * + * @async + * @param {string} authorId - The id of the message author + * @param {string} memberName - The name of the member + * @param {string} command - The command being called. + * @param {string[]} values - The values to be passed in. Only includes the values after member name and command name. + * @param {string | null} attachmentUrl - The attachment URL, if any + * @param {string | null} attachmentExpiration - The attachment expiry date, if any + * @returns {Promise} A success message. + * @returns {Promise } A list of 25 members as an embed. + * @returns {Promise } A list of member commands and descriptions. + * @returns {Promise<{EmbedBuilder, [string], string}>} A member info embed + info/errors. + * @throws {Error} + */ +mh.memberCommandHandler = async function(authorId, command, memberName, values, attachmentUrl = null, attachmentExpiration = null) { + switch (command) { + case 'new': + return await mh.addNewMember(authorId, memberName, values, attachmentUrl, attachmentExpiration).catch((e) => {throw e}); + case 'remove': + return await mh.removeMember(authorId, memberName).catch((e) => {throw e}); case 'name': - return await mh.updateName(authorId, args).catch((e) => { - throw e - }); + return await mh.updateName(authorId, memberName, values[0]).catch((e) => {throw e}); case 'displayname': - return await mh.updateDisplayName(authorId, args).catch((e) => { - throw e - }); + return await mh.updateDisplayName(authorId, memberName, values[0]).catch((e) => {throw e}); case 'proxy': - if (!args[2]) return await mh.getProxyByMember(authorId, args[0]).catch((e) => { - throw e - }); - return await mh.updateProxy(authorId, args).catch((e) => { - throw e - }); + return await mh.updateProxy(authorId, memberName, values[0]).catch((e) => {throw e}); case 'propic': - return await mh.updatePropic(authorId, args, attachmentUrl, attachmentExpiration).catch((e) => { - throw e - }); - default: - return member; + return await mh.updatePropic(authorId, memberName, values[0], attachmentUrl, attachmentExpiration).catch((e) => {throw e}); } } @@ -86,24 +197,23 @@ mh.parseMemberCommand = async function (authorId, authorFull, args, attachmentUr * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @param {string | null} attachmentURL - The attachment URL, if any exists - * @returns {Promise} A successful addition. - * @throws {Error} When the member exists, or creating a member doesn't work. + * @param {string} memberName - The member name + * @param {string[]} values - The arguments following the member name and command + * @param {string | null} [attachmentUrl] - The attachment URL, if any + * @param {string | null} [attachmentExpiration] - The attachment expiry date, if any + * @returns {Promise<{EmbedBuilder, string[], string}>} A successful addition. + * @throws {Error} When creating a member doesn't work. */ -mh.addNewMember = async function (authorId, args, attachmentURL = null) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.NEW; - } - const memberName = args[1]; - const displayName = args[2]; - const proxy = args[3]; - const propic = args[4] ?? attachmentURL; +mh.addNewMember = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) { + const displayName = values[0]; + const proxy = values[1]; + const propic = values[2] ?? attachmentUrl; - return await mh.addFullMember(authorId, memberName, displayName, proxy, propic).then(async(response) => { - const memberInfoEmbed = await mh.getMemberInfo(authorId, memberName).catch((e) => {throw e}) + return await mh.addFullMember(authorId, memberName, displayName, proxy, propic, attachmentExpiration).then((response) => { + const memberInfoEmbed = mh.getMemberInfo(response.member); return {embed: memberInfoEmbed, errors: response.errors, success: `${memberName} has been added successfully.`}; }).catch(e => { + console.error(e); throw e; }) } @@ -113,24 +223,17 @@ mh.addNewMember = async function (authorId, args, attachmentURL = null) { * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments + * @param {string} memberName - The member to update + * @param {string} name - The message arguments * @returns {Promise} A successful update. * @throws {RangeError} When the name doesn't exist. */ -mh.updateName = async function (authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.NAME; - } - - const name = args[2]; - if (!name) { - return `The name for ${args[0]} is ${args[0]}, but you probably knew that!`; - } +mh.updateName = async function (authorId, memberName, name) { const trimmedName = name.trim(); if (trimmedName === '') { throw new RangeError(`Name ${enums.err.NO_VALUE}`); } - return await mh.updateMemberField(authorId, args).catch((e) => { + return await mh.updateMemberField(authorId, memberName, "name", trimmedName).catch((e) => { throw e }); } @@ -140,34 +243,21 @@ mh.updateName = async function (authorId, args) { * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments + * @param {string} membername - The member to update + * @param {string} displayname - The display name to set * @returns {Promise} A successful update. * @throws {RangeError} When the display name is too long or doesn't exist. */ -mh.updateDisplayName = async function (authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.DISPLAY_NAME; - } +mh.updateDisplayName = async function (authorId, membername, displayname) { + const trimmedName = displayname.trim(); - const memberName = args[0]; - const displayName = args[2]; - const trimmedName = displayName ? displayName.trim() : null; - - if (!displayName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member && member.displayname) { - return `Display name for ${memberName} is: \"${member.displayname}\".`; - } else if (member) { - throw new Error(`Display name ${enums.err.NO_VALUE}`); - } - }); - } else if (displayName.length > 32) { + if (trimmedName.length > 32) { throw new RangeError(enums.err.DISPLAY_NAME_TOO_LONG); } else if (trimmedName === '') { throw new RangeError(`Display name ${enums.err.NO_VALUE}`); } - return await mh.updateMemberField(authorId, args).catch((e) => { + return await mh.updateMemberField(authorId, membername, "displayname", trimmedName).catch((e) => { throw e }); } @@ -177,24 +267,16 @@ mh.updateDisplayName = async function (authorId, args) { * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments + * @param {string} memberName - The member to update + * @param {string} proxy - The proxy to set * @returns {Promise } A successful update. - * @throws {RangeError | Error} When an empty proxy was provided, or no proxy exists. + * @throws {Error} When an empty proxy was provided, or a proxy exists. */ -mh.updateProxy = async function (authorId, args) { - if (args[2] && args[2] === "--help") { - return enums.help.PROXY; - } - const proxyExists = await mh.checkIfProxyExists(authorId, args[2]).then((proxyExists) => { - return proxyExists; - }).catch((e) => { - throw e - }); - if (!proxyExists) { - return await mh.updateMemberField(authorId, args).catch((e) => { - throw e - }); - } +mh.updateProxy = async function (authorId, memberName, proxy) { + // Throws error if exists + await mh.checkIfProxyExists(authorId, proxy).catch((e) => { throw e; }); + + return await mh.updateMemberField(authorId, memberName, "proxy", proxy).catch((e) => { throw e;}); } /** @@ -202,53 +284,19 @@ mh.updateProxy = async function (authorId, args) { * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments - * @param {string} attachmentUrl - The url of the first attachment in the message - * @param {string | null} attachmentExpiry - The expiration date of the first attachment in the message (if uploaded to Fluxer) + * @param {string} memberName - The member to update + * @param {string} values - The message arguments + * @param {string | null} attachmentUrl - The attachment URL, if any + * @param {string | null} attachmentExpiration - The attachment expiry date, if any * @returns {Promise} A successful update. * @throws {Error} When loading the profile picture from a URL doesn't work. */ -mh.updatePropic = async function (authorId, args, attachmentUrl, attachmentExpiry = null) { - if (args[2] && args[2] === "--help") { - return enums.help.PROPIC; - } - let img; - const updatedArgs = args; - if (!updatedArgs[1] && !attachmentUrl) { - return enums.help.PROPIC; - } else if (attachmentUrl) { - updatedArgs[2] = attachmentUrl; - updatedArgs[3] = attachmentExpiry; - } - if (updatedArgs[2]) { - img = updatedArgs[2]; - } - const isValidImage = await mh.checkImageFormatValidity(img).catch((e) => { - throw e - }); - if (isValidImage) { - return await mh.updateMemberField(authorId, updatedArgs).catch((e) => { - throw e - }); - } -} +mh.updatePropic = async function (authorId, memberName, values, attachmentUrl = null, attachmentExpiration = null) { + const imgUrl = values ?? attachmentUrl; + // Throws error if invalid + await utils.checkImageFormatValidity(imgUrl).catch((e) => { throw e }); -/** - * Checks if an uploaded picture is in the right format. - * - * @async - * @param {string} imageUrl - The url of the image - * @returns {Promise} - If the image is a valid format. - * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. - */ -mh.checkImageFormatValidity = async function (imageUrl) { - const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; - return await fetch(imageUrl).then(r => r.blob()).then(blobFile => { - if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); - return true; - }).catch((error) => { - throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); - }); + return await mh.updateMemberField(authorId, memberName, "propic", imgUrl, attachmentExpiration).catch((e) => { throw e }); } /** @@ -256,16 +304,11 @@ mh.checkImageFormatValidity = async function (imageUrl) { * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments + * @param {string} memberName - The name of the member to remove * @returns {Promise} A successful removal. - * @throws {EmptyResultError} When there is no member to remove. + * @throws {Error} When there is no member to remove. */ -mh.removeMember = async function (authorId, args) { - if (args[1] && args[1] === "--help" || !args[1]) { - return enums.help.REMOVE; - } - - const memberName = args[1]; +mh.removeMember = async function (authorId, memberName) { return await database.members.destroy({ where: { name: {[Op.iLike]: memberName}, @@ -275,7 +318,7 @@ mh.removeMember = async function (authorId, args) { if (result) { return `Member "${memberName}" has been deleted.`; } - throw new EmptyResultError(`${enums.err.NO_MEMBER}`); + throw new Error(`${enums.err.NO_MEMBER}`); }) } @@ -287,13 +330,14 @@ mh.removeMember = async function (authorId, args) { * @async * @param {string} authorId - The author of the message * @param {string} memberName - The name of the member. - * @param {string | null} displayName - The display name of the member. - * @param {string | null} proxy - The proxy tag of the member. - * @param {string | null} propic - The profile picture URL of the member. - * @returns {Promise<{model, []}>} A successful addition object, including errors if there are any. + * @param {string | null} [displayName] - The display name of the member. + * @param {string | null} [proxy] - The proxy tag of the member. + * @param {string | null} [propic] - The profile picture URL of the member. + * @param {string | null} [attachmentExpiration] - The expiration date of an uploaded profile picture. + * @returns {Promise<{model, string[]}>} A successful addition object, including errors if there are any. * @throws {Error} When the member already exists, there are validation errors, or adding a member doesn't work. */ -mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { +mh.addFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null, attachmentExpiration = null) { await mh.getMemberByName(authorId, memberName).then((member) => { if (member) { throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); @@ -301,10 +345,19 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro }); const errors = []; + const trimmedName = memberName.trim(); + if (trimmedName.length === 0) { + throw new Error(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`); + } + let isValidDisplayName; - if (displayName && displayName.length > 0) { - const trimmedName = displayName ? displayName.trim() : null; - if (trimmedName && trimmedName.length > 32) { + if (displayName) { + const trimmedDisplayName= displayName ? displayName.trim() : null; + if (!trimmedDisplayName || trimmedDisplayName.length === 0) { + errors.push(`Display name ${enums.err.NO_VALUE}. ${enums.err.SET_TO_NULL}`); + isValidDisplayName = false; + } + else if (trimmedDisplayName.length > 32) { errors.push(`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`); isValidDisplayName = false; } @@ -313,6 +366,7 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro } } + let isValidProxy; if (proxy && proxy.length > 0) { await mh.checkIfProxyExists(authorId, proxy).then(() => { @@ -325,13 +379,16 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro let isValidPropic; if (propic && propic.length > 0) { - await mh.checkImageFormatValidity(propic).then(() => { + await utils.checkImageFormatValidity(propic).then(() => { isValidPropic = true; }).catch((e) => { errors.push(`Tried to set profile picture to \"${propic}\". ${e.message}. ${enums.err.SET_TO_NULL}`); isValidPropic = false; }); } + if (isValidPropic && attachmentExpiration) { + errors.push(mh.setExpirationWarning(attachmentExpiration)); + } const member = await database.members.create({ name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName : null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null }); @@ -339,126 +396,24 @@ mh.addFullMember = async function (authorId, memberName, displayName = null, pro return {member: member, errors: errors}; } -// mh.mergeFullMember = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { -// await mh.getMemberByName(authorId, memberName).then((member) => { -// if (member) { -// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); -// } -// }); -// -// let isValidDisplayName; -// if (displayName) { -// const trimmedName = displayName ? displayName.trim() : null; -// if (trimmedName && trimmedName.length > 32) { -// if (!isImport) { -// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); -// } -// isValidDisplayName = false; -// } -// } -// -// let isValidProxy; -// if (proxy) { -// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => { -// return res; -// }).catch((e) => { -// if (!isImport) { -// throw e -// } -// return false; -// }); -// } -// -// let isValidPropic; -// if (propic) { -// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => { -// return valid; -// }).catch((e) => { -// if (!isImport) { -// throw (e); -// } -// return false; -// }); -// } -// -// const member = await database.members.create({ -// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null, -// }); -// if (!member) { -// new Error(`${enums.err.ADD_ERROR}`); -// } -// return member; -// } -// -// mh.overwriteFullMemberFromImport = async function (authorId, memberName, displayName = null, proxy = null, propic = null) { -// await mh.getMemberByName(authorId, memberName).then((member) => { -// if (member) { -// throw new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`); -// } -// }); -// -// let isValidDisplayName; -// if (displayName) { -// const trimmedName = displayName ? displayName.trim() : null; -// if (trimmedName && trimmedName.length > 32) { -// if (!isImport) { -// throw new RangeError(`Can't add ${memberName}. ${enums.err.DISPLAY_NAME_TOO_LONG}`); -// } -// isValidDisplayName = false; -// } -// } -// -// let isValidProxy; -// if (proxy) { -// isValidProxy = await mh.checkIfProxyExists(authorId, proxy).then((res) => { -// return res; -// }).catch((e) => { -// if (!isImport) { -// throw e -// } -// return false; -// }); -// } -// -// let isValidPropic; -// if (propic) { -// isValidPropic = await mh.checkImageFormatValidity(propic).then((valid) => { -// return valid; -// }).catch((e) => { -// if (!isImport) { -// throw (e); -// } -// return false; -// }); -// } -// -// const member = await database.members.create({ -// name: memberName, userid: authorId, displayname: isValidDisplayName ? displayName: null, proxy: isValidProxy ? proxy : null, propic: isValidPropic ? propic : null, -// }); -// if (!member) { -// new Error(`${enums.err.ADD_ERROR}`); -// } -// return member; -// } - /** * Updates one fields for a member in the database. * * @async * @param {string} authorId - The author of the message - * @param {string[]} args - The message arguments + * @param {string} memberName - The member to update + * @param {string} columnName - The column name to update. + * @param {string} value - The value to update to. + * @param {string | null} [attachmentExpiration] - The attachment expiration date (if any) * @returns {Promise} A successful update. - * @throws {EmptyResultError | Error} When the member is not found, or catchall error. + * @throws {Error} When no member row was updated. */ -mh.updateMemberField = async function (authorId, args) { - const memberName = args[0]; - const columnName = args[1]; - const value = args[2]; +mh.updateMemberField = async function (authorId, memberName, columnName, value, attachmentExpiration = null) { let fluxerPropicWarning; // indicates that an attachment was uploaded on Fluxer directly - if (columnName === "propic" && args[3]) { - fluxerPropicWarning = mh.setExpirationWarning(args[3]); + if (columnName === "propic" && attachmentExpiration) { + fluxerPropicWarning = mh.setExpirationWarning(value); } return await database.members.update({[columnName]: value}, { where: { @@ -467,7 +422,7 @@ mh.updateMemberField = async function (authorId, args) { } }).then((res) => { if (res[0] === 0) { - throw new EmptyResultError(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`); + throw new Error(`Can't update ${memberName}. ${enums.err.NO_MEMBER}.`); } else { return `Updated ${columnName} for ${memberName} to ${value}${fluxerPropicWarning ?? ''}.`; } @@ -491,25 +446,19 @@ mh.setExpirationWarning = function (expirationString) { /** * Gets the details for a member. * - * @async - * @param {string} authorId - The author of the message - * @param {string} memberName - The message arguments - * @returns {Promise} The member's info. + * @param {model} member - The member object + * @returns {EmbedBuilder} The member's info. */ -mh.getMemberInfo = async function (authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return new EmbedBuilder() - .setTitle(member.name) - .setDescription(`Details for ${member.name}`) - .addFields({ - name: 'Display name: ', - value: member.displayname ?? 'unset', - inline: true - }, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},) - .setImage(member.propic); - } - }); +mh.getMemberInfo = function (member) { + return new EmbedBuilder() + .setTitle(member.name) + .setDescription(`Details for ${member.name}`) + .addFields({ + name: 'Display name: ', + value: member.displayname ?? 'unset', + inline: true + }, {name: 'Proxy tag: ', value: member.proxy ?? 'unset', inline: true},) + .setImage(member.propic ?? null); } /** @@ -539,42 +488,11 @@ mh.getAllMembersInfo = async function (authorId, authorName) { * @param {string} authorId - The author of the message. * @param {string} memberName - The member's name. * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. */ mh.getMemberByName = async function (authorId, memberName) { return await database.members.findOne({where: {userid: authorId, name: {[Op.iLike]: memberName}}}); } -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message. - * @param {string} memberName - The member's name. - * @returns {Promise} The member object. - * @throws { EmptyResultError } When the member is not found. - */ -mh.getProxyByMember = async function (authorId, memberName) { - return await mh.getMemberByName(authorId, memberName).then((member) => { - if (member) { - return member.proxy; - } - throw new EmptyResultError(enums.err.NO_MEMBER); - }) -} - -/** - * Gets a member based on the author and proxy tag. - * - * @async - * @param {string} authorId - The author of the message - * @param {string} proxy - The proxy tag - * @returns {Promise} The member object. - */ -mh.getMemberByProxy = async function (authorId, proxy) { - return await db.members.findOne({where: {userid: authorId, proxy: proxy}}); -} - /** * Gets all members belonging to the author. * @@ -586,7 +504,6 @@ mh.getMembersByAuthor = async function (authorId) { return await database.members.findAll({where: {userid: authorId}}); } - /** * Checks if proxy exists for a member. * @@ -596,21 +513,19 @@ mh.getMembersByAuthor = async function (authorId) { * @throws {Error} When an empty proxy was provided, or no proxy exists. */ mh.checkIfProxyExists = async function (authorId, proxy) { - if (proxy) { - const splitProxy = proxy.trim().split("text"); - if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); - if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); - - await mh.getMembersByAuthor(authorId).then((memberList) => { - const proxyExists = memberList.some(member => member.proxy === proxy); - if (proxyExists) { - throw new Error(enums.err.PROXY_EXISTS); - } - }).catch(e => { - throw e - }); - } + const splitProxy = proxy.trim().split("text"); + if (splitProxy.length < 2) throw new Error(enums.err.NO_TEXT_FOR_PROXY); + if (!splitProxy[0] && !splitProxy[1]) throw new Error(enums.err.NO_PROXY_WRAPPER); + await mh.getMembersByAuthor(authorId).then((memberList) => { + const proxyExists = memberList.some(member => member.proxy === proxy); + if (proxyExists) { + throw new Error(enums.err.PROXY_EXISTS); + } + }).catch(e => { + throw e + }); + return false; } /** diff --git a/src/helpers/messageHelper.js b/src/helpers/messageHelper.js index b80676d..6137439 100644 --- a/src/helpers/messageHelper.js +++ b/src/helpers/messageHelper.js @@ -1,15 +1,9 @@ import {memberHelper} from "./memberHelper.js"; -import {enums} from "../enums.js"; -import tmp, {setGracefulCleanup} from "tmp"; -import fs from 'fs'; -import {Message} from "@fluxerjs/core"; const msgh = {}; msgh.prefix = "pf;" -setGracefulCleanup(); - /** * Parses and slices up message arguments, retaining quoted strings. * @@ -38,11 +32,12 @@ msgh.parseCommandArgs = function(content, commandName) { /** * Parses messages to see if any part of the text matches the tags of any member belonging to an author. * + * @async * @param {string} authorId - The author of the message. * @param {string} content - The full message content - * @param {string | null} attachmentUrl - The url for an attachment to the message, if any exists. + * @param {string | null} [attachmentUrl] - The url for an attachment to the message, if any exists. * @returns {{model, string, bool}} The proxy message object. - * @throws {Error} If a proxy message is sent with no message within it. + * @throws {Error} If a proxy message is sent with no message or attachment within it. */ msgh.parseProxyTags = async function (authorId, content, attachmentUrl = null){ const members = await memberHelper.getMembersByAuthor(authorId); diff --git a/src/helpers/utils.js b/src/helpers/utils.js new file mode 100644 index 0000000..a980cbf --- /dev/null +++ b/src/helpers/utils.js @@ -0,0 +1,29 @@ +import {enums} from '../enums.js' + +const u = {}; + +u.debounce = function(func, delay) { + let timeout = null; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), delay); + }; +} + +/** + * Checks if an uploaded picture is in the right format. + * + * @async + * @param {string} imageUrl - The url of the image + * @throws {Error} When loading the profile picture from a URL doesn't work, or it fails requirements. + */ +u.checkImageFormatValidity = async function (imageUrl) { + const acceptableImages = ['image/png', 'image/jpg', 'image/jpeg', 'image/webp']; + await fetch(imageUrl).then(r => r.blob()).then(blobFile => { + if (blobFile.size > 1000000 || !acceptableImages.includes(blobFile.type)) throw new Error(enums.err.PROPIC_FAILS_REQUIREMENTS); + }).catch((error) => { + throw new Error(`${enums.err.PROPIC_CANNOT_LOAD}: ${error.message}`); + }); +} + +export const utils = u; diff --git a/tests/bot.test.js b/tests/bot.test.js new file mode 100644 index 0000000..bc75a07 --- /dev/null +++ b/tests/bot.test.js @@ -0,0 +1,322 @@ +const env = require('dotenv').config({path: './.env.jest'}) +const {enums} = require("../src/enums.js"); + +jest.mock('@fluxerjs/core', () => { + return { + Events: { + MessageCreate: jest.fn(), + Ready: jest.fn(), + GuildCreate: jest.fn(), + }, + Client: jest.fn().mockImplementation(() => { + return { + on: jest.fn(), + intents: 0, + login: jest.fn() + } + }), + Message: jest.fn() + }; +}); + +jest.mock("../src/helpers/messageHelper.js", () => { + return { + messageHelper: { + parseCommandArgs: jest.fn(), + prefix: "pf;" + } + } +}); + +jest.mock("../src/helpers/webhookHelper.js", () => { + return { + webhookHelper: { + sendMessageAsMember: jest.fn() + } + } +}) +jest.mock("../src/helpers/utils.js", () => { + return { + utils: { + debounce: jest.fn() + } + } +}) + +jest.mock("../src/commands.js", () => { + return { + commands: { + commandsMap: { + get: jest.fn(), + }, + aliasesMap: { + get: jest.fn() + } + } + } +}) + + +const {Client, Events} = require('@fluxerjs/core'); +const {messageHelper} = require("../src/helpers/messageHelper.js"); + +const {commands} = require("../src/commands.js"); +const {webhookHelper} = require("../src/helpers/webhookHelper.js"); + +const {utils} = require("../src/helpers/utils.js"); +let {handleMessageCreate, client} = require("../src/bot.js"); + +describe('bot', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }) + + describe('handleMessageCreate', () => { + + test('on message creation, if message is from bot, return', () => { + // Arrange + const message = { + author: { + bot: true + } + } + // Act + return handleMessageCreate(message).then((res) => { + expect(res).toBe(undefined); + }); + }) + + test('on message creation, if message is empty, return', () => { + // Arrange + const message = { + content: " ", + author: { + bot: false + } + } + // Act + return handleMessageCreate(message).then((res) => { + // Assert + expect(res).toBe(undefined); + }); + }) + + test("if message doesn't start with bot prefix, call sendMessageAsMember", () => { + // Arrange + webhookHelper.sendMessageAsMember.mockResolvedValue(); + const message = { + content: "hello", + author: { + bot: false + } + } + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledTimes(1); + expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledWith(client, message) + }); + }) + + test("if sendMessageAsMember returns error, log error", () => { + // Arrange + webhookHelper.sendMessageAsMember.mockImplementation(() => { + throw Error("error") + }); + const message = { + content: "hello", + author: { + bot: false + } + } + jest.mock('console', () => { + return {error: jest.fn()} + }) + // Act + return handleMessageCreate(message).catch(() => { + // Assert + expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledTimes(1); + expect(webhookHelper.sendMessageAsMember).toHaveBeenCalledWith(client, message) + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(new Error('error')) + }); + }) + + test("if no command after prefix, return correct enum", () => { + // Arrange + const message = { + content: "pf;", + author: { + bot: false + }, + reply: jest.fn() + } + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith(enums.help.SHORT_DESC_PLURALFLUX); + expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled(); + }); + }) + + test("if command after prefix, call parseCommandArgs and commandsMap.get", () => { + // Arrange + const message = { + content: "pf;help", + author: { + bot: false + }, + reply: jest.fn() + } + const command = { + execute: jest.fn().mockResolvedValue(), + } + commands.commandsMap.get = jest.fn().mockReturnValue(command); + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(messageHelper.parseCommandArgs).toHaveBeenCalledTimes(1); + expect(messageHelper.parseCommandArgs).toHaveBeenCalledWith('pf;help', 'help'); + expect(commands.commandsMap.get).toHaveBeenCalledTimes(1); + expect(commands.commandsMap.get).toHaveBeenCalledWith('help'); + expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled(); + }); + }) + + test('if commands.commandsMap.get returns undefined, call aliasesMap.get and commandsMap.get again with that value', () => { + // Arrange + const message = { + content: "pf;m", + author: { + bot: false + }, + reply: jest.fn() + } + const mockAlias = { + command: 'member' + } + commands.commandsMap.get = jest.fn().mockReturnValueOnce(); + commands.aliasesMap.get = jest.fn().mockReturnValueOnce(mockAlias); + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(commands.commandsMap.get).toHaveBeenCalledTimes(2); + expect(commands.commandsMap.get).toHaveBeenNthCalledWith(1, 'm'); + expect(commands.commandsMap.get).toHaveBeenNthCalledWith(2, 'member'); + expect(commands.aliasesMap.get).toHaveBeenCalledTimes(1); + expect(commands.aliasesMap.get).toHaveBeenCalledWith('m'); + }); + }) + + + test('if aliasesMap.get returns undefined, do not call commandsMap again', () => { + // Arrange + const message = { + content: "pf;m", + author: { + bot: false + }, + reply: jest.fn() + } + const mockAlias = { + command: 'member' + } + commands.commandsMap.get = jest.fn().mockReturnValueOnce(); + commands.aliasesMap.get = jest.fn().mockReturnValueOnce(); + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(commands.commandsMap.get).toHaveBeenCalledTimes(1); + expect(commands.commandsMap.get).toHaveBeenNthCalledWith(1, 'm'); + expect(commands.aliasesMap.get).toHaveBeenCalledTimes(1); + expect(commands.aliasesMap.get).toHaveBeenCalledWith('m'); + }); + }) + + test("if command exists, call command.execute", () => { + // Arrange + const message = { + content: "pf;member test", + author: { + bot: false + }, + reply: jest.fn() + } + const command = { + execute: jest.fn() + } + messageHelper.parseCommandArgs = jest.fn().mockReturnValue(['test']); + commands.commandsMap.get = jest.fn().mockReturnValue(command); + command.execute = jest.fn().mockResolvedValue(); + + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(command.execute).toHaveBeenCalledTimes(1); + expect(command.execute).toHaveBeenCalledWith(message, ['test']); + expect(webhookHelper.sendMessageAsMember).not.toHaveBeenCalled(); + }); + }) + + test("if command.execute returns error, log error", () => { + // Arrange + const command = { + execute: jest.fn() + } + commands.get = jest.fn().mockReturnValue(command); + command.execute.mockImplementation(() => { + throw Error("error") + }); + const message = { + content: "pf;member test", + author: { + bot: false + }, + reply: jest.fn() + } + jest.mock('console', () => { + return {error: jest.fn()} + }) + // Act + return handleMessageCreate(message).catch(() => { + // Assert + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith(new Error('error')) + }); + }) + + test("if command does not exist, return correct enum", () => { + // Arrange + commands.commandsMap.get = jest.fn().mockReturnValue(); + commands.aliasesMap.get = jest.fn().mockReturnValue(); + const message = { + content: "pf;asdfjlas", + author: { + bot: false + }, + reply: jest.fn() + } + // Act + return handleMessageCreate(message).then(() => { + // Assert + expect(message.reply).toHaveBeenCalledWith(enums.err.COMMAND_NOT_RECOGNIZED); + expect(message.reply).toHaveBeenCalledTimes(1); + }); + }) + }) + + test('calls client.login with correct argument', () => { + // Act + client.login = jest.fn().mockResolvedValue(); + // Assert + expect(client.login).toHaveBeenCalledTimes(1); + expect(client.login).toHaveBeenCalledWith(process.env.FLUXER_BOT_TOKEN) + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) \ No newline at end of file diff --git a/tests/commands.test.js b/tests/commands.test.js new file mode 100644 index 0000000..11bb337 --- /dev/null +++ b/tests/commands.test.js @@ -0,0 +1,183 @@ +import {enums} from "../src/enums.js"; + +jest.mock("../src/helpers/messageHelper.js", () => { + return { + messageHelper: { + returnBufferFromText: jest.fn(), + prefix: 'pf;' + } + } +}) + +jest.mock('../src/helpers/memberHelper.js', () => { + return { + memberHelper: { + parseMemberCommand: jest.fn() + } + } +}) + +jest.mock('../src/helpers/importHelper.js', () => { + return { + importHelper: { + pluralKitImport: jest.fn() + } + } +}) +jest.mock('console', () => { + return {error: jest.fn()} +}) + +import {messageHelper, prefix} from "../src/helpers/messageHelper.js"; + +import {memberHelper} from "../src/helpers/memberHelper.js"; +import {EmbedBuilder} from "@fluxerjs/core"; +import {importHelper} from "../src/helpers/importHelper.js"; +import {commands} from "../src/commands.js"; + + +describe('commands', () => { + const authorId = '123'; + const discriminator = '123'; + const username = 'somePerson' + const attachmentUrl = 'oya.png'; + const attachmentExpiration = new Date('2026-01-01').toDateString(); + const message = { + author: { + username: username, + id: authorId, + discriminator: discriminator, + }, + attachments: { + size: 1, + first: jest.fn().mockImplementation(() => ({ + expires_at: attachmentExpiration, + url: attachmentUrl + })) + }, + reply: jest.fn().mockResolvedValue(), + } + const args = ['new'] + + beforeEach(() => { + + jest.resetModules(); + jest.clearAllMocks(); + }) + + describe('memberCommand', () => { + + + test('calls parseMemberCommand with the correct arguments', () => { + // Arrange + memberHelper.parseMemberCommand = jest.fn().mockResolvedValue("parsed command"); + // Act + return commands.memberCommand(message, args).then(() => { + expect(memberHelper.parseMemberCommand).toHaveBeenCalledTimes(1); + expect(memberHelper.parseMemberCommand).toHaveBeenCalledWith(authorId, `${username}#${discriminator}`, args, attachmentUrl, attachmentExpiration); + }); + }) + + test('if parseMemberCommand returns error, log error and reply with error', () => { + // Arrange + memberHelper.parseMemberCommand = jest.fn().mockImplementation(() => {throw new Error('error')}); + // Act + return commands.memberCommand(message, args).catch(() => { + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith('error'); + expect(console.error).toHaveBeenCalledWith(new Error('error')); + }); + }) + + test('if parseMemberCommand returns embed, reply with embed', () => { + // Arrange + const embed = new EmbedBuilder(); + memberHelper.parseMemberCommand = jest.fn().mockResolvedValue(); + // Act + return commands.memberCommand(message, args).catch(() => { + // Assert + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith({embeds: [embed]}) + }); + }) + + test('if parseMemberCommand returns object, reply with embed and content', () => { + // Arrange + const reply = { + errors: ['error', 'error2'], + success: 'success', + embed: {} + } + memberHelper.parseMemberCommand = jest.fn().mockResolvedValue(reply); + // Act + return commands.memberCommand(message, args).catch(() => { + // Assert + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith({content: `success\n\n${enums.err.ERRORS_OCCURRED}\n\nerror\nerror2}`, embeds: [reply.embed]}) + }); + }) + }) + + describe('importCommand', () => { + test('if message includes --help and no attachmentURL, return help message', () => { + const args = ["--help"]; + message.content = "pf;import --help"; + message.attachments.size = 0; + return commands.importCommand(message, args).then(() => { + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith(enums.help.IMPORT); + expect(importHelper.pluralKitImport).not.toHaveBeenCalled(); + }) + }) + + test('if no args and no attachmentURL, return help message', () => { + const args = [""]; + message.content = 'pf;import' + message.attachments.size = 0; + return commands.importCommand(message, args).then(() => { + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith(enums.help.IMPORT); + expect(importHelper.pluralKitImport).not.toHaveBeenCalled(); + }) + }) + + test('if attachment URL, call pluralKitImport with correct arguments', () => { + const args = [""]; + message.content = 'pf;import' + importHelper.pluralKitImport = jest.fn().mockResolvedValue('success'); + return commands.importCommand(message, args).then(() => { + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith('success'); + expect(importHelper.pluralKitImport).toHaveBeenCalledTimes(1); + expect(importHelper.pluralKitImport).toHaveBeenCalledWith(authorId, attachmentUrl); + }) + }) + + test('if pluralKitImport returns aggregate errors, send errors.', () => { + const args = [""]; + message.content = 'pf;import' + importHelper.pluralKitImport = jest.fn().mockImplementation(() => {throw new AggregateError(['error1', 'error2'], 'errors')}); + return commands.importCommand(message, args).catch(() => { + expect(message.reply).toHaveBeenCalledTimes(1); + expect(message.reply).toHaveBeenCalledWith(`errors. \n\n${enums.err.ERRORS_OCCURRED}\n\nerror1\nerror2`); + }) + }) + + test('if message.reply throws error, call returnBufferFromText and message.reply again.', () => { + // Arrange + const args = [""]; + message.content = 'pf;import' + message.reply = jest.fn().mockImplementationOnce(() => {throw e}) + messageHelper.returnBufferFromText = jest.fn().mockResolvedValue({file: 'test.txt', text: 'normal content'}); + return commands.importCommand(message, args).catch(() => { + expect(message.reply).toHaveBeenCalledTimes(2); + expect(message.reply).toHaveBeenNthCalledWith(1, {content: 'normal content', files: [{name: 'test.txt', data: 'test.txt' }],}); + }) + }) + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) \ No newline at end of file diff --git a/tests/helpers/importHelper.test.js b/tests/helpers/importHelper.test.js new file mode 100644 index 0000000..d4a0f29 --- /dev/null +++ b/tests/helpers/importHelper.test.js @@ -0,0 +1,100 @@ +const {enums} = require('../../src/enums.js'); +const fetchMock = require('jest-fetch-mock'); + +jest.mock('../../src/helpers/memberHelper.js', () => { + return { + memberHelper: { + addFullMember: jest.fn() + } + } +}) + +fetchMock.enableMocks(); +const {memberHelper} = require("../../src/helpers/memberHelper.js"); +const {importHelper} = require('../../src/helpers/importHelper.js'); + +describe('importHelper', () => { + const authorId = '123'; + const attachmentUrl = 'system.json'; + const mockImportedMember = { + proxy_tags: [{ + prefix: "SP{", + suffix: "}" + }], + display_name: "SomePerson", + avatar_url: 'oya.png', + name: 'somePerson' + } + const mockData = { + members: [mockImportedMember] + }; + const mockAddReturnMember = { + proxy: "SP{text}", + displayname: "SomePerson", + propic: 'oya.png', + name: 'somePerson' + } + const mockAddReturn = { + member: mockAddReturnMember, + errors: [] + } + + beforeEach(() => { + global.fetch = jest.fn(); + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData) + }) + + }) + + describe('pluralKitImport', () => { + + test('if no attachment URL, throws error', () => { + return importHelper.pluralKitImport(authorId).catch((e) => { + expect(e).toEqual(new Error(enums.err.NOT_JSON_FILE)); + }) + }) + + test('if attachment URL, calls fetch and addFullMember and returns value', () => { + memberHelper.addFullMember.mockResolvedValue(mockAddReturn); + return importHelper.pluralKitImport(authorId, attachmentUrl).then((res) => { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(attachmentUrl); + expect(memberHelper.addFullMember).toHaveBeenCalledWith(authorId, mockImportedMember.name, mockImportedMember.display_name, 'SP{text}', mockImportedMember.avatar_url); + expect(res).toEqual(`Successfully added members: ${mockAddReturnMember.name}`) + }) + }) + + test('if addFullMember returns nothing, return correct enum', () => { + memberHelper.addFullMember.mockResolvedValue(); + return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => { + expect(res).toEqual(new AggregateError([], enums.err.NO_MEMBERS_IMPORTED)); + }) + }) + + test('if addFullMember returns nothing and throws error, catch and return error', () => { + memberHelper.addFullMember.mockResolvedValue(new Error('error')); + return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => { + expect(res).toEqual(new AggregateError([new Error('error')], enums.err.NO_MEMBERS_IMPORTED)) + }) + }) + + test('if addFullMember returns member but also contains error, return member and error', () => { + // Arrange + const memberObj = {errors: ['error'], member: mockAddReturnMember}; + memberHelper.addFullMember.mockResolvedValue(memberObj); + // Act + return importHelper.pluralKitImport(authorId, attachmentUrl).catch((res) => { + // Assert + expect(res).toEqual(new AggregateError(['error'], `Successfully added members: ${mockAddReturnMember.name}`)) + }) + }) + + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.clearAllMocks(); + }); +}) \ No newline at end of file diff --git a/tests/helpers/memberHelper.test.js b/tests/helpers/memberHelper.test.js index 3f12fc1..738e286 100644 --- a/tests/helpers/memberHelper.test.js +++ b/tests/helpers/memberHelper.test.js @@ -1,8 +1,5 @@ -const {EmbedBuilder} = require("@fluxerjs/core"); -const {database} = require('../../src/database.js'); const {enums} = require('../../src/enums.js'); -const {EmptyResultError, Op} = require('sequelize'); -const {memberHelper} = require("../../src/helpers/memberHelper.js"); +const {utils} = require("../../src/helpers/utils.js"); jest.mock('@fluxerjs/core', () => jest.fn()); jest.mock('../../src/database.js', () => { @@ -12,18 +9,38 @@ jest.mock('../../src/database.js', () => { create: jest.fn().mockResolvedValue(), update: jest.fn().mockResolvedValue(), destroy: jest.fn().mockResolvedValue(), + findOne: jest.fn().mockResolvedValue(), + findAll: jest.fn().mockResolvedValue(), } } } }); -jest.mock('sequelize', () => jest.fn()); +jest.mock("../../src/helpers/utils.js", () => { + return { + utils: + { + checkImageFormatValidity: jest.fn().mockResolvedValue(), + } + } +}); + +const {Op} = require('sequelize'); + +const {memberHelper} = require("../../src/helpers/memberHelper.js"); +const {database} = require("../../src/database"); describe('MemberHelper', () => { const authorId = "0001"; const authorFull = "author#0001"; const attachmentUrl = "../oya.png"; - const attachmentExpiration = new Date('2026-01-01T00.00.00.0000Z') + const attachmentExpiration = new Date('2026-01-01').toDateString(); + const mockMember = { + name: "somePerson", + displayname: "Some Person", + proxy: "--text", + propic: attachmentUrl + } beforeEach(() => { jest.resetModules(); @@ -34,200 +51,343 @@ describe('MemberHelper', () => { beforeEach(() => { jest.spyOn(memberHelper, 'getMemberCommandInfo').mockResolvedValue("member command info"); - jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue("member info"); - jest.spyOn(memberHelper, 'addNewMember').mockResolvedValue("new member"); - jest.spyOn(memberHelper, 'removeMember').mockResolvedValue("remove member"); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); - jest.spyOn(memberHelper, 'updateName').mockResolvedValue("update name"); - jest.spyOn(memberHelper, 'updateDisplayName').mockResolvedValue("update display name"); - jest.spyOn(memberHelper, 'updateProxy').mockResolvedValue("update proxy"); - jest.spyOn(memberHelper, 'updatePropic').mockResolvedValue("update propic"); - jest.spyOn(memberHelper, 'getProxyByMember').mockResolvedValue("get proxy"); + jest.spyOn(memberHelper, 'memberArgumentHandler').mockResolvedValue("handled argument"); + jest.spyOn(memberHelper, 'memberCommandHandler').mockResolvedValue("called command"); + jest.spyOn(memberHelper, 'sendCurrentValue').mockResolvedValue("current value"); + jest.spyOn(memberHelper, 'sendHelpEnum').mockResolvedValue("help enum") }); test.each([ - [['remove'], 'remove member', 'removeMember', ['remove']], - [['list'], 'all member info', 'getAllMembersInfo', authorFull], - [['somePerson', 'name'], 'update name', 'updateName', ['somePerson', 'name']], - [['somePerson', 'displayname'], 'update display name', 'updateDisplayName', ['somePerson', 'displayname']], - [['somePerson', 'proxy'], 'get proxy', 'getProxyByMember', 'somePerson'], - [['somePerson', 'proxy', 'test'], 'update proxy', 'updateProxy', ['somePerson', 'proxy', 'test']], - [['somePerson'], 'member info', 'getMemberInfo', 'somePerson'], - ])('%s calls %s and returns correct values', async (args, expectedResult, method, passedIn) => { + [['--help']], + [['']], + [[]] + ])('%s calls getMemberCommandInfo and returns expected result', async (args) => { // Act return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { - // Assert - expect(result).toEqual(expectedResult); - expect(memberHelper[method]).toHaveBeenCalledTimes(1); - expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) - }); - }); - - - test.each([ - [['new'], attachmentUrl], - [['new'], null,] - ])('%s returns correct values and calls addNewMember', (args, attachmentUrl) => { - // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl).then((result) => { - // Assert - expect(result).toEqual("new member"); - expect(memberHelper.addNewMember).toHaveBeenCalledTimes(1); - expect(memberHelper.addNewMember).toHaveBeenCalledWith(authorId, args, attachmentUrl); - }); - }) - - test.each([ - [['']], - [['--help'], null,] - ])('%s returns correct values and calls getMemberEmbedInfo', (args) => { - // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl).then((result) => { // Assert expect(result).toEqual("member command info"); expect(memberHelper.getMemberCommandInfo).toHaveBeenCalledTimes(1); expect(memberHelper.getMemberCommandInfo).toHaveBeenCalledWith(); }); - }) + }); - test('["somePerson", "propic"] returns correct values and updatePropic', () => { - // Arrange - const args = ['somePerson', 'propic']; + test.each([ + [[mockMember.name, '--help'], null, null, undefined, true, undefined], + [['new', '--help'], null, null, 'new', true, '--help'], + [['remove', '--help'], null, null, 'remove', true, '--help'], + [['name', '--help'], null, null, 'name', true, '--help'], + [['list', '--help'], null, null, 'list', true, '--help'], + [['name', '--help'], null, null, 'name', true, '--help'], + [['displayname', '--help'], null, null, 'displayname', true, '--help'], + [['proxy', '--help'], null, null, 'proxy', true, '--help'], + [['propic', '--help'], null, null, 'propic', true, '--help'], + [['new'], null, null, 'new', true, undefined], + [['remove'], null, null, 'remove', true, undefined], + [['name'], null, null, 'name', true, undefined], + [['list'], null, null, 'list', false, undefined], + [['displayname'], null, null, 'displayname', true, undefined], + [['proxy'], null, null, 'proxy', true, undefined], + [['propic'], null, null, 'propic', true, undefined], + [[mockMember.name, 'remove'], null, null, 'remove', false, mockMember.name], + [[mockMember.name, 'remove', 'test'], null, null, 'remove', false, mockMember.name], + [[mockMember.name, 'new'], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', mockMember.displayname], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy,mockMember.propic], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new',mockMember.displayname, mockMember.proxy, null], mockMember.propic, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name], + [[mockMember.name, 'name', mockMember.name], null, null, 'name', false, mockMember.name], + [[mockMember.name, 'new', '', mockMember.proxy], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', '', '', mockMember.propic], null, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', '', '', null], mockMember.propic, null, 'new', false, mockMember.name], + [[mockMember.name, 'new', '', '', null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name], + // + [[mockMember.name, 'displayname', mockMember.displayname], null, null, 'displayname', false, mockMember.name], + [[mockMember.name, 'proxy', mockMember.proxy], null, null, 'proxy', false, mockMember.name], + [[mockMember.name, 'propic', mockMember.propic], null, null, 'propic', false, mockMember.name], + [[mockMember.name, 'propic', null], mockMember.propic, null, 'propic', false, mockMember.name], + [[mockMember.name, 'propic', null], mockMember.propic, attachmentExpiration, 'propic', false, mockMember.name], + [['remove', mockMember.name], null, null, 'remove', false, mockMember.name], + [['remove', mockMember.name, 'test'], null, null, 'remove', false, mockMember.name], + [['new', mockMember.name], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, mockMember.displayname], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'new', false, mockMember.name], + [['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name], + [['new',mockMember.name, '', mockMember.proxy], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, '', '', mockMember.propic], null, null, 'new', false, mockMember.name], + [['new', mockMember.name, '', '', null], mockMember.propic, null, 'new', false, mockMember.name], + [['new', mockMember.name, '', '', null], mockMember.propic, attachmentExpiration, 'new', false, mockMember.name], + // + [['name', mockMember.name, mockMember.name], null, null, 'name', false, mockMember.name], + [['displayname', mockMember.name, mockMember.name, mockMember.displayname], null, null, 'displayname', false, mockMember.name], + [['proxy', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'proxy', false, mockMember.name], + [['propic', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'propic', false, mockMember.name], + [['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'propic', false, mockMember.name], + [['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'propic', false, mockMember.name] + ])('%s args with attachmentURL %s and attachment expiration %s calls memberCommandHandler with correct values', (args, attachmentUrl, attachmentExpiration, command, isHelp, memberName) => { + console.log(args, command, isHelp) // Act return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).then((result) => { // Assert - expect(result).toEqual("update propic"); - expect(memberHelper['updatePropic']).toHaveBeenCalledTimes(1); - expect(memberHelper['updatePropic']).toHaveBeenCalledWith(authorId, args, attachmentUrl, attachmentExpiration) + expect(result).toEqual("handled argument"); + expect(memberHelper.memberArgumentHandler).toHaveBeenCalledTimes(1); + expect(memberHelper.memberArgumentHandler).toHaveBeenCalledWith(authorId, authorFull, isHelp, command, memberName, args, attachmentUrl, attachmentExpiration); + }); + }) + }); + + describe('memberArgumentHandler', () => { + beforeEach(() => { + jest.spyOn(memberHelper, 'memberCommandHandler').mockResolvedValue("handled command"); + jest.spyOn(memberHelper, 'getAllMembersInfo').mockResolvedValue("all member info"); + jest.spyOn(memberHelper, 'sendCurrentValue').mockResolvedValue("current value"); + jest.spyOn(memberHelper, 'sendHelpEnum').mockReturnValue("help enum"); + }) + + test('when all values are null should return command not recognized enum', () => { + // Arrange + return memberHelper.memberArgumentHandler(authorId, authorFull, false, null, null, []).catch((result) => { + // Assert + expect(result).toEqual(new Error(enums.err.COMMAND_NOT_RECOGNIZED)); }); }) test.each([ - [['name'], enums.help.NAME], - [['displayname'], enums.help.DISPLAY_NAME], - [['proxy'], enums.help.PROXY], - [['propic'], enums.help.PROPIC], - [['list', '--help'], enums.help.LIST], - [[''], enums.help.MEMBER], - ])('%s returns correct enums', async (args, expectedResult) => { + ['new'], + ['remove'], + ['name'], + ['displayname'], + ['proxy'], + ['propic'], + ])('when %s is present but other values are null, should return no member enum', (command) => { // Arrange - const authorId = '1'; - const authorFull = 'somePerson#0001'; + return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, null, []).catch((result) => { + // Assert + expect(result).toEqual(new Error(enums.err.NO_MEMBER)); + }); + }) + + + test.each([ + ['new'], + ['remove'], + ['name'], + ['list'], + ['displayname'], + ['proxy'], + ['propic'], + ])('%s calls sendHelpEnum', (command) => { + // Arrange + return memberHelper.memberArgumentHandler(authorId, authorFull, true, command, mockMember.name, []).then((result) => { + // Assert + expect(result).toEqual("help enum"); + expect(memberHelper.sendHelpEnum).toHaveBeenCalledTimes(1); + expect(memberHelper.sendHelpEnum).toHaveBeenCalledWith(command); + }); + }) + + test('list should call getAllMembersInfo', () => { + // Arrange + return memberHelper.memberArgumentHandler(authorId, authorFull, false, 'list', mockMember.name, []).then((result) => { + // Assert + expect(result).toEqual("all member info"); + expect(memberHelper.getAllMembersInfo).toHaveBeenCalledTimes(1); + expect(memberHelper.getAllMembersInfo).toHaveBeenCalledWith(authorId, authorFull); + }); + }) + + test.each([ + [[mockMember.name, 'remove'], null, null, 'remove'], + [[mockMember.name, 'remove', 'test'], null, null, 'remove'], + [[mockMember.name, 'new'], null, null, 'new'], + [[mockMember.name, 'new', mockMember.displayname], null, null, 'new'], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy], null, null, 'new'], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy,mockMember.propic], null, null, 'new'], + [[mockMember.name, 'new',mockMember.displayname, mockMember.proxy, null], mockMember.propic, null, 'new'], + [[mockMember.name, 'new', mockMember.displayname, mockMember.proxy, null], mockMember.propic, attachmentExpiration, 'new'], + [[mockMember.name, 'name', mockMember.name], null, null, 'name'], + [[mockMember.name, 'displayname', mockMember.displayname], null, null, 'displayname'], + [[mockMember.name, 'new', mockMember.displayname], null, null, 'new'], + [[mockMember.name, 'new', '', mockMember.proxy], null, null, 'new'], + [[mockMember.name, 'new', '', '', mockMember.propic], null, null, 'new'], + [[mockMember.name, 'new', '', '', undefined], mockMember.propic, null, 'new'], + [[mockMember.name, 'new', '', '', undefined], mockMember.propic, attachmentExpiration, 'new'], + [[mockMember.name, 'new', '', ''], mockMember.propic, null, 'new'], + [[mockMember.name, 'new', '', ''], mockMember.propic, attachmentExpiration, 'new'], + [[mockMember.name, 'proxy', mockMember.proxy], null, null, 'proxy'], + [[mockMember.name, 'propic', mockMember.propic], null, null, 'propic'], + [[mockMember.name, 'propic', undefined], mockMember.propic, null, 'propic'], + [[mockMember.name, 'propic', undefined], mockMember.propic, attachmentExpiration, 'propic'], + [[mockMember.name, 'propic'], mockMember.propic, null, 'propic'], + [[mockMember.name, 'propic'], mockMember.propic, attachmentExpiration, 'propic'], + [['remove', mockMember.name], null, null, 'remove'], + [['remove', mockMember.name, 'test'], null, null, 'remove'], + [['new', mockMember.name], null, null, 'new'], + [['new', mockMember.name, mockMember.displayname], null, null, 'new'], + [['new', mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'new'], + [['new', mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'new'], + [['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'new'], + [['new', mockMember.name, undefined, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'new'], + [['new', mockMember.name, '', mockMember.proxy], null, null, 'new'], + [['new', mockMember.name, '', '', mockMember.propic], null, null, 'new'], + [['new', mockMember.name, '', '', undefined], mockMember.propic, null, 'new'], + [['new', mockMember.name, '', '', undefined], mockMember.propic, attachmentExpiration, 'new'], + [['new', mockMember.name, '', ''], mockMember.propic, null, 'new'], + [['new', mockMember.name, '', ''], mockMember.propic, attachmentExpiration, 'new'], + [['name', mockMember.name, mockMember.name], null, null, 'name'], + [['displayname', mockMember.name, mockMember.name, mockMember.displayname], null, null, 'displayname'], + [['proxy', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy], null, null, 'proxy'], + [['propic', mockMember.name, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic], null, null, 'propic'], + [['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, null, 'propic'], + [['propic', mockMember.name, undefined, mockMember.name, mockMember.displayname, mockMember.proxy, undefined], mockMember.propic, attachmentExpiration, 'propic'] + ])('%s args with attachmentURL %s and attachment expiration %s calls memberCommandHandler', (args, attachmentUrl, attachmentExpiration, command) => { + // Arrange + let values = args.slice(2); + + return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, args, attachmentUrl, attachmentExpiration).then((result) => { + // Assert + expect(result).toEqual("handled command"); + expect(memberHelper.memberCommandHandler).toHaveBeenCalledTimes(1); + expect(memberHelper.memberCommandHandler).toHaveBeenCalledWith(authorId, command, mockMember.name, values, attachmentUrl, attachmentExpiration); + }); + }) + + test.each([ + [null], + ['name'], + ['displayname'], + ['proxy'], + ['propic'], + ])('%s calls sendCurrentValue', (command) => { + return memberHelper.memberArgumentHandler(authorId, authorFull, false, command, mockMember.name, []).then((result) => { + // Assert + expect(result).toEqual("current value"); + expect(memberHelper.sendCurrentValue).toHaveBeenCalledTimes(1); + expect(memberHelper.sendCurrentValue).toHaveBeenCalledWith(authorId,mockMember.name, command); + }); + }) + + + }); + + describe('sendCurrentValue', () => { + + test.each([ + ['name', `The name of ${mockMember.name} is \"${mockMember.name}\" but you probably already knew that!`], + ['displayname', `The display name for ${mockMember.name} is \"${mockMember.displayname}\".`], + ['proxy', `The proxy for ${mockMember.name} is \"${mockMember.proxy}\".`], + ['propic', `The profile picture for ${mockMember.name} is \"${mockMember.propic}\".`], + ])('%s calls getMemberByName and returns value', (command, expected) => { + // Arrange + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember); // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args).then((result) => { - - expect(result).toEqual(expectedResult); + return memberHelper.sendCurrentValue(authorId, mockMember.name, command).then((result) => { + // Assert + expect(result).toEqual(expected); + expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); + expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId,mockMember.name); }); - }); + }) - describe('errors', () => { - beforeEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => { throw new Error('member info error')}); - jest.spyOn(memberHelper, 'addNewMember').mockImplementation(() => { throw new Error('new member error')}); - jest.spyOn(memberHelper, 'removeMember').mockImplementation(() => { throw new Error('remove member error')}); - jest.spyOn(memberHelper, 'getAllMembersInfo').mockImplementation(() => { throw new Error('all member info error')}); - jest.spyOn(memberHelper, 'updateName').mockImplementation(() => { throw new Error('update name error')}); - jest.spyOn(memberHelper, 'updateDisplayName').mockImplementation(() => { throw new Error('update display name error')}); - jest.spyOn(memberHelper, 'updateProxy').mockImplementation(() => { throw new Error('update proxy error')}); - jest.spyOn(memberHelper, 'updatePropic').mockImplementation(() => { throw new Error('update propic error')}); - jest.spyOn(memberHelper, 'getProxyByMember').mockImplementation(() => { throw new Error('get proxy error')}); - }) - test.each([ - [['remove'], 'remove member error', 'removeMember', ['remove']], - [['list'], 'all member info error', 'getAllMembersInfo', authorFull], - [['somePerson', 'name'], 'update name error', 'updateName', ['somePerson', 'name']], - [['somePerson', 'displayname'], 'update display name error', 'updateDisplayName', ['somePerson', 'displayname']], - [['somePerson', 'proxy'], 'get proxy error', 'getProxyByMember', 'somePerson'], - [['somePerson', 'proxy', 'test'], 'update proxy error', 'updateProxy', ['somePerson', 'proxy', 'test']], - [['somePerson'], 'member info error', 'getMemberInfo', 'somePerson'], - ])('%s calls methods and throws correct values', async (args, expectedError, method, passedIn) => { - // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args).catch((result) => { - // Assert - expect(result).toEqual(new Error(expectedError)); - expect(memberHelper[method]).toHaveBeenCalledTimes(1); - expect(memberHelper[method]).toHaveBeenCalledWith(authorId, passedIn) - }); + test('returns error if no member found', () => { + // Arrange + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(null); + // Act + return memberHelper.sendCurrentValue(authorId, mockMember.name, 'name').catch((result) => { + // Assert + expect(result).toEqual(new Error(enums.err.NO_MEMBER)); + expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); + expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId,mockMember.name); }); + }) - test.each([ - [['new'], attachmentUrl], - [['new'], null,] - ])('%s throws correct error when addNewMember returns error', (args, attachmentUrl) => { - // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl).catch((result) => { - // Assert - expect(result).toEqual(new Error("new member error")); - expect(memberHelper.addNewMember).toHaveBeenCalledTimes(1); - expect(memberHelper.addNewMember).toHaveBeenCalledWith(authorId, args, attachmentUrl); - }); - }) + test('calls getMemberInfo with member if no command present', () => { + // Arrange + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(mockMember); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue('member info'); + // Act + return memberHelper.sendCurrentValue(authorId, mockMember.name, null).then((result) => { + // Assert + expect(result).toEqual('member info'); + expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1); + expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(mockMember); + }); + }) - test('["somePerson", "propic"] throws correct error when updatePropic returns error', () => { - // Arrange - const args = ['somePerson', 'propic']; - // Act - return memberHelper.parseMemberCommand(authorId, authorFull, args, attachmentUrl, attachmentExpiration).catch((result) => { - // Assert - expect(result).toEqual(new Error("update propic error")); - expect(memberHelper['updatePropic']).toHaveBeenCalledTimes(1); - expect(memberHelper['updatePropic']).toHaveBeenCalledWith(authorId, args, attachmentUrl, attachmentExpiration) - }); - }) + test.each([ + ['displayname', `Display name ${enums.err.NO_VALUE}`], + ['proxy', `Proxy ${enums.err.NO_VALUE}`], + ['propic', `Propic ${enums.err.NO_VALUE}`], + ])('returns null message if no member found', (command, expected) => { + // Arrange + const empty = {name: mockMember.name, displayname: null, proxy: null, propic: null} + jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(empty); + // Act + return memberHelper.sendCurrentValue(authorId, mockMember.name, command).then((result) => { + // Assert + expect(result).toEqual(expected); + expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); + expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId,mockMember.name); + }); }) }) describe('addNewMember', () => { - - test('returns help if --help passed in', async() => { + test('calls addFullMember with correct arguments', async() => { // Arrange - const args = ['new', '--help']; - const expected = enums.help.NEW; + const args = [mockMember.displayname, mockMember.proxy, mockMember.propic]; + jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(mockMember); + jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue(); + // Act + return memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration).then(() => { + expect(memberHelper.addFullMember).toHaveBeenCalledTimes(1); + expect(memberHelper.addFullMember).toHaveBeenCalledWith(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic, attachmentExpiration); + }) + }) + + test('calls getMemberInfo when successful and returns result', async () => { + // Arrange + const args = [mockMember.displayname, mockMember.proxy, mockMember.propic]; + const fullMemberResponse = {member: mockMember, errors: []} + const expected = {embed: mockMember, errors: [], success: `${mockMember.name} has been added successfully.`}; + jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(fullMemberResponse); + jest.spyOn(memberHelper, 'getMemberInfo').mockReturnValue(mockMember); //Act - return memberHelper.addNewMember(authorId, args).then((result) => { + return memberHelper.addNewMember(authorId, mockMember.name, args, attachmentUrl, attachmentExpiration).then((result) => { // Assert expect(result).toEqual(expected); - }) - }) - - test('calls getMemberInfo when successful and returns result', async () => { - // Arrange - const args = ['new', 'some person']; - const memberObject = { name: args[1] } - jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); - jest.spyOn(memberHelper, 'getMemberInfo').mockResolvedValue(memberObject); - //Act - return memberHelper.addNewMember(authorId, args).then((result) => { - // Assert - expect(result).toEqual(memberObject); expect(memberHelper.getMemberInfo).toHaveBeenCalledTimes(1); - expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(authorId, args[1]); + expect(memberHelper.getMemberInfo).toHaveBeenCalledWith(mockMember); }) }) - test('throws expected error when getMemberInfo throws error', async () => { + test('throws expected error when getMemberInfo throws error', async () => { // Arrange - const args = ['new', 'some person']; - const memberObject = { name: args[1] } + const args = []; + const memberObject = {name: args[1]} jest.spyOn(memberHelper, 'addFullMember').mockResolvedValue(memberObject); - jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => { throw new Error('getMemberInfo error') }); + jest.spyOn(memberHelper, 'getMemberInfo').mockImplementation(() => { + throw new Error('getMemberInfo error') + }); //Act - return memberHelper.addNewMember(authorId, args).catch((result) => { + return memberHelper.addNewMember(authorId, mockMember.name, args).catch((result) => { // Assert expect(result).toEqual(new Error('getMemberInfo error')); }) }) - test('throws expected error when addFullMember throws error', async () => { + test('throws expected error when addFullMember throws error', async () => { // Arrange - const args = ['new', 'somePerson']; + const args = []; const expected = 'add full member error'; - jest.spyOn(memberHelper, 'addFullMember').mockImplementation(() => { throw new Error(expected)}); + jest.spyOn(memberHelper, 'addFullMember').mockImplementation(() => { + throw new Error(expected) + }); //Act - return memberHelper.addNewMember(authorId, args).catch((result) => { + return memberHelper.addNewMember(authorId, mockMember.name, args).catch((result) => { // Assert expect(result).toEqual(new Error(expected)); }) @@ -236,105 +396,37 @@ describe('MemberHelper', () => { describe('updateName', () => { - test('sends help message when --help parameter passed in', async () => { - // Arrange - const args = ['somePerson', 'name', '--help']; - - // Act - return memberHelper.updateName(authorId, args).then((result) => { - // Assert - expect(result).toEqual(enums.help.NAME); - }) - }) - - test('Sends string when no name', async () => { - // Arrange - const args = ['somePerson', 'name']; - const expected = `The name for ${args[0]} is ${args[0]}, but you probably knew that!`; - - // Act - return memberHelper.updateName(authorId, args).then((result) => { - expect(result).toEqual(expected); - }) - }) - - test('throws error when name is empty', async () => { - // Arrange - const args = ['somePerson', 'name', " "]; - - // Act - return memberHelper.updateName(authorId, args).catch((result) => { - // Assert - expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); - }) - }) - - test('throws error when updateMemberField returns error', async () => { - // Arrange - const expected = 'update error'; - const args = ['somePerson', "name", "someNewPerson"]; - jest.spyOn(memberHelper, 'updateMemberField').mockImplementation(() => { - throw new Error(expected) - }); - // Act - return memberHelper.updateName(authorId, args).catch((result) => { - // Assert - expect(result).toEqual(new Error(expected)); - }) - }); - - test('sends string when updateMemberField returns successfully', async () => { - // Arrange - const args = ['somePerson', 'name', 'someNewPerson']; + test('call updateMemberField with correct arguments when displayname passed in correctly and returns string', async () => { + // Arrange; jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); - // Act - return memberHelper.updateName(authorId, args).then((result) => { + return memberHelper.updateName(authorId, mockMember.name, " somePerson ").then((result) => { // Assert expect(result).toEqual("Updated"); + expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1); + expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "name", "somePerson"); + }) + }) + + test('throws error when name is blank', async () => { + // Arrange; + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); + // Act + return memberHelper.updateName(authorId, mockMember.name, " ").catch((result) => { + // Assert + expect(result).toEqual(new RangeError("Name " + enums.err.NO_VALUE)); + expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); }) }) }) describe('updateDisplayName', () => { - test('sends help message when --help parameter passed in', async () => { + test('throws error when displayname is blank', async () => { // Arrange - const args = ['somePerson', 'displayname', '--help']; jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); // Act - return memberHelper.updateDisplayName(authorId, args).then((result) => { - // Assert - expect(result).toEqual(enums.help.DISPLAY_NAME); - expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); - }) - }) - - test('Sends string of current displayname when it exists and no displayname passed in', async () => { - // Arrange - const args = ['somePerson', 'displayname']; - const displayname = "Some Person"; - const member = { - displayname: displayname, - } - jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); - jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); - // Act - return memberHelper.updateDisplayName(authorId, args).then((result) => { - // Assert - expect(result).toEqual(`Display name for ${args[0]} is: "${member.displayname}".`); - expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); - }) - }) - - test('Sends error when no displayname passed in', async () => { - // Arrange - const args = ['somePerson', 'displayname']; - const member = {} - jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); - jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); - // Act - return memberHelper.updateDisplayName(authorId, args).catch((result) => { + return memberHelper.updateDisplayName(authorId, mockMember.name, mockMember.displayname).catch((result) => { // Assert expect(result).toEqual(new Error(`Display name ${enums.err.NO_VALUE}`)); expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); @@ -343,86 +435,120 @@ describe('MemberHelper', () => { test('Sends error when display name is too long', async () => { // Arrange - const displayname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - const args = ['somePerson', 'displayname', displayname]; - const member = {}; - jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); + const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); // Act - return memberHelper.updateDisplayName(authorId, args).catch((result) => { + return memberHelper.updateDisplayName(authorId, mockMember.name, tooLongDisplayName).catch((result) => { // Assert expect(result).toEqual(new RangeError(enums.err.DISPLAY_NAME_TOO_LONG)); expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); }) }) - test('Sends error when display name is blank', async () => { + test('call updateMemberField with correct arguments when displayname passed in correctly and returns string', async () => { // Arrange - const displayname = " "; - const args = ['somePerson', 'displayname', displayname]; - const member = {}; - jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(member); - jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(); + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); // Act - return memberHelper.updateDisplayName(authorId, args).catch((result) => { + return memberHelper.updateDisplayName(authorId, mockMember.name, " Some Person ").then((result) => { // Assert - expect(result).toEqual(new Error(`Display name ${enums.err.NO_VALUE}`)); - expect(memberHelper.updateMemberField).not.toHaveBeenCalled(); - }) - }) - - test('call updateMemberField with correct arguments when displayname passed in correctly', async() => { - // Arrange - const args = ['somePerson', 'displayname', "Some Person"]; - const member = {}; - jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue(member); - // Act - return memberHelper.updateDisplayName(authorId, args).then((result) => { - // Assert - expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, args); + expect(result).toEqual("Updated"); + expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "displayname", mockMember.displayname); expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1); }) }) }) + describe('updateProxy', () => { + test('calls checkIfProxyExists and updateMemberField and returns string', async() => { + // Arrange + jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(); + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); + // Act + return memberHelper.updateProxy(authorId, mockMember.name, "--text").then((result) => { + expect(result).toEqual("Updated"); + expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1); + expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy); + expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1); + expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "proxy", mockMember.proxy); + }); + }) + }) + + describe('updatePropic', () => { + test.each([ + [null, attachmentUrl, null, attachmentUrl], + [mockMember.propic, null, null, mockMember.propic], + [mockMember.propic, attachmentUrl, null, attachmentUrl], + [null, attachmentUrl, attachmentExpiration, attachmentUrl] + ])('calls checkImageFormatValidity and updateMemberField and returns string', async(imgUrl, attachmentUrl, attachmentExpiration, expected) => { + // Arrange + + jest.spyOn(memberHelper, 'updateMemberField').mockResolvedValue("Updated"); + // Act + return memberHelper.updatePropic(authorId, mockMember.name, imgUrl, attachmentUrl, attachmentExpiration).then((result) => { + expect(result).toEqual("Updated"); + expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1); + expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(expected); + expect(memberHelper.updateMemberField).toHaveBeenCalledTimes(1); + expect(memberHelper.updateMemberField).toHaveBeenCalledWith(authorId, mockMember.name, "propic", expected, attachmentExpiration); + }); + }) + }) + describe('addFullMember', () => { - const memberName = "somePerson"; - const displayName = "Some Person"; - const proxy = "--text"; - const propic = "oya.png"; + const { database} = require('../../src/database.js'); beforeEach(() => { - database.members.create = jest.fn().mockResolvedValue(); jest.spyOn(memberHelper, 'getMemberByName').mockResolvedValue(); }) - test('calls getMemberByName', async() => { + test('calls getMemberByName', async () => { // Act - return await memberHelper.addFullMember(authorId, memberName).then(() => { + return await memberHelper.addFullMember(authorId, mockMember.name).then(() => { // Assert - expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, memberName); + expect(memberHelper.getMemberByName).toHaveBeenCalledWith(authorId, mockMember.name); expect(memberHelper.getMemberByName).toHaveBeenCalledTimes(1); }) }) - test('if getMemberByName returns member, throw error', async() => { - memberHelper.getMemberByName.mockResolvedValue({name: memberName}); + test('if getMemberByName returns member, throw error', async () => { + memberHelper.getMemberByName.mockResolvedValue({name: mockMember.name}); // Act - return await memberHelper.addFullMember(authorId, memberName).catch((e) => { + return await memberHelper.addFullMember(authorId, mockMember.name).catch((e) => { // Assert - expect(e).toEqual(new Error(`Can't add ${memberName}. ${enums.err.MEMBER_EXISTS}`)) + expect(e).toEqual(new Error(`Can't add ${mockMember.name}. ${enums.err.MEMBER_EXISTS}`)) expect(database.members.create).not.toHaveBeenCalled(); }) }) - test('if displayname is over 32 characters, call database.member.create with null value', async() => { + + test('if name is not filled out, throw error', async () => { + // Act + return await memberHelper.addFullMember(authorId, " ").catch((e) => { + // Assert + expect(e).toEqual(new Error(`Name ${enums.err.NO_VALUE}. ${enums.err.NAME_REQUIRED}`)) + expect(database.members.create).not.toHaveBeenCalled(); + }) + }) + + test('if displayname is over 32 characters, call database.member.create with null value', async () => { // Arrange - const displayName = "Some person with a very very very long name that can't be processed"; - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + memberHelper.getMemberByName.mockResolvedValue(); + const tooLongDisplayName = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: null, + proxy: null, + propic: null + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); - const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set displayname to \"${displayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`]} + const expectedReturn = { + member: expectedMemberArgs, + errors: [`Tried to set displayname to \"${tooLongDisplayName}\". ${enums.err.DISPLAY_NAME_TOO_LONG}. ${enums.err.SET_TO_NULL}`] + } // Act - return await memberHelper.addFullMember(authorId, memberName, displayName, null, null).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, tooLongDisplayName, null, null).then((res) => { // Assert expect(res).toEqual(expectedReturn); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); @@ -430,33 +556,50 @@ describe('MemberHelper', () => { }) }) - test('if proxy, call checkIfProxyExists', async() => { + test('if proxy, call checkIfProxyExists', async () => { // Arrange jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(); - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: proxy, propic: null} + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: null, + proxy: mockMember.proxy, + propic: null + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); const expectedReturn = {member: expectedMemberArgs, errors: []} // Act - return await memberHelper.addFullMember(authorId, memberName, null, proxy).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy).then((res) => { // Assert expect(res).toEqual(expectedReturn); - expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, proxy); + expect(memberHelper.checkIfProxyExists).toHaveBeenCalledWith(authorId, mockMember.proxy); expect(memberHelper.checkIfProxyExists).toHaveBeenCalledTimes(1); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(database.members.create).toHaveBeenCalledTimes(1); }) }) - test('if checkProxyExists throws error, call database.member.create with null value', async() => { + test('if checkProxyExists throws error, call database.member.create with null value', async () => { // Arrange - jest.spyOn(memberHelper, 'checkIfProxyExists').mockImplementation(() => {throw new Error('error')}); - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + jest.spyOn(memberHelper, 'checkIfProxyExists').mockImplementation(() => { + throw new Error('error') + }); + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: null, + proxy: null, + propic: null + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); - const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set proxy to \"${proxy}\". error. ${enums.err.SET_TO_NULL}`]} + const expectedReturn = { + member: expectedMemberArgs, + errors: [`Tried to set proxy to \"${mockMember.proxy}\". error. ${enums.err.SET_TO_NULL}`] + } // Act - return await memberHelper.addFullMember(authorId, memberName, null, proxy, null).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, null, mockMember.proxy, null).then((res) => { // Assert expect(res).toEqual(expectedReturn); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); @@ -464,31 +607,45 @@ describe('MemberHelper', () => { }) }) - test('if propic, call checkImageFormatValidity', async() => { + test('if propic, call checkImageFormatValidity', async () => { // Arrange - jest.spyOn(memberHelper, 'checkImageFormatValidity').mockResolvedValue(); - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: propic} + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: null, + proxy: null, + propic: mockMember.propic + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); const expectedReturn = {member: expectedMemberArgs, errors: []} // Act - return await memberHelper.addFullMember(authorId, memberName, null, null, propic).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic).then((res) => { // Assert expect(res).toEqual(expectedReturn); - expect(memberHelper.checkImageFormatValidity).toHaveBeenCalledWith(propic); - expect(memberHelper.checkImageFormatValidity).toHaveBeenCalledTimes(1); + expect(utils.checkImageFormatValidity).toHaveBeenCalledWith(mockMember.propic); + expect(utils.checkImageFormatValidity).toHaveBeenCalledTimes(1); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); expect(database.members.create).toHaveBeenCalledTimes(1); }) }) - test('if checkImageFormatValidity throws error, call database.member.create with null value', async() => { + test('if checkImageFormatValidity throws error, call database.member.create with null value', async () => { // Arrange - jest.spyOn(memberHelper, 'checkImageFormatValidity').mockImplementation(() => {throw new Error('error')}); - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: null, proxy: null, propic: null} + utils.checkImageFormatValidity = jest.fn().mockImplementation(() => {throw new Error("error")}) + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: null, + proxy: null, + propic: null + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); - const expectedReturn = {member: expectedMemberArgs, errors: [`Tried to set profile picture to \"${propic}\". error. ${enums.err.SET_TO_NULL}`]} + const expectedReturn = { + member: expectedMemberArgs, + errors: [`Tried to set profile picture to \"${mockMember.propic}\". error. ${enums.err.SET_TO_NULL}`] + } // Act - return await memberHelper.addFullMember(authorId, memberName, null, null, propic).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, null, null, mockMember.propic).then((res) => { // Assert expect(res).toEqual(expectedReturn); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); @@ -496,16 +653,21 @@ describe('MemberHelper', () => { }) }) - test('if all values are valid, call database.members.create', async() => { + test('if all values are valid, call database.members.create', async () => { // Arrange jest.spyOn(memberHelper, 'checkIfProxyExists').mockResolvedValue(); - jest.spyOn(memberHelper, 'checkImageFormatValidity').mockResolvedValue(); - const expectedMemberArgs = {name: memberName, userid: authorId, displayname: displayName, proxy: proxy, propic: propic} + const expectedMemberArgs = { + name: mockMember.name, + userid: authorId, + displayname: mockMember.displayname, + proxy: mockMember.proxy, + propic: mockMember.propic + } database.members.create = jest.fn().mockResolvedValue(expectedMemberArgs); + utils.checkImageFormatValidity = jest.fn().mockResolvedValue(); const expectedReturn = {member: expectedMemberArgs, errors: []} // Act - // Act - return await memberHelper.addFullMember(authorId, memberName, displayName, proxy, propic).then((res) => { + return await memberHelper.addFullMember(authorId, mockMember.name, mockMember.displayname, mockMember.proxy, mockMember.propic).then((res) => { // Assert expect(res).toEqual(expectedReturn); expect(database.members.create).toHaveBeenCalledWith(expectedMemberArgs); @@ -515,6 +677,99 @@ describe('MemberHelper', () => { }) + describe('updateMemberField', () => { + const {database} = require('../../src/database.js'); + beforeEach(() => { + jest.spyOn(memberHelper, "setExpirationWarning").mockReturnValue(' warning'); + database.members = { + update: jest.fn().mockResolvedValue([1]) + }; + }) + + test('calls setExpirationWarning if attachmentExpiration', async () => { + return memberHelper.updateMemberField(authorId, mockMember.name, "propic", mockMember.propic, attachmentExpiration).then((res) => { + expect(memberHelper.setExpirationWarning).toHaveBeenCalledTimes(1); + expect(memberHelper.setExpirationWarning).toHaveBeenCalledWith(mockMember.propic); + }) + }) + + test.each([ + ['name', mockMember.name, null, `Updated name for ${mockMember.name} to ${mockMember.name}`], + ['displayname', mockMember.displayname, null, `Updated name for ${mockMember.name} to ${mockMember.displayname}`], + ['proxy', mockMember.proxy, null, `Updated name for ${mockMember.name} to ${mockMember.proxy}`], + ['propic', mockMember.propic, null, `Updated name for ${mockMember.name} to ${mockMember.propic}`], + ['propic', mockMember.propic, attachmentExpiration, `Updated name for ${mockMember.name} to ${mockMember.propic} warning}`] + ])('calls database.members.update with correct column and value and return string', async (columnName, value, attachmentExpiration) => { + // Arrange + return memberHelper.updateMemberField(authorId, mockMember.name, columnName, value, attachmentExpiration).then((res) => { + // Act + expect(database.members.update).toHaveBeenCalledTimes(1); + expect(database.members.update).toHaveBeenCalledWith({[columnName]: value}, { + where: { + name: {[Op.iLike]: mockMember.name}, + userid: authorId + } + }) + }) + }) + + test('if database.members.update returns 0 rows changed, throw error', () => { + // Arrange + database.members = { + update: jest.fn().mockResolvedValue([0]) + }; + // Act + return memberHelper.updateMemberField(authorId, mockMember.name, "displayname", mockMember.displayname).catch((res) => { + expect(res).toEqual(new Error(`Can't update ${mockMember.name}. ${enums.err.NO_MEMBER}.`)) + }) + }) + }) + + describe('checkIfProxyExists', () => { + + beforeEach(() => { + jest.spyOn(memberHelper, "getMembersByAuthor").mockResolvedValue([mockMember]); + }) + + test.each([ + ['!text'], + ['! text'], + ['⭐text'], + ['⭐ text'], + ['⭐ text ⭐'], + ['--text--'], + ['!text ?'], + ['SP: text'], + ['text --SP'], + ])('%s should call getMembersByAuthor and return false', async (proxy) => { + return memberHelper.checkIfProxyExists(authorId, proxy).then((res) => { + expect(res).toEqual(false) + expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1); + expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId); + }) + }) + + test.each([ + ['--', enums.err.NO_TEXT_FOR_PROXY, false], + [' ', enums.err.NO_TEXT_FOR_PROXY, false], + ['text', enums.err.NO_PROXY_WRAPPER, false], + ['--text', enums.err.PROXY_EXISTS, true] + ])('%s returns correct error and calls getMembersByAuthor if appropriate', async (proxy, error, shouldCall) => { + return memberHelper.checkIfProxyExists(authorId, proxy).catch((res) => { + expect(res).toEqual(new Error(error)) + if (shouldCall) { + expect(memberHelper.getMembersByAuthor).toHaveBeenCalledTimes(1); + expect(memberHelper.getMembersByAuthor).toHaveBeenCalledWith(authorId); + } + else { + expect(memberHelper.getMembersByAuthor).not.toHaveBeenCalled(); + } + }) + }) + + + }) + afterEach(() => { // restore the spy created with spyOn jest.restoreAllMocks(); diff --git a/tests/helpers/messageHelper.test.js b/tests/helpers/messageHelper.test.js index b0e9488..7163e21 100644 --- a/tests/helpers/messageHelper.test.js +++ b/tests/helpers/messageHelper.test.js @@ -1,11 +1,6 @@ const env = require('dotenv'); env.config(); -const {memberHelper} = require("../../src/helpers/memberHelper.js"); -const {Message} = require("@fluxerjs/core"); -const {fs} = require('fs'); -const {enums} = require('../../src/enums'); -const {tmp, setGracefulCleanup} = require('tmp'); jest.mock('../../src/helpers/memberHelper.js', () => { return {memberHelper: { @@ -13,10 +8,7 @@ jest.mock('../../src/helpers/memberHelper.js', () => { }} }) -jest.mock('tmp'); -jest.mock('fs'); -jest.mock('@fluxerjs/core'); - +const {memberHelper} = require("../../src/helpers/memberHelper.js"); const {messageHelper} = require("../../src/helpers/messageHelper.js"); describe('messageHelper', () => { diff --git a/tests/helpers/utils.test.js b/tests/helpers/utils.test.js new file mode 100644 index 0000000..5414193 --- /dev/null +++ b/tests/helpers/utils.test.js @@ -0,0 +1,19 @@ +const {enums} = require("../../src/enums"); + +const fetchMock = require('jest-fetch-mock'); +fetchMock.enableMocks(); + +const {utils} = require("../../src/helpers/utils.js"); + +describe('utils', () => { + + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }) + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); +}) \ No newline at end of file