├── .github ├── dependabot.yml └── workflows │ ├── create-release.yml │ ├── npm-deploy.yml │ └── run-test.yml ├── .gitignore ├── .jsdoc.conf ├── LICENSE ├── README.md ├── VERSION ├── doc └── zammad_objects.md ├── index.js ├── package-lock.json ├── package.json ├── src ├── ApiError.js ├── Endpoints.js ├── Ticket.js ├── TicketArticle.js ├── TicketPriority.js ├── TicketState.js ├── User.js ├── Utility.js └── ZammadApi.js └── test ├── Ticket.test.js ├── TicketArticle.test.js ├── TicketPriority.test.js ├── TicketState.test.js ├── User.test.js └── utility ├── DataSeeder.js └── DummyEndpointProvider.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | target-branch: "dev" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: "Version Number of Release (Format x.y.z)" 7 | required: true 8 | jobs: 9 | create_release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check version number validity 13 | id: check-version-number 14 | run: | 15 | if [[ ! "${{ github.event.inputs.version }}" =~ ^([0-9]+)(\.)([0-9]+)(\.)([0-9]+)$ ]]; then 16 | echo "Given version number is not valid." 17 | echo "Version number is supposed to be x.y.z" 18 | exit 1; 19 | fi 20 | - uses: actions/checkout@v2 21 | with: 22 | ref: 'dev' 23 | persist-credentials: false #credentials should not be saved in order to use custom PAT 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: '14' 27 | - name: Create new release brach 28 | id: create-release-branch 29 | run: | 30 | git branch "release-${{ github.event.inputs.version }}" 31 | git checkout "release-${{ github.event.inputs.version }}" 32 | - name: New NPM version and version file 33 | run: | 34 | npm --no-git-tag-version version "${{ github.event.inputs.version }}" 35 | echo "${{ github.event.inputs.version }}" > VERSION 36 | - name: Git commit and tag 37 | run: | 38 | git config user.name "Exanion Bot" 39 | git config user.email travis-builds@exanion.de 40 | git add VERSION 41 | git add package*.json 42 | git commit -m "Created release branch for version ${{ github.event.inputs.version }}" 43 | git tag "${{ github.event.inputs.version }}" 44 | - name: Git push 45 | run: | 46 | git push --tags "https://${{ secrets.GH_PAT }}@github.com/${{ github.repository }}.git" "release-${{ github.event.inputs.version }}" -------------------------------------------------------------------------------- /.github/workflows/npm-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to NPM 2 | on: 3 | push: 4 | branches: 5 | - 'release-*.*.*' 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: | 12 | export VERSION=$(cat VERSION) 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: '14' 16 | registry-url: 'https://registry.npmjs.org' 17 | - run: npm install 18 | - run: npm test 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/run-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - run: | 9 | export VERSION=$(cat VERSION) 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '14' 13 | - run: npm install 14 | - run: npm test 15 | - run: ./node_modules/.bin/jest --coverage 16 | - name: Coveralls 17 | uses: coverallsapp/github-action@master 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | # yarn v2 111 | 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .pnp.* 116 | 117 | out/ -------------------------------------------------------------------------------- /.jsdoc.conf: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [], 3 | "recurseDepth": 10, 4 | "source": { 5 | "include": [ 6 | "src" 7 | ], 8 | "includePattern": ".+\\.js(doc|x)?$", 9 | "excludePattern": "(^|\\/|\\\\)_" 10 | }, 11 | "sourceType": "module", 12 | "tags": { 13 | "allowUnknownTags": true, 14 | "dictionaries": ["jsdoc","closure"] 15 | }, 16 | "templates": { 17 | "cleverLinks": false, 18 | "monospaceLinks": false 19 | } 20 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Exanion UG (haftungsbeschraenkt) 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zammad JS API implementation 2 | 3 | [![Build Status](https://travis-ci.org/exanion/zammad-js-api.svg?branch=dev)](https://travis-ci.org/exanion/zammad-js-api) 4 | [![Coverage Status](https://coveralls.io/repos/github/exanion/zammad-js-api/badge.svg?branch=dev)](https://coveralls.io/github/exanion/zammad-js-api) 5 | 6 | ## Contribution 7 | 8 | Contributions are always welcome! 9 | 10 | Please fork this repository and craete pull requests to the `dev` branch. After all tests pass and a review, they'll be merged into the upstream dev branch and included in a release soon. -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.4 2 | -------------------------------------------------------------------------------- /doc/zammad_objects.md: -------------------------------------------------------------------------------- 1 | # Zammad objects as returned by API (samples) 2 | 3 | ## User object 4 | 5 | ``` 6 | { 7 | "id": 3, 8 | "organization_id": null, 9 | "login": "peter.kappelt@exanion.de", 10 | "firstname": "Peter", 11 | "lastname": "Kappelt", 12 | "email": "peter.kappelt@exanion.de", 13 | "image": null, 14 | "image_source": null, 15 | "web": "", 16 | "phone": "", 17 | "fax": "", 18 | "mobile": "", 19 | "department": "", 20 | "street": "", 21 | "zip": "", 22 | "city": "", 23 | "country": "", 24 | "address": "", 25 | "vip": false, 26 | "verified": false, 27 | "active": true, 28 | "note": "", 29 | "last_login": "2020-04-13T05:36:33.216Z", 30 | "source": null, 31 | "login_failed": 0, 32 | "out_of_office": false, 33 | "out_of_office_start_at": null, 34 | "out_of_office_end_at": null, 35 | "out_of_office_replacement_id": null, 36 | "preferences": { 37 | "notification_config": { 38 | "matrix": { 39 | "create": { 40 | "criteria": { 41 | "owned_by_me": true, 42 | "owned_by_nobody": true, 43 | "no": false 44 | }, 45 | "channel": { 46 | "email": true, 47 | "online": true 48 | } 49 | }, 50 | "update": { 51 | "criteria": { 52 | "owned_by_me": true, 53 | "owned_by_nobody": true, 54 | "no": false 55 | }, 56 | "channel": { 57 | "email": true, 58 | "online": true 59 | } 60 | }, 61 | "reminder_reached": { 62 | "criteria": { 63 | "owned_by_me": true, 64 | "owned_by_nobody": false, 65 | "no": false 66 | }, 67 | "channel": { 68 | "email": true, 69 | "online": true 70 | } 71 | }, 72 | "escalation": { 73 | "criteria": { 74 | "owned_by_me": true, 75 | "owned_by_nobody": false, 76 | "no": false 77 | }, 78 | "channel": { 79 | "email": true, 80 | "online": true 81 | } 82 | } 83 | } 84 | }, 85 | "locale": "de-de", 86 | "intro": true 87 | }, 88 | "updated_by_id": 3, 89 | "created_by_id": 1, 90 | "created_at": "2020-04-12T03:38:16.023Z", 91 | "updated_at": "2020-04-13T05:36:33.221Z", 92 | "role_ids": [ 93 | 1, 94 | 2 95 | ], 96 | "organization_ids": [], 97 | "authorization_ids": [], 98 | "group_ids": { 99 | "1": [ 100 | "full" 101 | ] 102 | } 103 | } 104 | ``` 105 | 106 | ## Ticket object 107 | ``` 108 | { 109 | "id": 2, 110 | "group_id": 1, 111 | "priority_id": 2, 112 | "state_id": 1, 113 | "organization_id": null, 114 | "number": "27002", 115 | "title": "test1", 116 | "owner_id": 3, 117 | "customer_id": 4, 118 | "note": null, 119 | "first_response_at": null, 120 | "first_response_escalation_at": null, 121 | "first_response_in_min": null, 122 | "first_response_diff_in_min": null, 123 | "close_at": null, 124 | "close_escalation_at": null, 125 | "close_in_min": null, 126 | "close_diff_in_min": null, 127 | "update_escalation_at": null, 128 | "update_in_min": null, 129 | "update_diff_in_min": null, 130 | "last_contact_at": "2020-04-26T08:29:38.941Z", 131 | "last_contact_agent_at": null, 132 | "last_contact_customer_at": "2020-04-26T08:29:38.941Z", 133 | "last_owner_update_at": "2020-04-26T08:40:42.212Z", 134 | "create_article_type_id": 1, 135 | "create_article_sender_id": 2, 136 | "article_count": 1, 137 | "escalation_at": null, 138 | "pending_time": null, 139 | "type": null, 140 | "time_unit": null, 141 | "preferences": { 142 | "channel_id": 3 143 | }, 144 | "updated_by_id": 3, 145 | "created_by_id": 4, 146 | "created_at": "2020-04-26T08:29:38.915Z", 147 | "updated_at": "2020-04-26T08:40:42.201Z" 148 | } 149 | ``` 150 | 151 | ## Ticket State 152 | ``` 153 | { 154 | "id": 1, 155 | "state_type_id": 1, 156 | "name": "new", 157 | "next_state_id": null, 158 | "ignore_escalation": false, 159 | "default_create": true, 160 | "default_follow_up": false, 161 | "note": null, 162 | "active": true, 163 | "updated_by_id": 1, 164 | "created_by_id": 1, 165 | "created_at": "2020-04-26T08:20:31.876Z", 166 | "updated_at": "2020-04-26T08:20:31.895Z" 167 | } 168 | ``` 169 | 170 | ## Ticket Priority 171 | ``` 172 | { 173 | "id": 1, 174 | "name": "1 low", 175 | "default_create": false, 176 | "ui_icon": "low-priority", 177 | "ui_color": "low-priority", 178 | "note": null, 179 | "active": true, 180 | "updated_by_id": 1, 181 | "created_by_id": 1, 182 | "created_at": "2020-04-26T08:20:31.942Z", 183 | "updated_at": "2020-04-26T08:20:31.959Z" 184 | } 185 | ``` 186 | 187 | ## Ticket Article 188 | ``` 189 | { 190 | "id": 1, 191 | "ticket_id": 1, 192 | "type_id": 5, 193 | "sender_id": 2, 194 | "from": "Nicole Braun ", 195 | "to": null, 196 | "cc": null, 197 | "subject": null, 198 | "reply_to": null, 199 | "message_id": null, 200 | "message_id_md5": null, 201 | "in_reply_to": null, 202 | "content_type": "text/plain", 203 | "references": null, 204 | "body": "Welcome!\n\n Thank you for choosing Zammad.\n\n You will find updates and patches at https://zammad.org/. Online\n documentation is available at https://zammad.org/documentation. Get\n involved (discussions, contributing, ...) at https://zammad.org/participate.\n\n Regards,\n\n Your Zammad Team\n ", 205 | "internal": false, 206 | "preferences": {}, 207 | "updated_by_id": 2, 208 | "created_by_id": 2, 209 | "origin_by_id": 2, 210 | "created_at": "2020-04-26T08:20:32.327Z", 211 | "updated_at": "2020-04-26T08:20:32.327Z", 212 | "attachments": [], 213 | "type": "phone", 214 | "sender": "Customer", 215 | "created_by": "nicole.braun@zammad.org", 216 | "updated_by": "nicole.braun@zammad.org", 217 | "origin_by": "nicole.braun@zammad.org" 218 | } 219 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ApiError: require("./src/ApiError"), 3 | Ticket: require("./src/Ticket"), 4 | TicketArticle: require("./src/TicketArticle"), 5 | TicketPriority: require("./src/TicketPriority"), 6 | TicketState: require("./src/TicketState"), 7 | User: require("./src/User"), 8 | ZammadApi: require("./src/ZammadApi"), 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zammad-js-api", 3 | "version": "0.1.4", 4 | "description": "Zammad API implementation for JS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "coveralls": "jest --coverage && cat ./coverage/lcov.info | coveralls", 9 | "doc": "jsdoc -c .jsdoc.conf" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/exanion/zammad-js-api.git" 14 | }, 15 | "keywords": [ 16 | "zammad", 17 | "ticket", 18 | "helpdesk" 19 | ], 20 | "author": "Exanion UG (https://exanion.de)", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/exanion/zammad-js-api/issues" 24 | }, 25 | "homepage": "https://github.com/exanion/zammad-js-api#readme", 26 | "dependencies": { 27 | "axios": "^0.21.1", 28 | "axios-debug-log": "^0.8.2" 29 | }, 30 | "devDependencies": { 31 | "coveralls": "^3.0.11", 32 | "express": "^4.17.1", 33 | "jest": "^27.0.4", 34 | "jsdoc": "^3.6.4", 35 | "randomstring": "^1.1.5" 36 | }, 37 | "jest": { 38 | "testEnvironment": "node", 39 | "coveragePathIgnorePatterns": [ 40 | "/node_modules/", 41 | "/test" 42 | ] 43 | }, 44 | "files": [ 45 | "src/" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/ApiError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Various class definitions for errors that occur during API calls 3 | * @author Peter Kappelt 4 | */ 5 | 6 | class ApiError extends Error { 7 | /** 8 | * Instantiate a new Zammad Api Error object 9 | * @param {*} message Message of this error 10 | */ 11 | constructor(message) { 12 | super(`[ZammadApiError] ${message}`); 13 | this.name = "ZammadApiError"; 14 | } 15 | } 16 | 17 | class UnexpectedResponse extends ApiError { 18 | /** 19 | * Instantiate a new UnexpectedResponse error object 20 | * @param {*} message Message to show user 21 | * @param {*} expected Data type/ data that was expected to be received 22 | * @param {*} received Data type/ data that was actually received 23 | */ 24 | constructor(message, expected, received) { 25 | super(`[UnexpectedResponse] ${message}`); 26 | this.name = "ZammadApiError.UnexpectedResponse"; 27 | 28 | this.expected = expected; 29 | this.received = received; 30 | } 31 | } 32 | 33 | class InvalidRequest extends ApiError { 34 | /** 35 | * Instantiate a new InvalidRequest error object 36 | * @param {*} message Message to store 37 | */ 38 | constructor(message) { 39 | super(`[InvalidRequest] ${message}`); 40 | this.name = "ZammadApiError.InvalidRequest"; 41 | } 42 | } 43 | 44 | class Unimplemented extends ApiError { 45 | /** 46 | * Instantiate a new Unimplemented error object 47 | */ 48 | constructor(message = null) { 49 | super(`[Unimplemented] ${message}`); 50 | this.name = "ZammadApiError.Unimplemented"; 51 | } 52 | } 53 | 54 | module.exports = { 55 | ApiError, 56 | UnexpectedResponse, 57 | InvalidRequest, 58 | Unimplemented, 59 | }; 60 | -------------------------------------------------------------------------------- /src/Endpoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Endpoint path definitions for Zammad REST api 3 | * @author Peter Kappelt 4 | */ 5 | 6 | module.exports = { 7 | PREFIX: "/api/v1", 8 | USER_CURRENT: "/users/me", 9 | USER_LIST: "/users", 10 | USER_SEARCH: "/users/search", 11 | USER_SEARCH_QUERY: "query", 12 | USER_SHOW: "/users/", 13 | USER_CREATE: "/users", 14 | USER_UPDATE: "/users/", 15 | USER_DELETE: "/users/", 16 | TICKET_LIST: "/tickets", 17 | TICKET_SEARCH: "/tickets/search", 18 | TICKET_SEARCH_QUERY: "query", 19 | TICKET_SHOW: "/tickets/", 20 | TICKET_CREATE: "/tickets", 21 | TICKET_UPDATE: "/tickets/", 22 | TICKET_DELETE: "/tickets/", 23 | TICKET_STATE_LIST: "/ticket_states", 24 | TICKET_STATE_SHOW: "/ticket_states/", 25 | TICKET_STATE_CREATE: "/ticket_states", 26 | TICKET_STATE_UPDATE: "/ticket_states/", 27 | TICKET_STATE_DELETE: "/ticket_states", 28 | TICKET_PRIORITY_LIST: "/ticket_priorities", 29 | TICKET_PRIORITY_SHOW: "/ticket_priorities/", 30 | TICKET_PRIORITY_CREATE: "/ticket_priorities", 31 | TICKET_PRIORITY_UPDATE: "/ticket_priorities/", 32 | TICKET_PRIORITY_DELETE: "/ticket_priorities", 33 | TICKET_ARTICLE_BY_TICKET: "/ticket_articles/by_ticket/", 34 | TICKET_ARTICLE_SHOW: "/ticket_articles/", 35 | TICKET_ARTICLE_CREATE: "/ticket_articles" 36 | }; 37 | -------------------------------------------------------------------------------- /src/Ticket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ticket object 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const endpoints = require("./Endpoints"); 7 | const ApiError = require("./ApiError"); 8 | const User = require("./User"); 9 | const TicketState = require("./TicketState"); 10 | const TicketPriority = require("./TicketPriority"); 11 | const TicketArticle = require("./TicketArticle"); 12 | const Utility = require("./Utility"); 13 | const { Validators } = Utility; 14 | 15 | class Ticket { 16 | /** 17 | * Create new ticket object 18 | * @param {int} id Ticket object id 19 | * @param {string} number System ticket number 20 | * @param {string} title Ticket title 21 | * @param {int} groupId id of the group the ticket belongs to 22 | * @param {int} stateId id of the ticket's state 23 | * @param {in} priorityId id of the ticket's priority 24 | * @param {int} customerId id of the customer the ticket belongs to 25 | * @param {int} ownerId id of the user this ticket is assigned to, 1 (system) if unassigned 26 | * @param {string|null} note Note for the ticket 27 | * @param {string} updatedAt Updated at timestamp 28 | * @param {string} createdAt Created at timestamp 29 | */ 30 | constructor( 31 | id, 32 | title, 33 | number, 34 | groupId, 35 | stateId, 36 | priorityId, 37 | customerId, 38 | ownerId, 39 | note, 40 | updatedAt, 41 | createdAt 42 | ) { 43 | this.id = id; 44 | this.title = title; 45 | this.number = number; 46 | this.groupId = groupId; 47 | this.stateId = stateId; 48 | this.priorityId = priorityId; 49 | this.customerId = customerId; 50 | this.ownerId = ownerId; 51 | this.note = note; 52 | this.updatedAt = updatedAt; 53 | this.createdAt = createdAt; 54 | } 55 | 56 | /** 57 | * Create a new ticket object from the response received via API 58 | * @param {*} json Parsed json object 59 | */ 60 | static fromApiObject(response) { 61 | //sanity check of api response 62 | [ 63 | "id", 64 | "title", 65 | "number", 66 | "group_id", 67 | "state_id", 68 | "priority_id", 69 | "customer_id", 70 | "owner_id", 71 | "note", 72 | "updated_at", 73 | "created_at", 74 | ].forEach(key => { 75 | Utility.ObjectHasKeyOrUnexpectedResponse(response, key); 76 | }); 77 | 78 | let ticket = new Ticket( 79 | response.id, 80 | response.title, 81 | response.number, 82 | response.group_id, 83 | response.state_id, 84 | response.priority_id, 85 | response.customer_id, 86 | response.owner_id, 87 | response.note, 88 | response.updated_at, 89 | response.created_at 90 | ); 91 | 92 | return ticket; 93 | } 94 | 95 | /** 96 | * Serialize the current ticket object to an API valid json object 97 | */ 98 | toApiObject() { 99 | let ticket = {}; 100 | 101 | ticket.id = this.id; 102 | ticket.title = this.title; 103 | ticket.number = this.number; 104 | ticket.group_id = this.groupId; 105 | ticket.state_id = this.stateId; 106 | ticket.priority_id = this.priorityId; 107 | ticket.customer_id = this.customerId; 108 | ticket.owner_id = this.ownerId; 109 | ticket.note = this.note; 110 | 111 | return ticket; 112 | } 113 | 114 | /** 115 | * Gets all tickets that the authenticated user can view 116 | * @param {ZammadApi} api Initialized API object 117 | */ 118 | static async getAll(api) { 119 | let response = await api.doGetCall(endpoints.TICKET_LIST); 120 | if (!Array.isArray(response)) { 121 | throw new ApiError.UnexpectedResponse( 122 | "Invalid response (not received array)", 123 | "array", 124 | typeof response 125 | ); 126 | } 127 | 128 | let tickets = Array(); 129 | response.forEach(obj => { 130 | tickets.push(Ticket.fromApiObject(obj)); 131 | }); 132 | 133 | return tickets; 134 | } 135 | 136 | /** 137 | * Get a ticket by its id 138 | * @param {ZammadApi} api Initialized API object 139 | * @param {number} ticketId of ticket to get 140 | */ 141 | static async getById(api, ticketId) { 142 | ticketId = Validators.AssertInt(ticketId); 143 | let response = await api.doGetCall(endpoints.TICKET_SHOW + ticketId); 144 | return Ticket.fromApiObject(response); 145 | } 146 | 147 | /** 148 | * Search for one or more tickets that match the given query 149 | * @param {ZammadApi} api Initialized API object 150 | * @param {string} query Query string 151 | */ 152 | static async search(api, query) { 153 | let response = await api.doGetCallWithParams(endpoints.TICKET_SEARCH, { 154 | [endpoints.TICKET_SEARCH_QUERY]: query, 155 | }); 156 | if (!Array.isArray(response)) { 157 | throw new ApiError.UnexpectedResponse( 158 | "Invalid response (not received array)", 159 | "array", 160 | typeof response 161 | ); 162 | } 163 | 164 | let tickets = Array(); 165 | response.forEach(obj => { 166 | tickets.push(Ticket.fromApiObject(obj)); 167 | }); 168 | 169 | return tickets; 170 | } 171 | 172 | /** 173 | * Create a new ticket 174 | * @param {ZammadApi} api Initialized API object 175 | * @param {object} opt ticket options 176 | * @param {string} opt.title Ticket title 177 | * @param {int} opt.groupId Group Id for ticket 178 | * @param {int} opt.customerId Customer Id for ticket 179 | * @param {int} opt.ownerId Owner/ assigned user for ticket. 1 (system) if unassigned 180 | * @param {string} opt.articleBody Body of article to add (defautlt non-internal note) 181 | * @param {string} opt.articleSubject (Optional) Subject of ticket article, default null 182 | * @param {string} opt.articleType (Optional) Type of first article to add, default note 183 | * @param {boolean} opt.articleInternal (Optional) Set the internal attribute for the article, default false 184 | * @todo data validation 185 | * @return Ticket that was created 186 | */ 187 | static async create(api, opt) { 188 | //build ticket object to send to api 189 | let ticket = {}; 190 | 191 | if (!("title" in opt)) { 192 | throw new ApiError.InvalidRequest("title is required"); 193 | } 194 | ticket.title = opt.title; 195 | 196 | if (!("groupId" in opt)) { 197 | throw new ApiError.InvalidRequest("groupId is required"); 198 | } 199 | ticket.group_id = opt.groupId; 200 | 201 | if (!("customerId" in opt)) { 202 | throw new ApiError.InvalidRequest("customerId is required"); 203 | } 204 | ticket.customer_id = opt.customerId; 205 | 206 | if (!("articleBody" in opt)) { 207 | throw new ApiError.InvalidRequest("articleBody is required"); 208 | } 209 | ticket.article = { body: opt.articleBody }; 210 | 211 | //optional fields 212 | if ("articleSubject" in opt) { 213 | ticket.article.subject = opt.articleSubject; 214 | } 215 | if ("articleType" in opt) { 216 | ticket.article.type = opt.articleType; 217 | } 218 | if ("articleInternal" in opt) { 219 | ticket.article.internal = opt.articleInternal ? true : false; 220 | } 221 | if ("ownerId" in opt) { 222 | ticket.owner_id = opt.ownerId; 223 | } 224 | 225 | let response = await api.doPostCall(endpoints.TICKET_CREATE, ticket); 226 | 227 | return Ticket.fromApiObject(response); 228 | } 229 | 230 | /** 231 | * Push the changes of the current ticket 232 | * @param {ZammadApi} api Initialized API object 233 | * @todo fill response data in current object 234 | */ 235 | async update(api) { 236 | let ticket = this.toApiObject(); 237 | let response = await api.doPutCall( 238 | endpoints.TICKET_UPDATE + this.id, 239 | ticket 240 | ); 241 | } 242 | 243 | /** 244 | * Delete the current ticket on remote 245 | * @param {ZammadApi} api Initialized API object 246 | */ 247 | async delete(api) { 248 | await api.doDeleteCall(endpoints.TICKET_DELETE + this.id); 249 | } 250 | 251 | /** 252 | * Return the customer the ticket belongs to 253 | * @param {ZammadApi} api Initialized API object 254 | * @returns {User} Customer this ticket belongs to 255 | */ 256 | async customer(api) { 257 | if (this.customerId) { 258 | return await User.getById(api, this.customerId); 259 | } else { 260 | return null; 261 | } 262 | } 263 | 264 | /** 265 | * Return the user the ticket is assigned to 266 | * Returns system if unassigned 267 | * @param {ZammadApi} api Initialized API object 268 | * @returns {User} User this ticket is assigned to 269 | */ 270 | async owner(api) { 271 | if (this.ownerId) { 272 | return await User.getById(api, this.ownerId); 273 | } else { 274 | return null; 275 | } 276 | } 277 | 278 | /** 279 | * Return the priority for this ticket 280 | * @param {ZammadApi} api Initialized API object 281 | * @returns {TicketPriority} priority of this ticket 282 | */ 283 | async priority(api) { 284 | if (this.priorityId) { 285 | return await TicketPriority.getById(api, this.priorityId); 286 | } else { 287 | return null; 288 | } 289 | } 290 | 291 | /** 292 | * Return the state of this ticket 293 | * @param {ZammadApi} api Initialized API object 294 | * @returns {TicketState} current state of the ticket 295 | */ 296 | async state(api) { 297 | if (this.stateId) { 298 | return await TicketState.getById(api, this.stateId); 299 | } else { 300 | return null; 301 | } 302 | } 303 | 304 | /** 305 | * Return all articles (messages, notes etc) that belong to this tickets 306 | * @param {ZammadApi} api Initialized API object 307 | * @returns {TicketArticle[]} All articles for this ticket 308 | */ 309 | async articles(api) { 310 | return await TicketArticle.getForTicket(api, this.id); 311 | } 312 | } 313 | 314 | module.exports = Ticket; 315 | -------------------------------------------------------------------------------- /src/TicketArticle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ticket article object 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const endpoints = require("./Endpoints"); 7 | const ApiError = require("./ApiError"); 8 | const Ticket = require("./TicketState"); 9 | const Utility = require("./Utility"); 10 | const { Validators } = Utility; 11 | 12 | class TicketArticle { 13 | /** 14 | * Create a new ticket article object 15 | * @param {int} id Ticket article id 16 | * @param {int} ticketId Id of the ticket this article belongs to 17 | * @param {int} senderId Id of the user who sent this article 18 | * @param {string|null} subject subject of the article 19 | * @param {string} body Body of the article 20 | * @param {string} contentType MIME type of the body, usually "text/plain" 21 | * @param {boolean} internal Internal flag 22 | * @param {string} type Type of the created article, e.g. "phone", "mail" 23 | * @param {string} sender Sender of article, e.g. "Agent", "System", "Customer" 24 | * @param {int} createdById Created by ID 25 | * @param {int} updatedById Updated by ID 26 | * @param {string} updatedAt Updated at timestamp 27 | * @param {string} createdAt Created at timestamp 28 | */ 29 | constructor( 30 | id, 31 | ticketId, 32 | senderId, 33 | subject, 34 | body, 35 | contentType, 36 | internal, 37 | type, 38 | sender, 39 | from, 40 | to, 41 | cc, 42 | createdById, 43 | updatedById, 44 | updatedAt, 45 | createdAt 46 | ) { 47 | this.id = id; 48 | this.ticketId = ticketId; 49 | this.senderId = senderId; 50 | this.subject = subject; 51 | this.body = body; 52 | this.contentType = contentType; 53 | this.internal = internal; 54 | this.type = type; 55 | this.sender = sender; 56 | this.from = from; 57 | this.to = to; 58 | this.cc = cc; 59 | this.createdById = createdById; 60 | this.updatedById = updatedById; 61 | this.updatedAt = updatedAt; 62 | this.createdAt = createdAt; 63 | } 64 | 65 | /** 66 | * Create a new ticket article object from the response received via API 67 | * @param {*} json Parsed json object 68 | */ 69 | static fromApiObject(response) { 70 | //sanity check of api response 71 | [ 72 | "id", 73 | "ticket_id", 74 | "sender_id", 75 | "subject", 76 | "body", 77 | "content_type", 78 | "internal", 79 | "type", 80 | "sender", 81 | "from", 82 | "to", 83 | "cc", 84 | "created_by_id", 85 | "updated_by_id", 86 | "created_at", 87 | "updated_at", 88 | ].forEach((key) => { 89 | Utility.ObjectHasKeyOrUnexpectedResponse(response, key); 90 | }); 91 | 92 | let article = new TicketArticle( 93 | response.id, 94 | response.ticket_id, 95 | response.sender_id, 96 | response.subject, 97 | response.body, 98 | response.content_type, 99 | response.internal, 100 | response.type, 101 | response.sender, 102 | response.from, 103 | response.to, 104 | response.cc, 105 | response.created_by_id, 106 | response.updated_by_id, 107 | response.updated_at, 108 | response.created_at 109 | ); 110 | 111 | return article; 112 | } 113 | 114 | /** 115 | * Gets all articles that belong to a ticket 116 | * @param {ZammadApi} api Initialized API object 117 | * @param {int} ticketId Id of the ticket to query 118 | */ 119 | static async getForTicket(api, ticketId) { 120 | let response = await api.doGetCall( 121 | endpoints.TICKET_ARTICLE_BY_TICKET + ticketId 122 | ); 123 | if (!Array.isArray(response)) { 124 | throw new ApiError.UnexpectedResponse( 125 | "Invalid response (not received array)", 126 | "array", 127 | typeof response 128 | ); 129 | } 130 | 131 | let articles = Array(); 132 | response.forEach((obj) => { 133 | articles.push(TicketArticle.fromApiObject(obj)); 134 | }); 135 | 136 | return articles; 137 | } 138 | 139 | /** 140 | * Get a ticket article by its id 141 | * @param {ZammadApi} api Initialized API object 142 | * @param {number} articleId id of article to get 143 | */ 144 | static async getById(api, articleId) { 145 | articleId = Validators.AssertInt(articleId); 146 | let response = await api.doGetCall( 147 | endpoints.TICKET_ARTICLE_SHOW + articleId 148 | ); 149 | return TicketArticle.fromApiObject(response); 150 | } 151 | 152 | /** 153 | * Create a new ticket article 154 | * @param {ZammadApi} api Initialized API object 155 | * @param {object} opt article options 156 | * @param {string} opt.body article body content 157 | * @param {number} opt.ticketId Ticket the article shall be created for 158 | * @param {string|null} opt.to To address for email article 159 | * @param {string|null} opt.cc CC address for email article 160 | * @param {string|null} opt.subject (Optional) subject of article 161 | * @param {string|null} opt.contentType (Optional) content type of body 162 | * @param {boolean} opt.internal (Optional) internal flag for the article, default false 163 | * @param {string} opt.type (Optional) type for the article, default "note" 164 | * @todo data validation 165 | * @return Ticket article that was created 166 | */ 167 | static async create(api, opt) { 168 | //build article object to send to api 169 | let article = {}; 170 | 171 | if (!("body" in opt)) { 172 | throw new ApiError.InvalidRequest("body is required"); 173 | } 174 | article.body = opt.body; 175 | if (!("ticketId" in opt)) { 176 | throw new ApiError.InvalidRequest("ticketId is required"); 177 | } 178 | article.ticket_id = opt.ticketId; 179 | 180 | //optional fields 181 | if ("subject" in opt) { 182 | article.subject = opt.subject; 183 | } 184 | if ("contentType" in opt) { 185 | article.content_type = opt.contentType; 186 | } 187 | if ("internal" in opt) { 188 | article.internal = opt.internal ? true : false; 189 | } 190 | if ("type" in opt) { 191 | article.type = opt.type; 192 | } 193 | if("to" in opt){ 194 | article.to = opt.to; 195 | } 196 | if("cc" in opt){ 197 | article.cc = opt.cc; 198 | } 199 | 200 | let response = await api.doPostCall( 201 | endpoints.TICKET_ARTICLE_CREATE, 202 | article 203 | ); 204 | 205 | return TicketArticle.fromApiObject(response); 206 | } 207 | 208 | /** 209 | * Return the sender who created this article 210 | * @param {ZammadApi} api Initialized API object 211 | * @returns {User} Sender of the ticket 212 | */ 213 | async sender(api) { 214 | if (this.customerId) { 215 | return await User.getById(api, this.customerId); 216 | } else { 217 | return null; 218 | } 219 | } 220 | 221 | /** 222 | * Get the ticket this article belongs to 223 | * @param {ZammadApi} api Initialized API object 224 | * @returns {Ticket} ticket 225 | */ 226 | async ticket(api) { 227 | if (this.ticketId) { 228 | return await Ticket.getById(api, this.ticketId); 229 | } else { 230 | return null; 231 | } 232 | } 233 | } 234 | 235 | module.exports = TicketArticle; 236 | -------------------------------------------------------------------------------- /src/TicketPriority.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TicketPriority object 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const endpoints = require("./Endpoints"); 7 | const ApiError = require("./ApiError"); 8 | const Utility = require("./Utility"); 9 | const { Validators } = Utility; 10 | 11 | class TicketPriority { 12 | /** 13 | * Create a new ticket priority object 14 | * @param {int} id Ticket Priority ID 15 | * @param {string} name Name of ticket priority 16 | * @param {boolean} active 17 | * @param {string} note 18 | * @param {string} updatedAt 19 | * @param {string} createdAt 20 | */ 21 | constructor(id, name, active, note, updatedAt, createdAt) { 22 | this.id = id; 23 | this.name = name; 24 | this.active = active; 25 | this.note = note; 26 | this.updatedAt = updatedAt; 27 | this.createdAt = createdAt; 28 | } 29 | 30 | /** 31 | * Create a new ticket priorioty object from the response received via API 32 | * @param {*} json Parsed json object 33 | */ 34 | static fromApiObject(response) { 35 | //sanity check of api response 36 | ["id", "name", "active", "note", "updated_at", "created_at"].forEach( 37 | (key) => { 38 | Utility.ObjectHasKeyOrUnexpectedResponse(response, key); 39 | } 40 | ); 41 | 42 | let tprio = new TicketPriority( 43 | response.id, 44 | response.name, 45 | response.active, 46 | response.note, 47 | response.updated_at, 48 | response.created_at 49 | ); 50 | 51 | return tprio; 52 | } 53 | 54 | /** 55 | * Serialize the current ticket priority object to an API valid json object 56 | */ 57 | toApiObject() { 58 | let tprio = {}; 59 | 60 | tprio.id = this.id; 61 | tprio.name = this.name; 62 | tprio.active = this.active; 63 | tprio.note = this.note; 64 | 65 | return tprio; 66 | } 67 | 68 | /** 69 | * Gets all ticket priorities the instance supports 70 | * @param {ZammadApi} api Initialized API object 71 | */ 72 | static async getAll(api) { 73 | let response = await api.doGetCall(endpoints.TICKET_PRIORITY_LIST); 74 | if (!Array.isArray(response)) { 75 | throw new ApiError.UnexpectedResponse( 76 | "Invalid response (not received array)", 77 | "array", 78 | typeof response 79 | ); 80 | } 81 | 82 | let tprios = Array(); 83 | response.forEach((obj) => { 84 | tprios.push(TicketPriority.fromApiObject(obj)); 85 | }); 86 | 87 | return tprios; 88 | } 89 | 90 | /** 91 | * Get a ticket priority by its id 92 | * @param {ZammadApi} api Initialized API object 93 | * @param {number} tprioId id of priority to get 94 | * @todo implementation 95 | */ 96 | static async getById(api, tprioId) { 97 | tprioId = Validators.AssertInt(tprioId); 98 | let response = await api.doGetCall( 99 | endpoints.TICKET_PRIORITY_SHOW + tprioId 100 | ); 101 | return TicketPriority.fromApiObject(response); 102 | } 103 | 104 | /** 105 | * Create a new ticket priority 106 | * @param {ZammadApi} api Initialized API object 107 | * @param {object} opt priority options 108 | * @param {string} opt.name State Name 109 | * @param {boolean} opt.active 110 | * @param {string|null} opt.note 111 | * @todo implementation 112 | * @return User that was created 113 | */ 114 | static async create(api, opt) { 115 | throw new ApiError.Unimplemented(); 116 | } 117 | 118 | /** 119 | * Push the changes of the current priority 120 | * @param {ZammadApi} api Initialized API object 121 | * @todo implement 122 | */ 123 | async update(api) { 124 | throw new ApiError.Unimplemented(); 125 | } 126 | 127 | /** 128 | * Delete the current priority on remote 129 | * @param {ZammadApi} api Initialized API object 130 | * @todo implement 131 | */ 132 | async delete(api) { 133 | throw new ApiError.Unimplemented(); 134 | } 135 | } 136 | 137 | module.exports = TicketPriority; 138 | -------------------------------------------------------------------------------- /src/TicketState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TicketState object 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const endpoints = require("./Endpoints"); 7 | const ApiError = require("./ApiError"); 8 | const Utility = require("./Utility"); 9 | const { Validators } = Utility; 10 | 11 | class TicketState { 12 | /** 13 | * Create a new ticket state object 14 | * @param {int} id Ticket State ID 15 | * @param {string} name Name of ticket state 16 | * @param {int} stateTypeId 17 | * @param {int} nextStateId 18 | * @param {boolean} ignoreEscalation 19 | * @param {boolean} active 20 | * @param {string} note 21 | * @param {string} updatedAt 22 | * @param {string} createdAt 23 | */ 24 | constructor( 25 | id, 26 | name, 27 | stateTypeId, 28 | nextStateId, 29 | ignoreEscalation, 30 | active, 31 | note, 32 | updatedAt, 33 | createdAt 34 | ) { 35 | this.id = id; 36 | this.name = name; 37 | this.stateTypeId = stateTypeId; 38 | this.nextStateId = nextStateId; 39 | this.ignoreEscalation = ignoreEscalation; 40 | this.active = active; 41 | this.note = note; 42 | this.updatedAt = updatedAt; 43 | this.createdAt = createdAt; 44 | } 45 | 46 | /** 47 | * Create a new ticket state object from the response received via API 48 | * @param {*} json Parsed json object 49 | */ 50 | static fromApiObject(response) { 51 | //sanity check of api response 52 | [ 53 | "id", 54 | "name", 55 | "state_type_id", 56 | "next_state_id", 57 | "ignore_escalation", 58 | "active", 59 | "note", 60 | "updated_at", 61 | "created_at", 62 | ].forEach((key) => { 63 | Utility.ObjectHasKeyOrUnexpectedResponse(response, key); 64 | }); 65 | 66 | let tstate = new TicketState( 67 | response.id, 68 | response.name, 69 | response.state_type_id, 70 | response.next_state_id, 71 | response.ignore_escalation, 72 | response.active, 73 | response.note, 74 | response.updated_at, 75 | response.created_at 76 | ); 77 | 78 | return tstate; 79 | } 80 | 81 | /** 82 | * Serialize the current ticket state object to an API valid json object 83 | */ 84 | toApiObject() { 85 | let tstate = {}; 86 | 87 | tstate.id = this.id; 88 | tstate.name = this.name; 89 | tstate.state_type_id = this.stateTypeId; 90 | tstate.next_state_id = this.nextStateId; 91 | tstate.ignore_escalation = this.ignoreEscalation; 92 | tstate.active = this.active; 93 | tstate.note = this.note; 94 | 95 | return tstate; 96 | } 97 | 98 | /** 99 | * Gets all ticket states the instance supports 100 | * @param {ZammadApi} api Initialized API object 101 | */ 102 | static async getAll(api) { 103 | let response = await api.doGetCall(endpoints.TICKET_STATE_LIST); 104 | if (!Array.isArray(response)) { 105 | throw new ApiError.UnexpectedResponse( 106 | "Invalid response (not received array)", 107 | "array", 108 | typeof response 109 | ); 110 | } 111 | 112 | let tstates = Array(); 113 | response.forEach((obj) => { 114 | tstates.push(TicketState.fromApiObject(obj)); 115 | }); 116 | 117 | return tstates; 118 | } 119 | 120 | /** 121 | * Get a ticket state by its id 122 | * @param {ZammadApi} api Initialized API object 123 | * @param {number} tstateId id of state to get 124 | * @todo implementation 125 | */ 126 | static async getById(api, tstateId) { 127 | tstateId = Validators.AssertInt(tstateId); 128 | let response = await api.doGetCall( 129 | endpoints.TICKET_STATE_SHOW + tstateId 130 | ); 131 | return TicketState.fromApiObject(response); 132 | } 133 | 134 | /** 135 | * Create a new ticket state 136 | * @param {ZammadApi} api Initialized API object 137 | * @param {object} opt state options 138 | * @param {string} opt.name State Name 139 | * @param {int} opt.stateTypeId 140 | * @param {int|null} opt.nextStateId 141 | * @param {boolean} opt.ignoreEscalation 142 | * @param {boolean} opt.active 143 | * @param {string|null} opt.note 144 | * @todo implementation 145 | * @return User that was created 146 | */ 147 | static async create(api, opt) { 148 | throw new ApiError.Unimplemented(); 149 | } 150 | 151 | /** 152 | * Push the changes of the current state 153 | * @param {ZammadApi} api Initialized API object 154 | * @todo implement 155 | */ 156 | async update(api) { 157 | throw new ApiError.Unimplemented(); 158 | } 159 | 160 | /** 161 | * Delete the current state on remote 162 | * @param {ZammadApi} api Initialized API object 163 | * @todo implement 164 | */ 165 | async delete(api) { 166 | throw new ApiError.Unimplemented(); 167 | } 168 | } 169 | 170 | module.exports = TicketState; 171 | -------------------------------------------------------------------------------- /src/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User object 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const endpoints = require("./Endpoints"); 7 | const ApiError = require("./ApiError"); 8 | const Utility = require("./Utility"); 9 | const { Validators } = Utility; 10 | 11 | class User { 12 | /** 13 | * Create a new user object 14 | * @param {*} id Unique user id 15 | * @param {*} firstname User's firstname 16 | * @param {*} lastname User's lastname 17 | * @param {*} updatedAt Date of last user data update 18 | * @param {*} createdAt Date of user creation 19 | */ 20 | constructor(id, firstname, lastname, updatedAt, createdAt) { 21 | this.id = id; 22 | this.firstname = firstname; 23 | this.lastname = lastname; 24 | this.updatedAt = updatedAt; 25 | this.createdAt = createdAt; 26 | } 27 | 28 | /** 29 | * Set mail address of the user 30 | * @param {*} email Mail address of user 31 | */ 32 | setEmail(email) { 33 | this.email = email; 34 | } 35 | 36 | /** 37 | * Set a custom note associated with this user 38 | * @param {*} note Note for this user 39 | */ 40 | setNote(note) { 41 | this.note = note; 42 | } 43 | 44 | /** 45 | * Set the organization associated with this user 46 | * @param {int} organizationId Id of organization 47 | * @param {string} organizationName Name of organization 48 | */ 49 | setOrganization(organizationId, organizationName) { 50 | this.organizationId = organizationId; 51 | this.organizationName = organizationName; 52 | } 53 | 54 | /** 55 | * Create a new user object from the response received via API 56 | * @param {*} json Parsed json object 57 | */ 58 | static fromApiObject(response) { 59 | //sanity check of api response 60 | ["id", "firstname", "lastname", "updated_at", "created_at"].forEach( 61 | (key) => { 62 | Utility.ObjectHasKeyOrUnexpectedResponse(response, key); 63 | } 64 | ); 65 | 66 | let user = new User( 67 | response.id, 68 | response.firstname, 69 | response.lastname, 70 | response.updated_at, 71 | response.created_at 72 | ); 73 | 74 | //optional fields available in User object 75 | if ("email" in response) { 76 | user.setEmail(response.email); 77 | } 78 | if ("note" in response) { 79 | user.setNote(response.note); 80 | } 81 | if ("organization" in response && "organization_id" in response) { 82 | user.setOrganization( 83 | response.orgnaization_id, 84 | response.organization 85 | ); 86 | } 87 | 88 | return user; 89 | } 90 | 91 | /** 92 | * Serialize the current user object to an API valid json object 93 | */ 94 | toApiObject() { 95 | let user = {}; 96 | 97 | user.id = this.id; 98 | user.firstname = this.firstname; 99 | user.lastname = this.lastname; 100 | if (this.email) { 101 | user.email = this.email; 102 | } 103 | if (this.note) { 104 | user.note = this.note; 105 | } 106 | if (this.organizationId && this.organizationName) { 107 | user.orgnaization_id = this.organizationId; 108 | user.organization_name = this.organizationName; 109 | } 110 | 111 | return user; 112 | } 113 | 114 | /** 115 | * Gets the user that is currently authenticated for the API 116 | * @param {ZammadApi} api Initialized API object 117 | */ 118 | static async getAuthenticated(api) { 119 | let response = await api.doGetCall(endpoints.USER_CURRENT); 120 | return User.fromApiObject(response); 121 | } 122 | 123 | /** 124 | * Gets all users that are available for the system (if sufficient permission), 125 | * only own account otherwise 126 | * @param {ZammadApi} api Initialized API object 127 | */ 128 | static async getAll(api) { 129 | let response = await api.doGetCall(endpoints.USER_LIST); 130 | if (!Array.isArray(response)) { 131 | throw new ApiError.UnexpectedResponse( 132 | "Invalid response (not received array)", 133 | "array", 134 | typeof response 135 | ); 136 | } 137 | 138 | let users = Array(); 139 | response.forEach((obj) => { 140 | users.push(User.fromApiObject(obj)); 141 | }); 142 | 143 | return users; 144 | } 145 | 146 | /** 147 | * Get a user by its id 148 | * @param {ZammadApi} api Initialized API object 149 | * @param {number} userId ID of user to get 150 | */ 151 | static async getById(api, userId) { 152 | userId = Validators.AssertInt(userId); 153 | let response = await api.doGetCall(endpoints.USER_SHOW + userId); 154 | return User.fromApiObject(response); 155 | } 156 | 157 | /** 158 | * Search for one or more users that match the given query 159 | * @param {ZammadApi} api Initialized API object 160 | * @param {string} query Query string 161 | */ 162 | static async search(api, query) { 163 | let response = await api.doGetCallWithParams(endpoints.USER_SEARCH, { 164 | [endpoints.USER_SEARCH_QUERY]: query, 165 | }); 166 | if (!Array.isArray(response)) { 167 | throw new ApiError.UnexpectedResponse( 168 | "Invalid response (not received array)", 169 | "array", 170 | typeof response 171 | ); 172 | } 173 | 174 | let users = Array(); 175 | response.forEach((obj) => { 176 | users.push(User.fromApiObject(obj)); 177 | }); 178 | 179 | return users; 180 | } 181 | 182 | /** 183 | * Create a new user 184 | * @param {ZammadApi} api Initialized API object 185 | * @param {object} opt user options 186 | * @param {string} opt.firstname Firstname of user 187 | * @param {string} opt.lastname Lastname of user 188 | * @param {string} opt.email (Optional) Email of user 189 | * @param {string} opt.organization (Optional) Name of organization user belongs to 190 | * @todo data validation 191 | * @return User that was created 192 | */ 193 | static async create(api, opt) { 194 | //build user object to send to api 195 | let user = {}; 196 | 197 | if (!("firstname" in opt)) { 198 | throw new ApiError.InvalidRequest("firstname is required"); 199 | } 200 | user.firstname = opt.firstname; 201 | 202 | if (!("lastname" in opt)) { 203 | throw new ApiError.InvalidRequest("lastname is required"); 204 | } 205 | user.lastname = opt.lastname; 206 | 207 | //optional fields 208 | if ("email" in opt) { 209 | user.email = opt.email; 210 | } 211 | if ("organization" in opt) { 212 | user.organization = opt.organization; 213 | } 214 | 215 | let response = await api.doPostCall(endpoints.USER_CREATE, user); 216 | 217 | return User.fromApiObject(response); 218 | } 219 | 220 | /** 221 | * Push the changes of the current user 222 | * @param {ZammadApi} api Initialized API object 223 | * @todo fill response data in current object 224 | */ 225 | async update(api) { 226 | let user = this.toApiObject(); 227 | let response = await api.doPutCall( 228 | endpoints.USER_UPDATE + this.id, 229 | user 230 | ); 231 | } 232 | 233 | /** 234 | * Delete the current user on remote 235 | * @param {ZammadApi} api Initialized API object 236 | */ 237 | async delete(api) { 238 | await api.doDeleteCall(endpoints.USER_DELETE + this.id); 239 | } 240 | } 241 | 242 | module.exports = User; 243 | -------------------------------------------------------------------------------- /src/Utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Various utility functions 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const ApiError = require("./ApiError"); 7 | 8 | module.exports = { 9 | Validators: { 10 | AssertInt: (check) => { 11 | if (check != parseInt(check)) { 12 | throw new ApiError.InvalidRequest("Expected integer"); 13 | } 14 | 15 | return parseInt(check); 16 | }, 17 | }, 18 | /** 19 | * Throw ApiError.UnexpectedResponse if key is not in object 20 | * @param {object} object object to check 21 | * @param {string} key key that the object shall have 22 | * @throws ApiError.UnexpectedResponse 23 | */ 24 | ObjectHasKeyOrUnexpectedResponse: (object, key) => { 25 | if (!(key in object)) { 26 | throw new ApiError.UnexpectedResponse( 27 | `${key} attribute missing`, 28 | `${key} field`, 29 | `no ${key} field present` 30 | ); 31 | } 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/ZammadApi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ZammadApi top level and helper functions 3 | * @author Peter Kappelt 4 | */ 5 | 6 | const _axios = require("axios"); 7 | let axios; 8 | const endpoints = require("./Endpoints"); 9 | const ApiError = require("./ApiError"); 10 | 11 | class ZammadApi { 12 | /** 13 | * Connect to a zammad API 14 | * @param {string} host Hostname of Zammad instance with protocol and port 15 | * @param {string} username Username for authentication 16 | * @param {string} password Password for authentication 17 | * @todo hostname check and sanitising 18 | */ 19 | constructor(host, username, password) { 20 | this.host = host; 21 | this.username = username; 22 | this.password = password; 23 | axios = _axios.create({ 24 | baseURL: this.host + endpoints.PREFIX, 25 | auth: { 26 | username, 27 | password, 28 | }, 29 | headers: { 30 | "User-Agent": "Zammad Mobile by Exanion/1.0" 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * Throws an error if the given data is not an object 37 | * @param {*} data Data to be checked 38 | * @throws ApiError.UnexpectedResponse if not an object 39 | */ 40 | _isObjectOrError(data) { 41 | if (typeof data !== "object") { 42 | throw new ApiError.UnexpectedResponse( 43 | "Type of checked data is not object!", 44 | "object", 45 | typeof data 46 | ); 47 | } 48 | } 49 | 50 | /** 51 | * Check axios http response code 52 | * @param {*} res Axios response 53 | */ 54 | _checkResponseCode(res) { 55 | if (res.status !== 200 && res.status !== 201) { 56 | throw new ApiError.UnexpectedResponse( 57 | "Unexpected response code", 58 | "200/ 201", 59 | res.status 60 | ); 61 | } 62 | } 63 | 64 | /** 65 | * Perform a get call on a given endpoint, return result 66 | * @param {*} endpoint Endpoint to call 67 | */ 68 | async doGetCall(endpoint) { 69 | return await this.doGetCallWithParams(endpoint, {}); 70 | } 71 | 72 | /** 73 | * Perform a get call on a given endpoint with 74 | * additional query parameters, return result 75 | * @param {*} endpoint Endpoint to call 76 | * @param {*} params associative array in form "param": "value" 77 | */ 78 | async doGetCallWithParams(endpoint, params) { 79 | let response = await axios.get(endpoint, { 80 | params, 81 | }); 82 | this._isObjectOrError(response.data); 83 | this._checkResponseCode(response); 84 | return response.data; 85 | } 86 | 87 | /** 88 | * Perform a post call on a given endpoint, return result 89 | * @param {*} endpoint Endpoint to call 90 | * @param {string|object} body Body of the post request 91 | */ 92 | async doPostCall(endpoint, body) { 93 | let response = await axios.post(endpoint, body); 94 | this._isObjectOrError(response.data); 95 | this._checkResponseCode(response); 96 | return response.data; 97 | } 98 | 99 | async doPutCall(endpoint, body) { 100 | let response = await axios.put(endpoint, body); 101 | this._isObjectOrError(response.data); 102 | this._checkResponseCode(response); 103 | return response.data; 104 | } 105 | 106 | async doDeleteCall(endpoint) { 107 | let response = await axios.delete(endpoint); 108 | this._isObjectOrError(response.data); 109 | this._checkResponseCode(response); 110 | return response.data; 111 | } 112 | } 113 | 114 | module.exports = ZammadApi; 115 | -------------------------------------------------------------------------------- /test/Ticket.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const { DummyEndpointProvider } = require("./utility/DummyEndpointProvider"); 6 | const DataSeeder = require("./utility/DataSeeder"); 7 | 8 | const ZammadApi = require("../src/ZammadApi"); 9 | const Ticket = require("../src/Ticket"); 10 | 11 | let api; 12 | let ep; 13 | 14 | beforeAll(() => { 15 | ep = new DummyEndpointProvider("/api/v1"); 16 | api = new ZammadApi(`http://localhost:${ep.port}`); 17 | }); 18 | 19 | /** 20 | * Create a user json object with random data 21 | */ 22 | function createRandomTicket() { 23 | const id = DataSeeder.randomId(); 24 | const group_id = DataSeeder.randomId(); 25 | const priority_id = DataSeeder.randomId(); 26 | const state_id = DataSeeder.randomId(); 27 | const number = DataSeeder.randomString(5); 28 | const title = DataSeeder.randomString(20); 29 | const customer_id = DataSeeder.randomId(); 30 | const owner_id = DataSeeder.randomId(); 31 | const note = DataSeeder.randomString(50); 32 | const updatedAt = DataSeeder.randomIsoTimestamp(); 33 | const createdAt = DataSeeder.randomIsoTimestamp(); 34 | 35 | return { 36 | id, 37 | group_id, 38 | priority_id, 39 | state_id, 40 | number, 41 | title, 42 | customer_id, 43 | owner_id, 44 | note, 45 | updated_at: updatedAt, 46 | created_at: createdAt, 47 | }; 48 | } 49 | 50 | /** 51 | * Check if api ticket object matches parsed ticket 52 | * @param {object} apiTicket json ticket api object 53 | * @param {User} parsedTicket ticket parsed by the implementation 54 | */ 55 | function checkIfApiTicketMatchesParsed(apiTicket, parsedTicket) { 56 | expect(typeof parsedTicket).toBe("object"); 57 | expect(typeof apiTicket).toBe("object"); 58 | 59 | expect(parsedTicket.id).toBe(apiTicket.id); 60 | expect(parsedTicket.groupId).toBe(apiTicket.group_id); 61 | expect(parsedTicket.priorityId).toBe(apiTicket.priority_id); 62 | expect(parsedTicket.stateId).toBe(apiTicket.state_id); 63 | expect(parsedTicket.number).toBe(apiTicket.number); 64 | expect(parsedTicket.title).toBe(apiTicket.title); 65 | expect(parsedTicket.customerId).toBe(apiTicket.customer_id); 66 | expect(parsedTicket.ownerId).toBe(apiTicket.owner_id); 67 | expect(parsedTicket.note).toBe(apiTicket.note); 68 | expect(parsedTicket.updatedAt).toBe(apiTicket.updated_at); 69 | expect(parsedTicket.createdAt).toBe(apiTicket.created_at); 70 | } 71 | 72 | test("ticket list get", async () => { 73 | let randomApiTickets = Array(Math.floor(Math.random() * 9) + 1); 74 | let requestMade = false; 75 | 76 | for (i = 0; i < randomApiTickets.length; i++) { 77 | randomApiTickets[i] = createRandomTicket(); 78 | } 79 | ep.createEndpoint( 80 | DummyEndpointProvider.Method.GET, 81 | "/tickets", 82 | randomApiTickets, 83 | (req) => { 84 | requestMade = true; 85 | } 86 | ); 87 | 88 | let response = await Ticket.getAll(api); 89 | let checkedObjects = 0; 90 | 91 | for (i = 0; i < response.length; i++) { 92 | //iterate over all users, search for matching json object 93 | randomApiTickets.forEach((obj) => { 94 | if (obj.id == response[i].id) { 95 | checkIfApiTicketMatchesParsed(obj, response[i]); 96 | checkedObjects++; 97 | } 98 | }); 99 | } 100 | 101 | expect(checkedObjects).toBe(randomApiTickets.length); 102 | expect(requestMade).toBe(true); 103 | }); 104 | 105 | test("ticket search", async () => { 106 | let queryString = DataSeeder.randomString(10); 107 | let requestMade = false; 108 | 109 | let randomApiTickets = Array(Math.floor(Math.random() * 9) + 1); 110 | for (i = 0; i < randomApiTickets.length; i++) { 111 | randomApiTickets[i] = createRandomTicket(); 112 | } 113 | ep.createEndpoint( 114 | DummyEndpointProvider.Method.GET, 115 | "/tickets/search", 116 | randomApiTickets, 117 | async (req) => { 118 | //expect to have query string that matches 119 | expect(req.query).toHaveProperty("query", queryString); 120 | requestMade = true; 121 | } 122 | ); 123 | 124 | let response = await Ticket.search(api, queryString); 125 | let checkedObjects = 0; 126 | 127 | for (i = 0; i < response.length; i++) { 128 | //iterate over all users, search for matching json object 129 | randomApiTickets.forEach((obj) => { 130 | if (obj.id == response[i].id) { 131 | checkIfApiTicketMatchesParsed(obj, response[i]); 132 | checkedObjects++; 133 | } 134 | }); 135 | } 136 | 137 | expect(checkedObjects).toBe(randomApiTickets.length); 138 | expect(requestMade).toBe(true); 139 | }); 140 | 141 | test("show ticket details", async () => { 142 | let ticket = createRandomTicket(); 143 | let requestMade = false; 144 | 145 | ep.createEndpoint( 146 | DummyEndpointProvider.Method.GET, 147 | "/tickets/" + ticket.id, 148 | ticket, 149 | (req) => { 150 | requestMade = true; 151 | } 152 | ); 153 | 154 | let response = await Ticket.getById(api, ticket.id); 155 | 156 | checkIfApiTicketMatchesParsed(ticket, response); 157 | expect(requestMade).toBe(true); 158 | }); 159 | 160 | test("ticket create", async () => { 161 | let plainTicket = createRandomTicket(); 162 | plainTicket.article = { 163 | body: DataSeeder.randomString(30), 164 | }; 165 | let ticket; 166 | let requestMade = false; 167 | 168 | ep.createEndpoint( 169 | DummyEndpointProvider.Method.POST, 170 | "/tickets", 171 | plainTicket, 172 | async (req) => { 173 | expect(req.body.title).toBe(plainTicket.title); 174 | expect(req.body.group_id).toBe(plainTicket.group_id); 175 | expect(req.body.customer_id).toBe(plainTicket.customer_id); 176 | expect(req.body.owner_id).toBe(plainTicket.owner_id); 177 | expect(typeof req.body.article).toBe("object"); 178 | expect(req.body.article.body).toBe(plainTicket.article.body); 179 | requestMade = true; 180 | } 181 | ); 182 | 183 | ticket = await Ticket.create(api, { 184 | title: plainTicket.title, 185 | groupId: plainTicket.group_id, 186 | customerId: plainTicket.customer_id, 187 | ownerId: plainTicket.owner_id, 188 | articleBody: plainTicket.article.body, 189 | }); 190 | 191 | checkIfApiTicketMatchesParsed(plainTicket, ticket); 192 | expect(requestMade).toBe(true); 193 | }); 194 | 195 | test("ticket update", async () => { 196 | let plainTicket = createRandomTicket(); 197 | let ticket = Ticket.fromApiObject(plainTicket); 198 | let requestMade = false; 199 | 200 | ep.createEndpoint( 201 | DummyEndpointProvider.Method.PUT, 202 | "/tickets/" + plainTicket.id, 203 | plainTicket, 204 | async (req) => { 205 | checkIfApiTicketMatchesParsed(ticket, req.body); 206 | requestMade = true; 207 | } 208 | ); 209 | 210 | await ticket.update(api); 211 | expect(requestMade).toBe(true); 212 | }); 213 | 214 | test("ticket delete", async () => { 215 | let plainTicket = createRandomTicket(); 216 | let ticket = Ticket.fromApiObject(plainTicket); 217 | let requestMade = false; 218 | 219 | ep.createEndpoint( 220 | DummyEndpointProvider.Method.DELETE, 221 | "/tickets/" + plainTicket.id, 222 | {}, 223 | (req) => { 224 | requestMade = true; 225 | } 226 | ); 227 | await ticket.delete(api); 228 | expect(requestMade).toBe(true); 229 | }); 230 | 231 | afterAll(() => { 232 | ep.closeServer(); 233 | }); 234 | -------------------------------------------------------------------------------- /test/TicketArticle.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const { DummyEndpointProvider } = require("./utility/DummyEndpointProvider"); 6 | const DataSeeder = require("./utility/DataSeeder"); 7 | 8 | const ZammadApi = require("../src/ZammadApi"); 9 | const TicketArticle = require("../src/TicketArticle"); 10 | 11 | let api; 12 | let ep; 13 | 14 | beforeAll(() => { 15 | ep = new DummyEndpointProvider("/api/v1"); 16 | api = new ZammadApi(`http://localhost:${ep.port}`); 17 | }); 18 | 19 | /** 20 | * Create a user json object with random data 21 | */ 22 | function createRandomArticle() { 23 | const id = DataSeeder.randomId(); 24 | const ticket_id = DataSeeder.randomId(); 25 | const sender_id = DataSeeder.randomId(); 26 | const subject = DataSeeder.randomString(10); 27 | const body = DataSeeder.randomString(50); 28 | const content_type = DataSeeder.randomString(10); 29 | const internal = DataSeeder.randomBool(); 30 | const type = DataSeeder.randomString(10); 31 | const sender = "System"; 32 | const from = DataSeeder.randomMail(); 33 | const to = DataSeeder.randomMail(); 34 | const cc = DataSeeder.randomMail(); 35 | const createdById = DataSeeder.randomId(); 36 | const updatedById = DataSeeder.randomId(); 37 | const updatedAt = DataSeeder.randomIsoTimestamp(); 38 | const createdAt = DataSeeder.randomIsoTimestamp(); 39 | 40 | return { 41 | id, 42 | ticket_id, 43 | sender_id, 44 | subject, 45 | body, 46 | content_type, 47 | internal, 48 | type, 49 | sender, 50 | from, 51 | to, 52 | cc, 53 | created_by_id: createdById, 54 | updated_by_id: updatedById, 55 | updated_at: updatedAt, 56 | created_at: createdAt, 57 | }; 58 | } 59 | 60 | /** 61 | * Check if api article object matches parsed article 62 | * @param {object} apiArticle json article api object 63 | * @param {object} parsedArticle article parsed by the implementation 64 | */ 65 | function checkIfApiArticleMatchesParsed(apiArticle, parsedArticle) { 66 | expect(typeof parsedArticle).toBe("object"); 67 | expect(typeof apiArticle).toBe("object"); 68 | 69 | expect(parsedArticle.id).toBe(apiArticle.id); 70 | expect(parsedArticle.ticketId).toBe(apiArticle.ticket_id); 71 | expect(parsedArticle.senderId).toBe(apiArticle.sender_id); 72 | expect(parsedArticle.subject).toBe(apiArticle.subject); 73 | expect(parsedArticle.body).toBe(apiArticle.body); 74 | expect(parsedArticle.contentType).toBe(apiArticle.content_type); 75 | expect(parsedArticle.internal).toBe(apiArticle.internal); 76 | expect(parsedArticle.type).toBe(apiArticle.type); 77 | expect(parsedArticle.sender).toBe(apiArticle.sender); 78 | expect(parsedArticle.from).toBe(apiArticle.from); 79 | expect(parsedArticle.to).toBe(apiArticle.to); 80 | expect(parsedArticle.cc).toBe(apiArticle.cc); 81 | expect(parsedArticle.createdById).toBe(apiArticle.created_by_id); 82 | expect(parsedArticle.updatedById).toBe(apiArticle.updated_by_id); 83 | expect(parsedArticle.updatedAt).toBe(apiArticle.updated_at); 84 | expect(parsedArticle.createdAt).toBe(apiArticle.created_at); 85 | } 86 | 87 | test("article by ticket id", async () => { 88 | let randomApiArticles = Array(Math.floor(Math.random() * 9) + 1); 89 | let requestMade = false; 90 | 91 | for (i = 0; i < randomApiArticles.length; i++) { 92 | randomApiArticles[i] = createRandomArticle(); 93 | } 94 | ep.createEndpoint( 95 | DummyEndpointProvider.Method.GET, 96 | "/ticket_articles/by_ticket/" + randomApiArticles[0].ticket_id, 97 | randomApiArticles, 98 | (req) => { 99 | requestMade = true; 100 | } 101 | ); 102 | 103 | let response = await TicketArticle.getForTicket( 104 | api, 105 | randomApiArticles[0].ticket_id 106 | ); 107 | let checkedObjects = 0; 108 | 109 | for (i = 0; i < response.length; i++) { 110 | //iterate over all users, search for matching json object 111 | randomApiArticles.forEach((obj) => { 112 | if (obj.id == response[i].id) { 113 | checkIfApiArticleMatchesParsed(obj, response[i]); 114 | checkedObjects++; 115 | } 116 | }); 117 | } 118 | 119 | expect(checkedObjects).toBe(randomApiArticles.length); 120 | expect(requestMade).toBe(true); 121 | }); 122 | 123 | test("show article details", async () => { 124 | let article = createRandomArticle(); 125 | let requestMade = false; 126 | 127 | ep.createEndpoint( 128 | DummyEndpointProvider.Method.GET, 129 | "/ticket_articles/" + article.id, 130 | article, 131 | (req) => { 132 | requestMade = true; 133 | } 134 | ); 135 | 136 | let response = await TicketArticle.getById(api, article.id); 137 | 138 | checkIfApiArticleMatchesParsed(article, response); 139 | expect(requestMade).toBe(true); 140 | }); 141 | 142 | test("article create", async () => { 143 | let plainArticle = createRandomArticle(); 144 | let article; 145 | let requestMade = false; 146 | 147 | ep.createEndpoint( 148 | DummyEndpointProvider.Method.POST, 149 | "/ticket_articles", 150 | plainArticle, 151 | async (req) => { 152 | ["body", "subject", "content_type", "internal", "type"].forEach( 153 | (key) => { 154 | expect(req.body[key]).toBe(plainArticle[key]); 155 | } 156 | ); 157 | requestMade = true; 158 | } 159 | ); 160 | 161 | article = await TicketArticle.create(api, { 162 | body: plainArticle.body, 163 | ticketId: plainArticle.ticket_id, 164 | subject: plainArticle.subject, 165 | contentType: plainArticle.content_type, 166 | internal: plainArticle.internal, 167 | type: plainArticle.type, 168 | }); 169 | 170 | checkIfApiArticleMatchesParsed(plainArticle, article); 171 | expect(requestMade).toBe(true); 172 | }); 173 | 174 | afterAll(() => { 175 | ep.closeServer(); 176 | }); 177 | -------------------------------------------------------------------------------- /test/TicketPriority.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const { DummyEndpointProvider } = require("./utility/DummyEndpointProvider"); 6 | const DataSeeder = require("./utility/DataSeeder"); 7 | 8 | const ZammadApi = require("../src/ZammadApi"); 9 | const ApiError = require("../src/ApiError"); 10 | const TicketPriority = require("../src/TicketPriority"); 11 | 12 | let api; 13 | let ep; 14 | 15 | beforeAll(() => { 16 | ep = new DummyEndpointProvider("/api/v1"); 17 | api = new ZammadApi(`http://localhost:${ep.port}`); 18 | }); 19 | 20 | /** 21 | * Create a user json object with random data 22 | */ 23 | function createRandomTicketPriority() { 24 | const id = DataSeeder.randomId(); 25 | const name = DataSeeder.randomString(10); 26 | const active = DataSeeder.randomBool(); 27 | const note = DataSeeder.randomString(30); 28 | const updatedAt = DataSeeder.randomIsoTimestamp(); 29 | const createdAt = DataSeeder.randomIsoTimestamp(); 30 | 31 | return { 32 | id, 33 | name, 34 | active, 35 | note, 36 | updated_at: updatedAt, 37 | created_at: createdAt, 38 | }; 39 | } 40 | 41 | /** 42 | * Check if api ticket state object matches parsed state 43 | * @param {object} apiTicket json state api object 44 | * @param {User} parsedTicket state parsed by the implementation 45 | */ 46 | function checkIfPrioMatchesParsed(apiPrio, parsedPrio) { 47 | expect(typeof parsedPrio).toBe("object"); 48 | expect(typeof apiPrio).toBe("object"); 49 | 50 | expect(parsedPrio.id).toBe(apiPrio.id); 51 | expect(parsedPrio.name).toBe(apiPrio.name); 52 | expect(parsedPrio.active).toBe(apiPrio.active); 53 | expect(parsedPrio.note).toBe(apiPrio.note); 54 | expect(parsedPrio.updatedAt).toBe(apiPrio.updated_at); 55 | expect(parsedPrio.createdAt).toBe(apiPrio.created_at); 56 | } 57 | 58 | test("ticket priority list get", async () => { 59 | let randomApiPrios = Array(Math.floor(Math.random() * 9) + 1); 60 | let requestMade = false; 61 | 62 | for (i = 0; i < randomApiPrios.length; i++) { 63 | randomApiPrios[i] = createRandomTicketPriority(); 64 | } 65 | 66 | ep.createEndpoint( 67 | DummyEndpointProvider.Method.GET, 68 | "/ticket_priorities", 69 | randomApiPrios, 70 | (req) => { 71 | requestMade = true; 72 | } 73 | ); 74 | 75 | let response = await TicketPriority.getAll(api); 76 | let checkedObjects = 0; 77 | 78 | for (i = 0; i < response.length; i++) { 79 | //iterate over all users, search for matching json object 80 | randomApiPrios.forEach((obj) => { 81 | if (obj.id == response[i].id) { 82 | checkIfPrioMatchesParsed(obj, response[i]); 83 | checkedObjects++; 84 | } 85 | }); 86 | } 87 | 88 | expect(checkedObjects).toBe(randomApiPrios.length); 89 | expect(requestMade).toBe(true); 90 | }); 91 | 92 | test("show priority details", async () => { 93 | let prio = createRandomTicketPriority(); 94 | let requestMade = false; 95 | 96 | ep.createEndpoint( 97 | DummyEndpointProvider.Method.GET, 98 | "/ticket_priorities/" + prio.id, 99 | prio, 100 | (req) => { 101 | requestMade = true; 102 | } 103 | ); 104 | 105 | let response = await TicketPriority.getById(api, prio.id); 106 | 107 | checkIfPrioMatchesParsed(prio, response); 108 | expect(requestMade).toBe(true); 109 | }); 110 | 111 | test("ticket priority create", () => { 112 | expect(TicketPriority.create()).rejects.toEqual( 113 | new ApiError.Unimplemented() 114 | ); 115 | }); 116 | 117 | test("ticket priority update", () => { 118 | expect( 119 | TicketPriority.fromApiObject(createRandomTicketPriority()).update() 120 | ).rejects.toEqual(new ApiError.Unimplemented()); 121 | }); 122 | 123 | test("ticket priority delete", () => { 124 | expect( 125 | TicketPriority.fromApiObject(createRandomTicketPriority()).delete() 126 | ).rejects.toEqual(new ApiError.Unimplemented()); 127 | }); 128 | 129 | afterAll(() => { 130 | ep.closeServer(); 131 | }); 132 | -------------------------------------------------------------------------------- /test/TicketState.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const { DummyEndpointProvider } = require("./utility/DummyEndpointProvider"); 6 | const DataSeeder = require("./utility/DataSeeder"); 7 | 8 | const ZammadApi = require("../src/ZammadApi"); 9 | const ApiError = require("../src/ApiError"); 10 | const TicketState = require("../src/TicketState"); 11 | 12 | let api; 13 | let ep; 14 | 15 | beforeAll(() => { 16 | ep = new DummyEndpointProvider("/api/v1"); 17 | api = new ZammadApi(`http://localhost:${ep.port}`); 18 | }); 19 | 20 | /** 21 | * Create a user json object with random data 22 | */ 23 | function createRandomTicketState() { 24 | const id = DataSeeder.randomId(); 25 | const name = DataSeeder.randomString(10); 26 | const state_type_id = DataSeeder.randomId(); 27 | const next_state_id = DataSeeder.randomId(); 28 | const ignore_escalation = DataSeeder.randomBool(); 29 | const active = DataSeeder.randomBool(); 30 | const note = DataSeeder.randomString(30); 31 | const updatedAt = DataSeeder.randomIsoTimestamp(); 32 | const createdAt = DataSeeder.randomIsoTimestamp(); 33 | 34 | return { 35 | id, 36 | name, 37 | state_type_id, 38 | next_state_id, 39 | ignore_escalation, 40 | active, 41 | note, 42 | updated_at: updatedAt, 43 | created_at: createdAt, 44 | }; 45 | } 46 | 47 | /** 48 | * Check if api ticket state object matches parsed state 49 | * @param {object} apiTicket json state api object 50 | * @param {User} parsedTicket state parsed by the implementation 51 | */ 52 | function checkIfStateMatchesParsed(apiState, parsedState) { 53 | expect(typeof parsedState).toBe("object"); 54 | expect(typeof apiState).toBe("object"); 55 | 56 | expect(parsedState.id).toBe(apiState.id); 57 | expect(parsedState.name).toBe(apiState.name); 58 | expect(parsedState.stateTypeId).toBe(apiState.state_type_id); 59 | expect(parsedState.nextStateId).toBe(apiState.next_state_id); 60 | expect(parsedState.ignoreEscalation).toBe(apiState.ignore_escalation); 61 | expect(parsedState.active).toBe(apiState.active); 62 | expect(parsedState.note).toBe(apiState.note); 63 | expect(parsedState.updatedAt).toBe(apiState.updated_at); 64 | expect(parsedState.createdAt).toBe(apiState.created_at); 65 | } 66 | 67 | test("ticket state list get", async () => { 68 | let randomApiStates = Array(Math.floor(Math.random() * 9) + 1); 69 | let requestMade = false; 70 | 71 | for (i = 0; i < randomApiStates.length; i++) { 72 | randomApiStates[i] = createRandomTicketState(); 73 | } 74 | 75 | ep.createEndpoint( 76 | DummyEndpointProvider.Method.GET, 77 | "/ticket_states", 78 | randomApiStates, 79 | (req) => { 80 | requestMade = true; 81 | } 82 | ); 83 | 84 | let response = await TicketState.getAll(api); 85 | let checkedObjects = 0; 86 | 87 | for (i = 0; i < response.length; i++) { 88 | //iterate over all users, search for matching json object 89 | randomApiStates.forEach((obj) => { 90 | if (obj.id == response[i].id) { 91 | checkIfStateMatchesParsed(obj, response[i]); 92 | checkedObjects++; 93 | } 94 | }); 95 | } 96 | 97 | expect(checkedObjects).toBe(randomApiStates.length); 98 | expect(requestMade).toBe(true); 99 | }); 100 | 101 | test("show state details", async () => { 102 | let state = createRandomTicketState(); 103 | let requestMade = false; 104 | 105 | ep.createEndpoint( 106 | DummyEndpointProvider.Method.GET, 107 | "/ticket_states/" + state.id, 108 | state, 109 | (req) => { 110 | requestMade = true; 111 | } 112 | ); 113 | 114 | let response = await TicketState.getById(api, state.id); 115 | 116 | checkIfStateMatchesParsed(state, response); 117 | expect(requestMade).toBe(true); 118 | }); 119 | 120 | test("ticket state create", () => { 121 | expect(TicketState.create()).rejects.toEqual(new ApiError.Unimplemented()); 122 | }); 123 | 124 | test("ticket state update", () => { 125 | expect( 126 | TicketState.fromApiObject(createRandomTicketState()).update() 127 | ).rejects.toEqual(new ApiError.Unimplemented()); 128 | }); 129 | 130 | test("ticket state delete", () => { 131 | expect( 132 | TicketState.fromApiObject(createRandomTicketState()).delete() 133 | ).rejects.toEqual(new ApiError.Unimplemented()); 134 | }); 135 | 136 | afterAll(() => { 137 | ep.closeServer(); 138 | }); 139 | -------------------------------------------------------------------------------- /test/User.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | const { DummyEndpointProvider } = require("./utility/DummyEndpointProvider"); 6 | const DataSeeder = require("./utility/DataSeeder"); 7 | 8 | const ZammadApi = require("../src/ZammadApi"); 9 | const User = require("../src/User"); 10 | 11 | let api; 12 | let ep; 13 | 14 | beforeAll(() => { 15 | ep = new DummyEndpointProvider("/api/v1"); 16 | api = new ZammadApi(`http://localhost:${ep.port}`); 17 | }); 18 | 19 | /** 20 | * Create a user json object with random data 21 | */ 22 | function createRandomUser() { 23 | const id = DataSeeder.randomId(); 24 | const firstname = DataSeeder.randomName(); 25 | const lastname = DataSeeder.randomName(); 26 | const email = DataSeeder.randomMail(); 27 | const note = DataSeeder.randomString(30); 28 | const updatedAt = DataSeeder.randomIsoTimestamp(); 29 | const createdAt = DataSeeder.randomIsoTimestamp(); 30 | 31 | return { 32 | id, 33 | firstname, 34 | lastname, 35 | email, 36 | note, 37 | updated_at: updatedAt, 38 | created_at: createdAt, 39 | }; 40 | } 41 | 42 | /** 43 | * Check if api user object matches parsed user 44 | * @param {object} apiUser json user api object 45 | * @param {User} parsedUser user parsed by the implementation 46 | */ 47 | function checkIfApiUserMatchesParsed(apiUser, parsedUser) { 48 | expect(typeof parsedUser).toBe("object"); 49 | expect(typeof apiUser).toBe("object"); 50 | 51 | expect(parsedUser.id).toBe(apiUser.id); 52 | expect(parsedUser.firstname).toBe(apiUser.firstname); 53 | expect(parsedUser.lastname).toBe(apiUser.lastname); 54 | expect(parsedUser.email).toBe(apiUser.email); 55 | expect(parsedUser.note).toBe(apiUser.note); 56 | expect(parsedUser.updatedAt).toBe(apiUser.updated_at); 57 | expect(parsedUser.createdAt).toBe(apiUser.created_at); 58 | } 59 | 60 | test("authenticated user get", async () => { 61 | let randomApiUser = createRandomUser(); 62 | let requestMade = false; 63 | 64 | ep.createEndpoint( 65 | DummyEndpointProvider.Method.GET, 66 | "/users/me", 67 | randomApiUser, 68 | (req) => { 69 | requestMade = true; 70 | } 71 | ); 72 | 73 | let response = await User.getAuthenticated(api); 74 | 75 | checkIfApiUserMatchesParsed(randomApiUser, response); 76 | expect(requestMade).toBe(true); 77 | }); 78 | 79 | test("user list get", async () => { 80 | let randomApiUsers = Array(Math.floor(Math.random() * 9) + 1); 81 | let requestMade = false; 82 | 83 | for (i = 0; i < randomApiUsers.length; i++) { 84 | randomApiUsers[i] = createRandomUser(); 85 | } 86 | 87 | ep.createEndpoint( 88 | DummyEndpointProvider.Method.GET, 89 | "/users", 90 | randomApiUsers, 91 | (req) => { 92 | requestMade = true; 93 | } 94 | ); 95 | 96 | let response = await User.getAll(api); 97 | let checkedObjects = 0; 98 | 99 | for (i = 0; i < response.length; i++) { 100 | //iterate over all users, search for matching json object 101 | randomApiUsers.forEach((obj) => { 102 | if (obj.id == response[i].id) { 103 | checkIfApiUserMatchesParsed(obj, response[i]); 104 | checkedObjects++; 105 | } 106 | }); 107 | } 108 | 109 | expect(checkedObjects).toBe(randomApiUsers.length); 110 | expect(requestMade).toBe(true); 111 | }); 112 | 113 | test("user search", async () => { 114 | let queryString = DataSeeder.randomString(10); 115 | let requestMade = false; 116 | let randomApiUsers = Array(Math.floor(Math.random() * 9) + 1); 117 | 118 | for (i = 0; i < randomApiUsers.length; i++) { 119 | randomApiUsers[i] = createRandomUser(); 120 | } 121 | ep.createEndpoint( 122 | DummyEndpointProvider.Method.GET, 123 | "/users/search", 124 | randomApiUsers, 125 | async (req) => { 126 | //expect to have query string that matches 127 | expect(req.query).toHaveProperty("query", queryString); 128 | requestMade = true; 129 | } 130 | ); 131 | 132 | let response = await User.search(api, queryString); 133 | let checkedObjects = 0; 134 | 135 | for (i = 0; i < response.length; i++) { 136 | //iterate over all users, search for matching json object 137 | randomApiUsers.forEach((obj) => { 138 | if (obj.id == response[i].id) { 139 | checkIfApiUserMatchesParsed(obj, response[i]); 140 | checkedObjects++; 141 | } 142 | }); 143 | } 144 | 145 | expect(checkedObjects).toBe(randomApiUsers.length); 146 | expect(requestMade).toBe(true); 147 | }); 148 | 149 | test("show user details", async () => { 150 | let user = createRandomUser(); 151 | let requestMade = false; 152 | 153 | ep.createEndpoint( 154 | DummyEndpointProvider.Method.GET, 155 | "/users/" + user.id, 156 | user, 157 | (req) => { 158 | requestMade = true; 159 | } 160 | ); 161 | 162 | let response = await User.getById(api, user.id); 163 | 164 | checkIfApiUserMatchesParsed(user, response); 165 | expect(requestMade).toBe(true); 166 | }); 167 | 168 | test("user create", async () => { 169 | let plainUser = createRandomUser(); 170 | let requestMade = false; 171 | let user; 172 | 173 | ep.createEndpoint( 174 | DummyEndpointProvider.Method.POST, 175 | "/users", 176 | plainUser, 177 | async (req) => { 178 | expect(req.body.firstname).toBe(plainUser.firstname); 179 | expect(req.body.lastname).toBe(plainUser.lastname); 180 | expect(req.body.email).toBe(plainUser.email); 181 | requestMade = true; 182 | } 183 | ); 184 | 185 | user = await User.create(api, { 186 | firstname: plainUser.firstname, 187 | lastname: plainUser.lastname, 188 | email: plainUser.email, 189 | }); 190 | 191 | checkIfApiUserMatchesParsed(plainUser, user); 192 | expect(requestMade).toBe(true); 193 | }); 194 | 195 | test("user update", async () => { 196 | let plainUser = createRandomUser(); 197 | let user = User.fromApiObject(plainUser); 198 | let requestMade = false; 199 | 200 | ep.createEndpoint( 201 | DummyEndpointProvider.Method.PUT, 202 | "/users/" + plainUser.id, 203 | plainUser, 204 | async (req) => { 205 | checkIfApiUserMatchesParsed(user, req.body); 206 | requestMade = true; 207 | } 208 | ); 209 | 210 | await user.update(api); 211 | expect(requestMade).toBe(true); 212 | }); 213 | 214 | test("user delete", async () => { 215 | let plainUser = createRandomUser(); 216 | let user = User.fromApiObject(plainUser); 217 | let requestMade = false; 218 | 219 | ep.createEndpoint( 220 | DummyEndpointProvider.Method.DELETE, 221 | "/users/" + plainUser.id, 222 | {}, 223 | (req) => { 224 | requestMade = true; 225 | } 226 | ); 227 | await user.delete(api); 228 | expect(requestMade).toBe(true); 229 | }); 230 | 231 | afterAll(() => { 232 | ep.closeServer(); 233 | }); 234 | -------------------------------------------------------------------------------- /test/utility/DataSeeder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create random data for testing utilities and APIs 3 | * @author Peter Kappelt 4 | */ 5 | 6 | 7 | const randomstring = require("randomstring"); 8 | 9 | class DataSeeder { 10 | /** 11 | * Returns a random integer that imitates an id 12 | */ 13 | static randomId() { 14 | return Math.floor(Math.random() * 10000); 15 | } 16 | 17 | /** 18 | * Generate a random character string 19 | * @param {*} length Length of string to be generated 20 | */ 21 | static randomString(length){ 22 | return randomstring.generate(length); 23 | } 24 | 25 | /** 26 | * Generate a random string imitating a name 27 | */ 28 | static randomName(){ 29 | return DataSeeder.randomString(10); 30 | } 31 | 32 | /** 33 | * Generate a random string imitating a mail address 34 | */ 35 | static randomMail(){ 36 | return DataSeeder.randomString(10) + "@" + DataSeeder.randomString(7) + ".com"; 37 | } 38 | 39 | /** 40 | * generate a random bool (true/ false) 41 | */ 42 | static randomBool(){ 43 | return (Math.random() > 0.5) ? true:false; 44 | } 45 | 46 | /** 47 | * generate a random iso formatted timestamp 48 | */ 49 | static randomIsoTimestamp(){ 50 | let randomTimestamp = Math.random() * (new Date().getTime()); 51 | let randomDate = new Date(); 52 | randomDate.setTime(randomTimestamp); 53 | return randomDate.toISOString(); 54 | } 55 | } 56 | 57 | module.exports = DataSeeder; 58 | -------------------------------------------------------------------------------- /test/utility/DummyEndpointProvider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides an http server with dummy endpoints that imitate a 3 | * Zammad instance for testing purposes 4 | * @author Peter Kappelt 5 | */ 6 | 7 | const express = require("express"); 8 | 9 | const PORT_MIN = 3000; 10 | const PORT_MAX = 4000; 11 | 12 | class DummyEndpointProvider { 13 | /** 14 | * Create a new dummy endpoint provider on a random port 15 | * @param {*} prefix Prefix path for all APIs, without leading slash! 16 | */ 17 | constructor(prefix) { 18 | this.port = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN)); 19 | this.prefix = prefix; 20 | this.endpoints = {}; 21 | 22 | this.app = express(); 23 | this.app.use(express.json()); 24 | 25 | this.server = this.app.listen(this.port, () => { 26 | console.log( 27 | `[Test.DummyEndpointProvider] Started dummy endpoint http provider on port ${this.port}` 28 | ); 29 | }); 30 | } 31 | 32 | /** 33 | * End the http server process 34 | */ 35 | closeServer(){ 36 | this.server.close(); 37 | } 38 | 39 | /** 40 | * Enum definition for http methods 41 | */ 42 | static get Method() { 43 | return { 44 | ALL: "all", 45 | GET: "get", 46 | POST: "post", 47 | PUT: "put", 48 | DELETE: "delete", 49 | }; 50 | } 51 | 52 | /** 53 | * Create a new endpoint that shall listen and send dummy data 54 | * @param {DummyEndpointProvider.Method} method HTTP method 55 | * @param {*} path Endpoint path without prefix that might have been specified 56 | * @param {*} data Data to send on this endpoint 57 | * @return Endpoint object for further modification/ handling 58 | */ 59 | createEndpoint(method, path, data, callback = null) { 60 | let endpoint = new DummyEndpoint( 61 | this.app, 62 | method, 63 | this.prefix + path, 64 | data, 65 | callback 66 | ); 67 | 68 | if (!this.endpoints[path]) { 69 | this.endpoints[path] = {}; 70 | } 71 | this.endpoints[path][method] = endpoint; 72 | 73 | return endpoint; 74 | } 75 | } 76 | 77 | class DummyEndpoint { 78 | /** 79 | * Create a new dummy endpoint. 80 | * Call "createEndpoint" on DummyEndpointProvider, rather than 81 | * instantiating this class yourself 82 | * @param {*} app Instance of running express app server 83 | * @param {*} method HTTP method to use 84 | * @param {*} path path to fulfill, e.g. "/api/v1/users" 85 | * @param {*} data Data to rsend 86 | * @param {*} callback Callback function on request to endpoint 87 | */ 88 | constructor(app, method, path, data, callback = null) { 89 | this.method = method; 90 | this.path = path; 91 | this.data = data; 92 | this.callback = callback; 93 | 94 | let appCallback = (req, res) => { 95 | if(callback){ 96 | callback(req); 97 | } 98 | return res.send(this.data); 99 | }; 100 | 101 | switch (method) { 102 | case DummyEndpointProvider.Method.ALL: 103 | app.all(path, appCallback); 104 | break; 105 | case DummyEndpointProvider.Method.DELETE: 106 | app.delete(path, appCallback); 107 | break; 108 | case DummyEndpointProvider.Method.GET: 109 | app.get(path, appCallback); 110 | break; 111 | case DummyEndpointProvider.Method.POST: 112 | app.post(path, appCallback); 113 | break; 114 | case DummyEndpointProvider.Method.PUT: 115 | app.put(path, appCallback); 116 | break; 117 | default: 118 | throw new Error( 119 | `[Test.DummyEndpoint] Invalid method requested: "${method}"` 120 | ); 121 | } 122 | } 123 | } 124 | 125 | module.exports = { 126 | DummyEndpointProvider, 127 | DummyEndpoint, 128 | }; 129 | --------------------------------------------------------------------------------