├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS └── workflows │ ├── CI-pipeline.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── VERSION ├── __tests__ ├── client-id.spec.js ├── config.spec.js ├── helpers.spec.js ├── publicReportingAPI.spec.js ├── report-portal-client.spec.js ├── rest.spec.js └── statistics.spec.js ├── jest.config.js ├── lib ├── commons │ ├── config.js │ └── errors.js ├── constants │ ├── events.js │ ├── outputs.js │ └── statuses.js ├── helpers.js ├── logger.js ├── publicReportingAPI.js ├── report-portal-client.js └── rest.js ├── package-lock.json ├── package.json ├── statistics ├── client-id.js ├── constants.js └── statistics.js ├── tsconfig.eslint.json ├── tsconfig.json └── version_fragment /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | __tests__/**/*.json 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:prettier/recommended" 6 | ], 7 | "plugins": ["prettier", "import"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "project": "./tsconfig.eslint.json" 13 | }, 14 | "globals": { 15 | "expectAsync": true 16 | }, 17 | "env": { 18 | "node": true, 19 | "es6": true, 20 | "jest": true 21 | }, 22 | "rules": { 23 | "valid-jsdoc": ["error", { "requireReturn": false }], 24 | "consistent-return": 0, 25 | "@typescript-eslint/no-plusplus": 0, 26 | "prettier/prettier": 2, 27 | "class-methods-use-this": 0, 28 | "no-else-return": 0, 29 | "@typescript-eslint/lines-between-class-members": 0, 30 | "import/no-extraneous-dependencies": 0, 31 | "func-names": 0, 32 | "import/prefer-default-export": 0, 33 | "@typescript-eslint/naming-convention": 0, 34 | "@typescript-eslint/dot-notation": 0, 35 | "@typescript-eslint/ban-ts-comment": 0, 36 | "@typescript-eslint/no-var-requires": 0, 37 | "@typescript-eslint/no-floating-promises": 2, 38 | "max-classes-per-file": 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AmsterGet 2 | -------------------------------------------------------------------------------- /.github/workflows/CI-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: CI-pipeline 15 | 16 | on: 17 | push: 18 | branches: 19 | - develop 20 | - '!master' 21 | paths-ignore: 22 | - README.md 23 | - CHANGELOG.md 24 | pull_request: 25 | branches: 26 | - develop 27 | - master 28 | paths-ignore: 29 | - README.md 30 | - CHANGELOG.md 31 | 32 | jobs: 33 | test: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | node: [14, 16, 18, 20, 22] 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Setup Node.js 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{ matrix.node }} 46 | 47 | - name: Install dependencies 48 | run: npm install 49 | 50 | - name: Build the source code 51 | run: npm run build 52 | 53 | - name: Run lint 54 | run: npm run lint 55 | 56 | - name: Run tests and check coverage 57 | run: npm run test:coverage 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: publish 15 | 16 | on: 17 | repository_dispatch: 18 | types: [version-released] 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | - name: Set up Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | - name: Install dependencies 31 | run: npm install 32 | - name: Run lint 33 | run: npm run lint 34 | - name: Run tests and check coverage 35 | run: npm run test:coverage 36 | 37 | publish-to-npm-and-gpr: 38 | needs: build 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | - name: Set up Node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 20 47 | registry-url: 'https://registry.npmjs.org' 48 | - name: Install dependencies 49 | run: npm install 50 | - name: Publish to NPM 51 | run: | 52 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 53 | npm config list 54 | npm publish --access public 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 57 | - name: Set up Node.js 58 | uses: actions/setup-node@v4 59 | with: 60 | node-version: 20 61 | registry-url: 'https://npm.pkg.github.com' 62 | scope: '@reportportal' 63 | - name: Publish to GPR 64 | run: | 65 | npm config set //npm.pkg.github.com/:_authToken=$NODE_AUTH_TOKEN 66 | npm config set scope '@reportportal' 67 | npm config list 68 | npm publish 69 | env: 70 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 EPAM Systems 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | name: release 15 | 16 | on: 17 | push: 18 | branches: 19 | - master 20 | paths-ignore: 21 | - '.github/**' 22 | - README.md 23 | - CHANGELOG.md 24 | 25 | env: 26 | versionFileName: 'VERSION' 27 | versionFragmentFileName: 'version_fragment' 28 | changelogFileName: 'CHANGELOG.md' 29 | jobs: 30 | calculate-version: 31 | runs-on: ubuntu-latest 32 | outputs: 33 | releaseVersion: ${{ steps.exposeVersion.outputs.releaseVersion }} 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | - name: Read version 38 | id: readVersion 39 | run: | 40 | read -r version < ${{ env.versionFileName }} 41 | echo "Snapshot version: $version"; 42 | version=$(echo $version | sed 's/-SNAPSHOT//'); 43 | echo $version; 44 | echo "::set-output name=version::$version" 45 | read -r versionFragment < ${{ env.versionFragmentFileName }} 46 | echo $versionFragment 47 | if [[ "$versionFragment" == "minor" ]]; then 48 | versionFragment=feature 49 | echo "Minor version will be used" 50 | elif [[ "$versionFragment" == "major" ]]; then 51 | echo "Major version will be used" 52 | else 53 | versionFragment=patch 54 | echo "Patch version will be used" 55 | fi 56 | echo "::set-output name=versionFragment::$versionFragment" 57 | - name: Bump release version if needed according to version fragment 58 | if: steps.readVersion.outputs.versionFragment != 'patch' 59 | id: bumpVersion 60 | uses: christian-draeger/increment-semantic-version@1.0.1 61 | with: 62 | current-version: ${{ steps.readVersion.outputs.version }} 63 | version-fragment: ${{ steps.readVersion.outputs.versionFragment }} 64 | - name: Expose release version 65 | id: exposeVersion 66 | run: | 67 | versionFragment=${{ steps.readVersion.outputs.versionFragment }} 68 | if [[ "$versionFragment" != "patch" ]]; then 69 | echo "::set-output name=releaseVersion::${{ steps.bumpVersion.outputs.next-version }}" 70 | else 71 | echo "::set-output name=releaseVersion::${{ steps.readVersion.outputs.version }}" 72 | fi 73 | 74 | create-tag: 75 | needs: calculate-version 76 | runs-on: ubuntu-latest 77 | outputs: 78 | versionInfo: ${{ steps.readChangelogEntry.outputs.log_entry }} 79 | steps: 80 | - name: Checkout repository 81 | uses: actions/checkout@v4 82 | - name: Setup NodeJS 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: '12' 86 | - name: Configure git 87 | run: | 88 | git config --global user.email "reportportal.io" 89 | git config --global user.name "reportportal.io" 90 | git remote set-url origin https://${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} 91 | - name: Update VERSION file 92 | run: | 93 | echo "${{ needs.calculate-version.outputs.releaseVersion }}" > ${{ env.versionFileName }} 94 | git status 95 | git add ${{ env.versionFileName }} 96 | git commit -m "Update VERSION file with ${{ needs.calculate-version.outputs.releaseVersion }}" 97 | - name: Create tag 98 | run: | 99 | git tag -a v${{ needs.calculate-version.outputs.releaseVersion }} -m ${{ needs.calculate-version.outputs.releaseVersion }} 100 | npm version from-git 101 | git push origin master 102 | - name: Update version in changelog file 103 | run: | 104 | releaseDate=$(date +'%Y-%m-%d') 105 | echo "Release date: $releaseDate" 106 | versionInfo="## [${{ needs.calculate-version.outputs.releaseVersion }}] - $releaseDate" 107 | sed -i '1s/^/\n'"$versionInfo"'\n/' ${{ env.changelogFileName }} 108 | git status 109 | git add ${{ env.changelogFileName }} 110 | git commit -m "Mention ${{ needs.calculate-version.outputs.releaseVersion }} version in changelog file" 111 | git push origin master 112 | - name: Read changelog Entry 113 | id: readChangelogEntry 114 | uses: mindsers/changelog-reader-action@v1.1.0 115 | with: 116 | version: ${{ needs.calculate-version.outputs.releaseVersion }} 117 | path: ./${{ env.changelogFileName }} 118 | - name: Bump snapshot version 119 | id: bumpSnapshotVersion 120 | uses: christian-draeger/increment-semantic-version@1.0.1 121 | with: 122 | current-version: ${{ needs.calculate-version.outputs.releaseVersion }} 123 | version-fragment: 'bug' 124 | - name: Update develop with snapshot version 125 | run: | 126 | git fetch 127 | git checkout develop 128 | git merge master -Xtheirs --allow-unrelated-histories 129 | echo "${{ steps.bumpSnapshotVersion.outputs.next-version }}-SNAPSHOT" > ${{ env.versionFileName }} 130 | echo "patch" > ${{ env.versionFragmentFileName }} 131 | git status 132 | git add ${{ env.versionFileName }} 133 | git add ${{ env.versionFragmentFileName }} 134 | git commit -m "${{ needs.calculate-version.outputs.releaseVersion }} -> ${{ steps.bumpSnapshotVersion.outputs.next-version }}-SNAPSHOT" 135 | git push origin develop 136 | 137 | create-release: 138 | needs: [calculate-version, create-tag] 139 | runs-on: ubuntu-latest 140 | steps: 141 | - name: Checkout repository 142 | uses: actions/checkout@v4 143 | - name: Create Release 144 | id: createRelease 145 | uses: actions/create-release@v1 146 | env: 147 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 148 | with: 149 | tag_name: v${{ needs.calculate-version.outputs.releaseVersion }} 150 | release_name: Release v${{ needs.calculate-version.outputs.releaseVersion }} 151 | body: ${{ needs.create-tag.outputs.versionInfo }} 152 | draft: false 153 | prerelease: false 154 | - name: Trigger the publish workflow 155 | if: success() 156 | uses: peter-evans/repository-dispatch@v1 157 | with: 158 | token: ${{ secrets.GITHUB_TOKEN }} 159 | event-type: version-released 160 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .log 4 | coverage.lcov 5 | .nyc_output 6 | coverage/ 7 | .npmrc 8 | build 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [5.4.0] - 2025-03-27 3 | ### Changed 4 | - Revert time format back to milliseconds (based on [#217](https://github.com/reportportal/client-javascript/issues/217#issuecomment-2659843471)). 5 | This is also fixing the issue with agent installation on ARM processors. 6 | ### Security 7 | - Updated versions of vulnerable packages (axios). 8 | 9 | ## [5.3.1] - 2025-01-31 10 | ### Fixed 11 | - Updated the `checkConnect` method implementation to utilize the `GET launches` API endpoint, ensuring compatibility with both old and new API versions. 12 | 13 | ## [5.3.0] - 2024-09-23 14 | ### Changed 15 | - The client now expects reporting the time for launches, test items and logs with microsecond precision in the ISO string format. 16 | Thus, the `helpers.now` function is adjusted accordingly. Details about [supported](./README.md#time-format) formats. 17 | For logs, microsecond precision is available on the UI from ReportPortal version 24.2. 18 | ### Security 19 | - Updated versions of vulnerable packages (micromatch). 20 | 21 | ## [5.2.0] - 2024-09-17 22 | ### Changed 23 | - **Breaking change** Drop support of Node.js 12. The version [5.1.4](https://github.com/reportportal/client-javascript/releases/tag/v5.1.4) is the latest that supports it. 24 | - The client now creates an instance of the `axios` HTTP client in the constructor. 25 | - The `HOST` HTTP header is added to all requests as it was skipped by the HTTP client. 26 | ### Fixed 27 | - Allow using `restClientConfig` in `checkConnect()` method. Thanks to [stevez](https://github.com/stevez). 28 | ### Security 29 | - Updated versions of vulnerable packages (braces). 30 | 31 | ## [5.1.4] - 2024-05-22 32 | ### Fixed 33 | - Use correct launch search URL based on config mode while merging launches. Resolves [#200](https://github.com/reportportal/client-javascript/issues/200). Thanks to [hoangthanhtri](https://github.com/hoangthanhtri). 34 | - Print launch UUID after merging launches. Resolves [#202](https://github.com/reportportal/client-javascript/issues/202). Thanks to [hoangthanhtri](https://github.com/hoangthanhtri). 35 | 36 | ## [5.1.3] - 2024-04-11 37 | ### Added 38 | - Output launch UUID to file and ENV variable, thanks to [artsiomBandarenka](https://github.com/artsiomBandarenka). Addressed [#195](https://github.com/reportportal/client-javascript/issues/195), [#50](https://github.com/reportportal/agent-js-webdriverio/issues/50). 39 | ### Security 40 | - Updated versions of vulnerable packages (follow-redirects). 41 | 42 | ## [5.1.2] - 2024-02-20 43 | ### Fixed 44 | - Execution sequence for retried tests [#134](https://github.com/reportportal/agent-js-playwright/issues/134). 45 | 46 | ## [5.1.1] - 2024-01-23 47 | ### Added 48 | - Debug logs for RestClient. 49 | 50 | ## [5.1.0] - 2024-01-19 51 | ### Changed 52 | - **Breaking change** Drop support of Node.js 10. The version [5.0.15](https://github.com/reportportal/client-javascript/releases/tag/v5.0.15) is the latest that supports it. 53 | ### Security 54 | - Updated versions of vulnerable packages (axios, follow-redirects). 55 | ### Deprecated 56 | - Node.js 12 usage. This minor version is the latest that supports Node.js 12. 57 | 58 | ## [5.0.15] - 2023-11-20 59 | ### Added 60 | - Logging link to the launch on its finish. 61 | ### Deprecated 62 | - Node.js 10 usage. This version is the latest that supports Node.js 10. 63 | 64 | ## [5.0.14] - 2023-10-05 65 | ### Added 66 | - `Promise.allSettled` polyfill to support NodeJS 10. 67 | ### Fixed 68 | - Reporting is down on error with collect request on reporting start. 69 | - Can not read property `realId` of undefined during reporting, resolves [#99](https://github.com/reportportal/agent-js-playwright/issues/99). 70 | 71 | ## [5.0.13] - 2023-08-28 72 | ### Added 73 | - `launchUuidPrint` and `launchUuidPrintOutput` configuration options to ease integration with CI tools, by @HardNorth. 74 | 75 | ## [5.0.12] - 2023-06-19 76 | ### Changed 77 | - `token` configuration option was renamed to `apiKey` to maintain common convention. 78 | 79 | ## [5.0.11] - 2023-06-01 80 | ### Fixed 81 | - Request body logging has been removed by default for the HTTP client. 82 | 83 | ## [5.0.10] - 2023-04-30 84 | ### Fixed 85 | - Node.js old versions support (client still supports Node.js >=10). 86 | 87 | ## [5.0.9] - 2023-04-29 88 | ### Changed 89 | - A lot of package dependencies have been updated. 90 | - Statistics engine updated on new contract. 91 | 92 | ## [5.0.8] - 2023-01-24 93 | ### Added 94 | - `mergeOptions` parameter to `mergeLaunches` method. 95 | - Dynamic page size for launch merge based on launches count, resolves [#86](https://github.com/reportportal/client-javascript/issues/86). 96 | ### Security 97 | - Updated versions of vulnerable packages (json5, minimist). 98 | 99 | ## [5.0.7] - 2022-12-26 100 | ### Added 101 | - The ability to see verbose logs in debug mode. To enable the debug mode, the `debug: true` flag should be specified in `params`. 102 | ### Security 103 | - Updated versions of vulnerable packages (ajv, qs, follow-redirects, minimatch). 104 | 105 | ## [5.0.6] - 2022-01-13 106 | ### Fixed 107 | - Security vulnerabilities (axios, path-parse, minimist) 108 | ### Changed 109 | - Package size reduced 110 | 111 | ## [5.0.5] - 2021-05-25 112 | ### Added 113 | - Possibility to change Axios Requests timeout (closes [#115](https://github.com/reportportal/client-javascript/issues/115)) 114 | ### Changed 115 | - Default timeout on Axios Requests increased to 30000ms 116 | ### Fixed 117 | - [Issue](https://github.com/reportportal/client-javascript/issues/102) with self-signed certificate 118 | - Security vulnerabilities (lodash, handlebars, hosted-git-info) 119 | 120 | ## [5.0.4] - 2021-04-29 121 | ### Added 122 | - Timeout (default is 5000ms) on Axios Requests 123 | ### Fixed 124 | - Security vulnerabilities ([axios](https://github.com/reportportal/client-javascript/issues/109)) 125 | - [Issue](https://github.com/reportportal/client-javascript/issues/94) with the finish of a non-existent test item 126 | 127 | ## [5.0.3] - 2020-11-09 128 | ### Added 129 | - Environment variable 130 | 131 | ## [5.0.2] - 2020-08-27 132 | ### Fixed 133 | - [Issue](https://github.com/reportportal/client-javascript/pull/91) with request headers 134 | - Default status for suite/test items without children 135 | 136 | ## [5.0.1] - 2020-08-14 137 | ### Changed 138 | - Packages publishing workflow improved 139 | ### Added 140 | - The ability to disable google analytics 141 | 142 | ## [5.0.0] - 2020-06-09 143 | ### Added 144 | - Full compatibility with ReportPortal version 5.* (see [reportportal releases](https://github.com/reportportal/reportportal/releases)) 145 | ### Deprecated 146 | - Previous package version (`reportportal-client`) will no longer supported by reportportal.io 147 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReportPortal js client 2 | 3 | This Client is to communicate with the ReportPortal on Node.js. 4 | 5 | Library is used only for implementors of custom listeners for ReportPortal. 6 | 7 | ## Already implemented listeners: 8 | 9 | * [Playwright integration](https://github.com/reportportal/agent-js-playwright) 10 | * [Cypress integration](https://github.com/reportportal/agent-js-cypress) 11 | * [Jest integration](https://github.com/reportportal/agent-js-jest) 12 | * [Mocha integration](https://github.com/reportportal/agent-js-mocha) 13 | * [Webdriverio integration](https://github.com/reportportal/agent-js-webdriverio) 14 | * [Postman integration](https://github.com/reportportal/agent-js-postman) 15 | * [Cucumber integration](https://github.com/reportportal/agent-js-cucumber) 16 | * [Vitest integration](https://github.com/reportportal/agent-js-vitest) 17 | * [Jasmine integration](https://github.com/reportportal/agent-js-jasmine) 18 | * [TestCafe integration](https://github.com/reportportal/agent-js-testcafe) 19 | * [Codecept integration](https://github.com/reportportal/agent-js-codecept) 20 | * [Nightwatch integration](https://github.com/reportportal/agent-js-nightwatch) 21 | 22 | Examples for test framework integrations from the list above described in [examples](https://github.com/reportportal/examples-js) repository. 23 | 24 | ## Installation 25 | 26 | The latest version is available on npm: 27 | ```cmd 28 | npm install @reportportal/client-javascript 29 | ``` 30 | 31 | ## Usage example 32 | 33 | ```javascript 34 | const RPClient = require('@reportportal/client-javascript'); 35 | 36 | const rpClient = new RPClient({ 37 | apiKey: 'reportportalApiKey', 38 | endpoint: 'http://your-instance.com:8080/api/v1', 39 | launch: 'LAUNCH_NAME', 40 | project: 'PROJECT_NAME' 41 | }); 42 | 43 | rpClient.checkConnect().then(() => { 44 | console.log('You have successfully connected to the server.'); 45 | }, (error) => { 46 | console.log('Error connection to server'); 47 | console.dir(error); 48 | }); 49 | ``` 50 | 51 | ## Configuration 52 | 53 | When creating a client instance, you need to specify the following options: 54 | 55 | | Option | Necessity | Default | Description | 56 | |-----------------------|------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 57 | | apiKey | Required | | User's reportportal token from which you want to send requests. It can be found on the profile page of this user. | 58 | | endpoint | Required | | URL of your server. For example, if you visit the page at 'https://server:8080/ui', then endpoint will be equal to 'https://server:8080/api/v1'. | 59 | | launch | Required | | Name of the launch at creation. | 60 | | project | Required | | The name of the project in which the launches will be created. | 61 | | headers | Optional | {} | The object with custom headers for internal http client. | 62 | | debug | Optional | false | This flag allows seeing the logs of the client. Useful for debugging. | 63 | | isLaunchMergeRequired | Optional | false | Allows client to merge launches into one at the end of the run via saving their UUIDs to the temp files at filesystem. At the end of the run launches can be merged using `mergeLaunches` method. Temp file format: `rplaunch-${launch_uuid}.tmp`. | 64 | | restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. | 65 | | launchUuidPrint | Optional | false | Whether to print the current launch UUID. | 66 | | launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR', 'FILE', 'ENVIRONMENT'. Works only if `launchUuidPrint` set to `true`. File format: `rp-launch-uuid-${launch_uuid}.tmp`. Env variable: `RP_LAUNCH_UUID`. | 67 | | token | Deprecated | Not set | Use `apiKey` instead. | 68 | 69 | ## Asynchronous reporting 70 | 71 | The client supports an asynchronous reporting (via the ReportPortal asynchronous API). 72 | If you want the client to report through the asynchronous API, change `v1` to `v2` in the `endpoint` address. 73 | 74 | ## API 75 | 76 | Each method (except checkConnect) returns an object in a specific format: 77 | ```javascript 78 | { 79 | tempId: '4ds43fs', // generated by the client id for further work with the created item 80 | promise: Promise // An object indicating the completion of an operation 81 | } 82 | ``` 83 | The client works synchronously, so it is not necessary to wait for the end of the previous requests to send following ones. 84 | 85 | ### Timeout (30000ms) on axios requests 86 | 87 | There is a timeout on axios requests. If for instance the server your making a request to is taking too long to load, then axios timeout will work and you will see the error 'Error: timeout of 30000ms exceeded'. 88 | 89 | You can simply change this timeout by adding a `timeout` property to `restClientConfig` with your desired numeric value (in _ms_) or *0* to disable it. 90 | 91 | ### checkConnect 92 | 93 | `checkConnect` - asynchronous method for verifying the correctness of the client connection 94 | 95 | ```javascript 96 | rpClient.checkConnect().then((response) => { 97 | console.log('You have successfully connected to the server.'); 98 | console.log(`You are using an account: ${response.fullName}`); 99 | }, (error) => { 100 | console.log('Error connection to server'); 101 | console.dir(error); 102 | }); 103 | ``` 104 | 105 | ### startLaunch 106 | 107 | `startLaunch` - starts a new launch, return temp id that you want to use for the all items within this launch. 108 | 109 | ```javascript 110 | const launchObj = rpClient.startLaunch({ 111 | name: 'Client test', 112 | startTime: rpClient.helpers.now(), 113 | description: 'description of the launch', 114 | attributes: [ 115 | { 116 | 'key': 'yourKey', 117 | 'value': 'yourValue' 118 | }, 119 | { 120 | 'value': 'yourValue' 121 | } 122 | ], 123 | //this param used only when you need client to send data into the existing launch 124 | id: 'id' 125 | }); 126 | console.log(launchObj.tempId); 127 | ``` 128 | The method takes one argument: 129 | * launch data object: 130 | 131 | | Option | Necessity | Default | Description | 132 | |-------------|-----------|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| 133 | | startTime | Optional | rpClient.helpers.now() | Start time of the launch (unix time). | 134 | | name | Optional | parameter 'launch' specified when creating the client instance | Name of the launch. | 135 | | mode | Optional | 'DEFAULT' | 'DEFAULT' - results will be submitted to Launches page, 'DEBUG' - results will be submitted to Debug page. | 136 | | description | Optional | '' | Description of the launch (supports markdown syntax). | 137 | | attributes | Optional | [] | Array of launch attributes (tags). | 138 | | id | Optional | Not set | `ID` of the existing launch in which tests data would be sent, without this param new launch instance will be created. | 139 | 140 | To get the real launch `ID` (also known as launch `UUID` from database) wait for the returned promise to finish. 141 | 142 | ```javascript 143 | const launchObj = rpClient.startLaunch(); 144 | launchObj.promise.then((response) => { 145 | console.log(`Launch real id: ${response.id}`); 146 | }, (error) => { 147 | console.dir(`Error at the start of launch: ${error}`); 148 | }) 149 | ``` 150 | As system attributes, this method sends the following data (these data are not for public use): 151 | * client name, version; 152 | * agent name, version (if given); 153 | * browser name, version (if given); 154 | * OS type, architecture; 155 | * RAMSize; 156 | * nodeJS version; 157 | 158 | ReportPortal is supporting now integrations with more than 15 test frameworks simultaneously. 159 | In order to define the most popular agents and plan the team workload accordingly, we are using Google Analytics. 160 | 161 | ReportPortal collects only information about agent name, version and version of Node.js. This information is sent to Google Analytics on the launch start. 162 | Please help us to make our work effective. 163 | If you still want to switch Off Google Analytics, please change env variable. 164 | 'REPORTPORTAL_CLIENT_JS_NO_ANALYTICS=true' 165 | 166 | ### finishLaunch 167 | 168 | `finishLaunch` - finish of the launch. After calling this method, you can not add items to the launch. 169 | The request to finish the launch will be sent only after all items within it have finished. 170 | 171 | ```javascript 172 | // launchObj - object returned by method 'startLaunch' 173 | const launchFinishObj = rpClient.finishLaunch(launchObj.tempId, { 174 | endTime: rpClient.helpers.now() 175 | }); 176 | ``` 177 | 178 | The method takes two arguments: 179 | * launch `tempId` (returned by the method `startLaunch`) 180 | * data object: 181 | 182 | |Option | Necessity | Default | Description | 183 | |--------- |-----------|---------|---------------------------------------------------------------------------------------------------| 184 | |endTime | Optional | rpClient.helpers.now() | End time of the launch. | 185 | |status | Optional | '' | Status of launch, one of '', 'PASSED', 'FAILED', 'STOPPED', 'SKIPPED', 'INTERRUPTED', 'CANCELLED'. | 186 | 187 | ### getPromiseFinishAllItems 188 | 189 | `getPromiseFinishAllItems` - returns promise that contains status about all data has been sent to the reportportal. 190 | This method needed when test frameworks don't wait for async methods until finished. 191 | 192 | ```javascript 193 | // jasmine example. tempLaunchId - tempId of the launch started by the current client process 194 | agent.getPromiseFinishAllItems(agent.tempLaunchId).then(() => done()); 195 | ``` 196 | 197 | | Option | Necessity | Default | Description | 198 | |--------------|-----------|---------|----------------------------| 199 | | tempLaunchId | Required | | `tempId` of the launch started by the current client process | 200 | 201 | ### updateLaunch 202 | 203 | `updateLaunch` - updates the launch data. Will send a request to the server only after finishing the launch. 204 | 205 | ```javascript 206 | // launchObj - object returned by method 'startLaunch' 207 | rpClient.updateLaunch( 208 | launchObj.tempId, 209 | { 210 | description: 'new launch description', 211 | attributes: [ 212 | { 213 | key: 'yourKey', 214 | value: 'yourValue' 215 | }, 216 | { 217 | value: 'yourValue' 218 | } 219 | ], 220 | mode: 'DEBUG' 221 | } 222 | ); 223 | ``` 224 | The method takes two arguments: 225 | * launch `tempId` (returned by the method 'startLaunch') 226 | * data object - may contain the following fields: `description`, `attributes`, `mode`. These fields can be looked up in the method `startLaunch`. 227 | 228 | ### startTestItem 229 | 230 | `startTestItem` - starts a new test item. 231 | 232 | ```javascript 233 | // launchObj - object returned by method 'startLaunch' 234 | const suiteObj = rpClient.startTestItem({ 235 | description: makeid(), 236 | name: makeid(), 237 | startTime: rpClient.helpers.now(), 238 | type: 'SUITE' 239 | }, launchObj.tempId); 240 | const stepObj = rpClient.startTestItem({ 241 | description: makeid(), 242 | name: makeid(), 243 | startTime: rpClient.helpers.now(), 244 | attributes: [ 245 | 246 | { 247 | key: 'yourKey', 248 | value: 'yourValue' 249 | }, 250 | { 251 | value: 'yourValue' 252 | } 253 | ], 254 | type: 'STEP' 255 | }, launchObj.tempId, suiteObj.tempId); 256 | 257 | ``` 258 | The method takes three arguments: 259 | * test item data object: 260 | 261 | |Option | Necessity | Default | Description | 262 | |--------- |-----------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 263 | |name | Required | | Test item name | 264 | |type | Required | | Test item type, one of 'SUITE', 'STORY', 'TEST', 'SCENARIO', 'STEP', 'BEFORE_CLASS', 'BEFORE_GROUPS','BEFORE_METHOD', 'BEFORE_SUITE', 'BEFORE_TEST', 'AFTER_CLASS', 'AFTER_GROUPS', 'AFTER_METHOD', 'AFTER_SUITE', 'AFTER_TEST' | 265 | |hasStats | Optional | true | Changes behavior for test item of type 'STEP'. When set to `true`, step is treaten as a test case (entity containig statistics). When false, step becomes a nested step. | 266 | |description | Optional | '' | Description of the test item (supports markdown syntax). | 267 | |startTime | Optional | rpClient.helpers.now() | Start time of the test item (unix time). | 268 | |attributes | Optional | [] | Array of the test item attributes. | 269 | 270 | * launch `tempId` (returned by the method `startLaunch`) 271 | * parent test item `tempId` (*optional*) (returned by method `startTestItem`) 272 | 273 | ### finishTestItem 274 | 275 | `finishTestItem` - finish of the test item. After calling this method, you can not add items to the test item. 276 | The request to finish the test item will be sent only after all test items within it have finished. 277 | 278 | ```javascript 279 | // itemObj - object returned by method 'startTestItem' 280 | rpClient.finishTestItem(itemObj.tempId, { 281 | status: 'failed' 282 | }) 283 | ``` 284 | The method takes two arguments: 285 | * test item `tempId` (returned by the method `startTestItem`) 286 | * data object: 287 | 288 | | Option | Necessity | Default | Description | 289 | |---------|-----------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------| 290 | | issue | Optional | true | Test item issue object. `issueType` is required, allowable values: 'pb***', 'ab***', 'si***', 'ti***', 'nd001'. Where `***` is locator id | 291 | | status | Optional | 'PASSED' | Test item status, one of '', 'PASSED', 'FAILED', 'STOPPED', 'SKIPPED', 'INTERRUPTED', 'CANCELLED'. | 292 | | endTime | Optional | rpClient.helpers.now() | End time of the launch (unix time). | 293 | 294 | Example issue object: 295 | ``` 296 | { 297 | issueType: 'string', 298 | comment: 'string', 299 | externalSystemIssues: [ 300 | { 301 | submitDate: 0, 302 | submitter: 'string', 303 | systemId: 'string', 304 | ticketId: 'string', 305 | url: 'string' 306 | } 307 | ] 308 | } 309 | ``` 310 | 311 | ### sendLog 312 | 313 | `sendLog` - adds a log to the test item. 314 | 315 | ```javascript 316 | // stepObj - object returned by method 'startTestItem' 317 | rpClient.sendLog(stepObj.tempId, { 318 | level: 'INFO', 319 | message: 'User clicks login button', 320 | time: rpClient.helpers.now() 321 | }) 322 | ``` 323 | The method takes three arguments: 324 | * test item `tempId` (returned by method `startTestItem`) 325 | * data object: 326 | 327 | | Option | Necessity | Default | Description | 328 | |---------|-----------|------------------------|----------------------------------------------------------------------| 329 | | message | Optional | '' | The log message. | 330 | | level | Optional | '' | The log level, one of 'trace', 'debug', 'info', 'warn', 'error', ''. | 331 | | time | Optional | rpClient.helpers.now() | The time of the log. | 332 | 333 | * file object (optional): 334 | 335 | | Option | Necessity | Default | Description | 336 | |---------|-----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 337 | | name | Required | | The name of the file. | 338 | | type | Required | | The file mimeType, example 'image/png' (support types: 'image/*', application/['xml', 'javascript', 'json', 'css', 'php'], other formats will be opened in reportportal in a new browser tab only). | 339 | | content | Required | | base64 encoded file content. | 340 | 341 | ### mergeLaunches 342 | 343 | `mergeLaunches` - merges already completed runs into one (useful when running tests in multiple threads on the same machine). 344 | 345 | **Note:** Works only if `isLaunchMergeRequired` option is set to `true`. 346 | 347 | ```javascript 348 | rpClient.mergeLaunches({ 349 | description: 'Regression tests', 350 | attributes: [ 351 | { 352 | key: 'build', 353 | value: '1.0.0' 354 | } 355 | ], 356 | endTime: rpClient.helpers.now(), 357 | extendSuitesDescription: false, 358 | launches: [1, 2, 3], 359 | mergeType: 'BASIC', 360 | mode: 'DEFAULT', 361 | name: 'Launch name', 362 | }) 363 | ``` 364 | The method takes one argument: 365 | 366 | * merge options object (optional): 367 | 368 | | Option | Necessity | Default | Description | 369 | |-------------------------|-----------|-----------------------------------------|------------------------------------------------------------------------------------------------------------| 370 | | description | Optional | config.description or 'Merged launch' | Description of the launch (supports markdown syntax). | 371 | | attributes | Optional | config.attributes or [] | Array of launch attributes (tags). | 372 | | endTime | Optional | rpClient.helpers.now() | End time of the launch (unix time) | 373 | | extendSuitesDescription | Optional | true | Whether to extend suites description or not. | 374 | | launches | Optional | ids of the launches saved to filesystem | The array of the real launch ids, not UUIDs | 375 | | mergeType | Optional | 'BASIC' | The type of the merge operation. Possible values are 'BASIC' or 'DEEP'. | 376 | | mode | Optional | config.mode or 'DEFAULT' | 'DEFAULT' - results will be submitted to Launches page, 'DEBUG' - results will be submitted to Debug page. | 377 | | name | Optional | config.launch or 'Test launch name' | Name of the launch after merge. | 378 | 379 | # Copyright Notice 380 | 381 | Licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 382 | license (see the LICENSE.txt file). 383 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 5.4.1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /__tests__/client-id.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const os = require('os'); 4 | const path = require('path'); 5 | const { v4: uuidv4 } = require('uuid'); 6 | const { getClientId } = require('../statistics/client-id'); 7 | 8 | const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 9 | const clientIdFile = path.join(os.homedir(), '.rp', 'rp.properties'); 10 | 11 | const unlink = util.promisify(fs.unlink); 12 | const readFile = util.promisify(fs.readFile); 13 | const writeFile = util.promisify(fs.writeFile); 14 | 15 | describe('Client ID test suite', () => { 16 | it('getClientId should return the same client ID for two calls', async () => { 17 | const clientId1 = await getClientId(); 18 | const clientId2 = await getClientId(); 19 | 20 | expect(clientId2).toEqual(clientId1); 21 | }); 22 | 23 | it('getClientId should return different client IDs if store file removed', async () => { 24 | const clientId1 = await getClientId(); 25 | await unlink(clientIdFile); 26 | const clientId2 = await getClientId(); 27 | expect(clientId2).not.toEqual(clientId1); 28 | }); 29 | 30 | it('getClientId should return UUID client ID', async () => { 31 | const clientId = await getClientId(); 32 | expect(clientId).toMatch(uuidv4Validation); 33 | }); 34 | 35 | it('getClientId should save client ID to ~/.rp/rp.properties', async () => { 36 | await unlink(clientIdFile); 37 | const clientId = await getClientId(); 38 | const content = await readFile(clientIdFile, 'utf-8'); 39 | expect(content).toMatch(new RegExp(`^client\\.id\\s*=\\s*${clientId}\\s*(?:$|\n)`)); 40 | }); 41 | 42 | it('getClientId should read client ID from ~/.rp/rp.properties', async () => { 43 | await unlink(clientIdFile); 44 | const clientId = uuidv4(undefined, undefined, 0); 45 | await writeFile(clientIdFile, `client.id=${clientId}\n`, 'utf-8'); 46 | expect(await getClientId()).toEqual(clientId); 47 | }); 48 | 49 | it( 50 | 'getClientId should read client ID from ~/.rp/rp.properties if it is not empty and client ID is the ' + 51 | 'first line', 52 | async () => { 53 | await unlink(clientIdFile); 54 | const clientId = uuidv4(undefined, undefined, 0); 55 | await writeFile(clientIdFile, `client.id=${clientId}\ntest.property=555\n`, 'utf-8'); 56 | expect(await getClientId()).toEqual(clientId); 57 | }, 58 | ); 59 | 60 | it( 61 | 'getClientId should read client ID from ~/.rp/rp.properties if it is not empty and client ID is not the ' + 62 | 'first line', 63 | async () => { 64 | await unlink(clientIdFile); 65 | const clientId = uuidv4(undefined, undefined, 0); 66 | await writeFile(clientIdFile, `test.property=555\nclient.id=${clientId}\n`, 'utf-8'); 67 | expect(await getClientId()).toEqual(clientId); 68 | }, 69 | ); 70 | 71 | it('getClientId should write client ID to ~/.rp/rp.properties if it is not empty', async () => { 72 | await unlink(clientIdFile); 73 | await writeFile(clientIdFile, `test.property=555`, 'utf-8'); 74 | const clientId = await getClientId(); 75 | const content = await readFile(clientIdFile, 'utf-8'); 76 | expect(content).toMatch(new RegExp(`(?:^|\n)client\\.id\\s*=\\s*${clientId}\\s*(?:$|\n)`)); 77 | expect(content).toMatch(/(?:^|\n)test\.property\s*=\s*555\s*(?:$|\n)/); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/config.spec.js: -------------------------------------------------------------------------------- 1 | const { getClientConfig, getRequiredOption, getApiKey } = require('../lib/commons/config'); 2 | const { 3 | ReportPortalRequiredOptionError, 4 | ReportPortalValidationError, 5 | } = require('../lib/commons/errors'); 6 | 7 | describe('Config commons test suite', () => { 8 | describe('getRequiredOption', () => { 9 | it('should return option if it presented in options and has value', () => { 10 | const option = getRequiredOption({ project: 1 }, 'project'); 11 | 12 | expect(option).toBe(1); 13 | }); 14 | 15 | it('should throw ReportPortalRequiredOptionError in case of empty option', () => { 16 | let error; 17 | try { 18 | getRequiredOption({ project: undefined }, 'project'); 19 | } catch (e) { 20 | error = e; 21 | } 22 | expect(error).toBeDefined(); 23 | expect(error).toBeInstanceOf(ReportPortalRequiredOptionError); 24 | }); 25 | 26 | it('should throw ReportPortalRequiredOptionError in case of option not present in options', () => { 27 | let error; 28 | try { 29 | getRequiredOption({ other: 1 }, 'project'); 30 | } catch (e) { 31 | error = e; 32 | } 33 | expect(error).toBeDefined(); 34 | expect(error).toBeInstanceOf(ReportPortalRequiredOptionError); 35 | }); 36 | }); 37 | 38 | describe('getApiKey', () => { 39 | it('should return apiKey if it presented in options and has value', () => { 40 | const apiKey = getApiKey({ apiKey: '1' }); 41 | 42 | expect(apiKey).toBe('1'); 43 | }); 44 | 45 | it('should return apiKey if it provided in options via deprecated token option', () => { 46 | const apiKey = getApiKey({ token: '1' }); 47 | 48 | expect(apiKey).toBe('1'); 49 | }); 50 | 51 | it('should print warning to console if deprecated token option used', () => { 52 | jest.spyOn(console, 'warn').mockImplementation(); 53 | 54 | getApiKey({ token: '1' }); 55 | 56 | expect(console.warn).toHaveBeenCalledWith( 57 | `Option 'token' is deprecated. Use 'apiKey' instead.`, 58 | ); 59 | }); 60 | 61 | it('should throw ReportPortalRequiredOptionError in case of no one option present', () => { 62 | let error; 63 | try { 64 | getApiKey({}); 65 | } catch (e) { 66 | error = e; 67 | } 68 | expect(error).toBeDefined(); 69 | expect(error).toBeInstanceOf(ReportPortalRequiredOptionError); 70 | }); 71 | }); 72 | 73 | describe('getClientConfig', () => { 74 | it('should print ReportPortalValidationError error to the console in case of options is not an object type', () => { 75 | jest.spyOn(console, 'dir').mockImplementation(); 76 | getClientConfig('options'); 77 | 78 | expect(console.dir).toHaveBeenCalledWith( 79 | new ReportPortalValidationError('`options` must be an object.'), 80 | ); 81 | }); 82 | 83 | it('should print ReportPortalRequiredOptionError to the console in case of "endpoint" option missed', () => { 84 | jest.spyOn(console, 'dir').mockImplementation(); 85 | getClientConfig({ 86 | apiKey: '123', 87 | project: 'prj', 88 | }); 89 | 90 | expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('endpoint')); 91 | }); 92 | 93 | it('should print ReportPortalRequiredOptionError to the console in case of "project" option missed', () => { 94 | jest.spyOn(console, 'dir').mockImplementation(); 95 | getClientConfig({ 96 | apiKey: '123', 97 | endpoint: 'https://abc.com', 98 | }); 99 | 100 | expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('project')); 101 | }); 102 | 103 | it('should print ReportPortalRequiredOptionError to the console in case of "apiKey" option missed', () => { 104 | jest.spyOn(console, 'dir').mockImplementation(); 105 | getClientConfig({ 106 | project: 'prj', 107 | endpoint: 'https://abc.com', 108 | }); 109 | 110 | expect(console.dir).toHaveBeenCalledWith(new ReportPortalRequiredOptionError('apiKey')); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /__tests__/helpers.spec.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const glob = require('glob'); 4 | const helpers = require('../lib/helpers'); 5 | const pjson = require('../package.json'); 6 | 7 | describe('Helpers', () => { 8 | describe('formatName', () => { 9 | it('slice last 256 symbols', () => { 10 | expect(helpers.formatName(`a${'b'.repeat(256)}`)).toBe('b'.repeat(256)); 11 | }); 12 | it('leave 256 symbol name as is', () => { 13 | expect(helpers.formatName('c'.repeat(256))).toBe('c'.repeat(256)); 14 | }); 15 | it('leave 3 symbol name as is', () => { 16 | expect(helpers.formatName('abc')).toBe('abc'); 17 | }); 18 | it('complete with dots 2 symbol name', () => { 19 | expect(helpers.formatName('ab')).toBe('ab.'); 20 | }); 21 | }); 22 | 23 | describe('now', () => { 24 | it('returns milliseconds from unix time', () => { 25 | expect(new Date() - helpers.now()).toBeLessThan(100); // less than 100 miliseconds difference 26 | }); 27 | }); 28 | 29 | describe('readLaunchesFromFile', () => { 30 | it('should return the right ids', () => { 31 | jest.spyOn(glob, 'sync').mockReturnValue(['rplaunch-fileOne.tmp', 'rplaunch-fileTwo.tmp']); 32 | 33 | const ids = helpers.readLaunchesFromFile(); 34 | 35 | expect(ids).toEqual(['fileOne', 'fileTwo']); 36 | }); 37 | }); 38 | 39 | describe('saveLaunchIdToFile', () => { 40 | it('should call fs.open method with right parameters', () => { 41 | jest.spyOn(fs, 'open').mockImplementation(); 42 | 43 | helpers.saveLaunchIdToFile('fileOne'); 44 | 45 | expect(fs.open).toHaveBeenCalledWith('rplaunch-fileOne.tmp', 'w', expect.any(Function)); 46 | }); 47 | }); 48 | 49 | describe('getSystemAttribute', () => { 50 | it('should return correct system attributes', () => { 51 | jest.spyOn(os, 'type').mockReturnValue('osType'); 52 | jest.spyOn(os, 'arch').mockReturnValue('osArchitecture'); 53 | jest.spyOn(os, 'totalmem').mockReturnValue('1'); 54 | const nodeVersion = process.version; 55 | const expectedAttr = [ 56 | { 57 | key: 'client', 58 | value: `${pjson.name}|${pjson.version}`, 59 | system: true, 60 | }, 61 | { 62 | key: 'os', 63 | value: 'osType|osArchitecture', 64 | system: true, 65 | }, 66 | { 67 | key: 'RAMSize', 68 | value: '1', 69 | system: true, 70 | }, 71 | { 72 | key: 'nodeJS', 73 | value: nodeVersion, 74 | system: true, 75 | }, 76 | ]; 77 | 78 | const attr = helpers.getSystemAttribute(); 79 | 80 | expect(attr).toEqual(expectedAttr); 81 | }); 82 | }); 83 | 84 | describe('generateTestCaseId', () => { 85 | it('should return undefined if there is no codeRef', () => { 86 | const testCaseId = helpers.generateTestCaseId(); 87 | 88 | expect(testCaseId).toEqual(undefined); 89 | }); 90 | 91 | it('should return codeRef if there is no params', () => { 92 | const testCaseId = helpers.generateTestCaseId('codeRef'); 93 | 94 | expect(testCaseId).toEqual('codeRef'); 95 | }); 96 | 97 | it('should return codeRef with parameters if there are all parameters', () => { 98 | const parameters = [ 99 | { 100 | key: 'key', 101 | value: 'value', 102 | }, 103 | { value: 'valueTwo' }, 104 | { key: 'keyTwo' }, 105 | { 106 | key: 'keyThree', 107 | value: 'valueThree', 108 | }, 109 | ]; 110 | 111 | const testCaseId = helpers.generateTestCaseId('codeRef', parameters); 112 | 113 | expect(testCaseId).toEqual('codeRef[value,valueTwo,valueThree]'); 114 | }); 115 | }); 116 | 117 | describe('saveLaunchUuidToFile', () => { 118 | it('should call fs.open method with right parameters', () => { 119 | jest.spyOn(fs, 'open').mockImplementation(); 120 | 121 | helpers.saveLaunchUuidToFile('fileOne'); 122 | 123 | expect(fs.open).toHaveBeenCalledWith('rp-launch-uuid-fileOne.tmp', 'w', expect.any(Function)); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /__tests__/publicReportingAPI.spec.js: -------------------------------------------------------------------------------- 1 | const PublicReportingAPI = require('../lib/publicReportingAPI'); 2 | const { EVENTS } = require('../lib/constants/events'); 3 | 4 | describe('PublicReportingAPI', () => { 5 | it('setDescription should trigger process.emit with correct parameters', () => { 6 | jest.spyOn(process, 'emit').mockImplementation(); 7 | 8 | PublicReportingAPI.setDescription('text', 'suite'); 9 | 10 | expect(process.emit).toHaveBeenCalledWith(EVENTS.SET_DESCRIPTION, { 11 | text: 'text', 12 | suite: 'suite', 13 | }); 14 | }); 15 | 16 | it('addAttributes should trigger process.emit with correct parameters', () => { 17 | jest.spyOn(process, 'emit').mockImplementation(); 18 | 19 | PublicReportingAPI.addAttributes([{ value: 'value' }], 'suite'); 20 | 21 | expect(process.emit).toHaveBeenCalledWith(EVENTS.ADD_ATTRIBUTES, { 22 | attributes: [{ value: 'value' }], 23 | suite: 'suite', 24 | }); 25 | }); 26 | 27 | it('addLog should trigger process.emit with correct parameters', () => { 28 | jest.spyOn(process, 'emit').mockImplementation(); 29 | 30 | PublicReportingAPI.addLog({ level: 'INFO', message: 'message' }, 'suite'); 31 | 32 | expect(process.emit).toHaveBeenCalledWith(EVENTS.ADD_LOG, { 33 | log: { level: 'INFO', message: 'message' }, 34 | suite: 'suite', 35 | }); 36 | }); 37 | 38 | it('addLaunchLog should trigger process.emit with correct parameters', () => { 39 | jest.spyOn(process, 'emit').mockImplementation(); 40 | 41 | PublicReportingAPI.addLaunchLog({ level: 'INFO', message: 'message' }); 42 | 43 | expect(process.emit).toHaveBeenCalledWith(EVENTS.ADD_LAUNCH_LOG, { 44 | level: 'INFO', 45 | message: 'message', 46 | }); 47 | }); 48 | 49 | it('setTestCaseId should trigger process.emit with correct parameters', () => { 50 | jest.spyOn(process, 'emit').mockImplementation(); 51 | 52 | PublicReportingAPI.setTestCaseId('testCaseId', 'suite'); 53 | 54 | expect(process.emit).toHaveBeenCalledWith(EVENTS.SET_TEST_CASE_ID, { 55 | testCaseId: 'testCaseId', 56 | suite: 'suite', 57 | }); 58 | }); 59 | 60 | it('setLaunchStatus should trigger process.emit with correct parameters', () => { 61 | jest.spyOn(process, 'emit').mockImplementation(); 62 | 63 | PublicReportingAPI.setLaunchStatus('passed'); 64 | 65 | expect(process.emit).toHaveBeenCalledWith(EVENTS.SET_LAUNCH_STATUS, 'passed'); 66 | }); 67 | 68 | it('setStatus should trigger process.emit with correct parameters', () => { 69 | jest.spyOn(process, 'emit').mockImplementation(); 70 | 71 | PublicReportingAPI.setStatus('passed', 'suite'); 72 | 73 | expect(process.emit).toHaveBeenCalledWith(EVENTS.SET_STATUS, { 74 | status: 'passed', 75 | suite: 'suite', 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/report-portal-client.spec.js: -------------------------------------------------------------------------------- 1 | const process = require('process'); 2 | const RPClient = require('../lib/report-portal-client'); 3 | const helpers = require('../lib/helpers'); 4 | const { OUTPUT_TYPES } = require('../lib/constants/outputs'); 5 | 6 | describe('ReportPortal javascript client', () => { 7 | afterEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | describe('constructor', () => { 12 | it('creates the client instance without error', () => { 13 | const client = new RPClient({ 14 | apiKey: 'test', 15 | project: 'test', 16 | endpoint: 'https://abc.com', 17 | }); 18 | 19 | expect(client.config.apiKey).toBe('test'); 20 | expect(client.config.project).toBe('test'); 21 | expect(client.config.endpoint).toBe('https://abc.com'); 22 | }); 23 | }); 24 | 25 | describe('logDebug', () => { 26 | it('should call console.log with provided message if debug is true', () => { 27 | const client = new RPClient({ 28 | apiKey: 'test', 29 | project: 'test', 30 | endpoint: 'https://abc.com', 31 | debug: true, 32 | }); 33 | jest.spyOn(console, 'log').mockImplementation(); 34 | 35 | client.logDebug('message'); 36 | 37 | expect(console.log).toHaveBeenCalledWith('message', ''); 38 | }); 39 | 40 | it('should call console.log with messages if debug is true and dataMsg provided', () => { 41 | const client = new RPClient({ 42 | apiKey: 'test', 43 | project: 'test', 44 | endpoint: 'https://abc.com', 45 | debug: true, 46 | }); 47 | jest.spyOn(console, 'log').mockImplementation(); 48 | 49 | client.logDebug('message', { key: 1, value: 2 }); 50 | 51 | expect(console.log).toHaveBeenCalledWith('message', { key: 1, value: 2 }); 52 | }); 53 | 54 | it('should not call console.log if debug is false', () => { 55 | const client = new RPClient({ 56 | apiKey: 'test', 57 | project: 'test', 58 | endpoint: 'https://abc.com', 59 | debug: false, 60 | }); 61 | jest.spyOn(console, 'log').mockImplementation(); 62 | 63 | client.logDebug('message'); 64 | 65 | expect(console.log).not.toHaveBeenCalled(); 66 | }); 67 | }); 68 | 69 | describe('calculateItemRetriesChainMapKey', () => { 70 | it("should return correct parameter's string", () => { 71 | const client = new RPClient({ 72 | apiKey: 'test', 73 | project: 'test', 74 | endpoint: 'https://abc.com', 75 | }); 76 | 77 | const str = client.calculateItemRetriesChainMapKey('lId', 'pId', 'name', 'itemId'); 78 | 79 | expect(str).toEqual('lId__pId__name__itemId'); 80 | }); 81 | 82 | it("should return correct parameter's string with default value if itemId doesn't pass", () => { 83 | const client = new RPClient({ 84 | apiKey: 'test', 85 | project: 'test', 86 | endpoint: 'https://abc.com', 87 | }); 88 | 89 | const str = client.calculateItemRetriesChainMapKey('lId', 'pId', 'name'); 90 | 91 | expect(str).toEqual('lId__pId__name__'); 92 | }); 93 | }); 94 | 95 | describe('getRejectAnswer', () => { 96 | it('should return object with tempId and promise.reject with error', () => { 97 | const client = new RPClient({ 98 | apiKey: 'test', 99 | project: 'test', 100 | endpoint: 'https://abc.com', 101 | }); 102 | 103 | const rejectAnswer = client.getRejectAnswer('tempId', 'error'); 104 | 105 | expect(rejectAnswer.tempId).toEqual('tempId'); 106 | return expect(rejectAnswer.promise).rejects.toEqual('error'); 107 | }); 108 | }); 109 | 110 | describe('cleanMap', () => { 111 | it('should delete element with id', () => { 112 | const client = new RPClient({ 113 | apiKey: 'test', 114 | project: 'test', 115 | endpoint: 'https://abc.com', 116 | }); 117 | client.map = { 118 | id1: 'firstElement', 119 | id2: 'secondElement', 120 | id3: 'thirdElement', 121 | }; 122 | 123 | client.cleanMap(['id1', 'id2']); 124 | 125 | expect(client.map).toEqual({ id3: 'thirdElement' }); 126 | }); 127 | }); 128 | 129 | describe('checkConnect', () => { 130 | it('should return promise', () => { 131 | const client = new RPClient({ 132 | apiKey: 'test', 133 | project: 'test', 134 | endpoint: 'https://abc.com', 135 | }); 136 | jest.spyOn(client.restClient, 'request').mockReturnValue(Promise.resolve('ok')); 137 | 138 | const request = client.checkConnect(); 139 | 140 | return expect(request).resolves.toBeDefined(); 141 | }); 142 | }); 143 | 144 | describe('triggerAnalyticsEvent', () => { 145 | const OLD_ENV = process.env; 146 | 147 | beforeEach(() => { 148 | process.env = { ...OLD_ENV }; 149 | }); 150 | 151 | afterEach(() => { 152 | process.env = OLD_ENV; 153 | }); 154 | 155 | it('should call statistics.trackEvent if REPORTPORTAL_CLIENT_JS_NO_ANALYTICS is not set', async () => { 156 | const client = new RPClient({ 157 | apiKey: 'startLaunchTest', 158 | endpoint: 'https://rp.us/api/v1', 159 | project: 'tst', 160 | }); 161 | jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); 162 | 163 | await client.triggerStatisticsEvent(); 164 | 165 | expect(client.statistics.trackEvent).toHaveBeenCalled(); 166 | }); 167 | 168 | it('should not call statistics.trackEvent if REPORTPORTAL_CLIENT_JS_NO_ANALYTICS is true', async () => { 169 | const client = new RPClient({ 170 | apiKey: 'startLaunchTest', 171 | endpoint: 'https://rp.us/api/v1', 172 | project: 'tst', 173 | }); 174 | process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = true; 175 | jest.spyOn(client.statistics, 'trackEvent').mockImplementation(); 176 | 177 | await client.triggerStatisticsEvent(); 178 | 179 | expect(client.statistics.trackEvent).not.toHaveBeenCalled(); 180 | }); 181 | 182 | it('should create statistics object with agentParams is not empty', () => { 183 | const agentParams = { 184 | name: 'name', 185 | version: 'version', 186 | }; 187 | const client = new RPClient( 188 | { 189 | apiKey: 'startLaunchTest', 190 | endpoint: 'https://rp.us/api/v1', 191 | project: 'tst', 192 | }, 193 | agentParams, 194 | ); 195 | process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS = false; 196 | 197 | expect(client.statistics.eventName).toEqual('start_launch'); 198 | expect(client.statistics.eventParams).toEqual( 199 | expect.objectContaining({ 200 | agent_name: agentParams.name, 201 | agent_version: agentParams.version, 202 | }), 203 | ); 204 | }); 205 | 206 | it('should create statistics object without agentParams if they are empty', () => { 207 | const client = new RPClient({ 208 | apiKey: 'startLaunchTest', 209 | endpoint: 'https://rp.us/api/v1', 210 | project: 'tst', 211 | }); 212 | 213 | expect(client.statistics.eventName).toEqual('start_launch'); 214 | expect(client.statistics.eventParams).not.toEqual( 215 | expect.objectContaining({ 216 | agent_name: expect.anything(), 217 | agent_version: expect.anything(), 218 | }), 219 | ); 220 | }); 221 | }); 222 | 223 | describe('startLaunch', () => { 224 | it('should call restClient with suitable parameters', () => { 225 | const fakeSystemAttr = [ 226 | { 227 | key: 'client', 228 | value: 'client-name|1.0', 229 | system: true, 230 | }, 231 | { 232 | key: 'os', 233 | value: 'osType|osArchitecture', 234 | system: true, 235 | }, 236 | ]; 237 | const client = new RPClient({ 238 | apiKey: 'startLaunchTest', 239 | endpoint: 'https://rp.us/api/v1', 240 | project: 'tst', 241 | }); 242 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 243 | const time = 12345734; 244 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 245 | jest.spyOn(helpers, 'getSystemAttribute').mockReturnValue(fakeSystemAttr); 246 | 247 | client.startLaunch({ 248 | startTime: time, 249 | }); 250 | 251 | expect(client.restClient.create).toHaveBeenCalledWith('launch', { 252 | name: 'Test launch name', 253 | startTime: time, 254 | attributes: fakeSystemAttr, 255 | }); 256 | }); 257 | 258 | it('should call restClient with suitable parameters, attributes is concatenated', () => { 259 | const fakeSystemAttr = [ 260 | { 261 | key: 'client', 262 | value: 'client-name|1.0', 263 | system: true, 264 | }, 265 | ]; 266 | const client = new RPClient({ 267 | apiKey: 'startLaunchTest', 268 | endpoint: 'https://rp.us/api/v1', 269 | project: 'tst', 270 | }); 271 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 272 | const time = 12345734; 273 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 274 | jest.spyOn(helpers, 'getSystemAttribute').mockReturnValue(fakeSystemAttr); 275 | 276 | client.startLaunch({ 277 | startTime: time, 278 | attributes: [{ value: 'value' }], 279 | }); 280 | 281 | expect(client.restClient.create).toHaveBeenCalledWith('launch', { 282 | name: 'Test launch name', 283 | startTime: time, 284 | attributes: [ 285 | { value: 'value' }, 286 | { 287 | key: 'client', 288 | value: 'client-name|1.0', 289 | system: true, 290 | }, 291 | ], 292 | }); 293 | }); 294 | 295 | it('dont start new launch if launchDataRQ.id is not empty', () => { 296 | const client = new RPClient({ 297 | apiKey: 'startLaunchTest', 298 | endpoint: 'https://rp.us/api/v1', 299 | project: 'tst', 300 | }); 301 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 302 | const startTime = 12345734; 303 | const id = 12345734; 304 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 305 | 306 | client.startLaunch({ 307 | startTime, 308 | id, 309 | }); 310 | 311 | expect(client.restClient.create).not.toHaveBeenCalled(); 312 | expect(client.launchUuid).toEqual(id); 313 | }); 314 | 315 | it('should log Launch UUID if enabled', () => { 316 | jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); 317 | const client = new RPClient({ 318 | apiKey: 'startLaunchTest', 319 | endpoint: 'https://rp.us/api/v1', 320 | project: 'tst', 321 | launchUuidPrint: true, 322 | }); 323 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 324 | const time = 12345734; 325 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 326 | return client 327 | .startLaunch({ 328 | startTime: time, 329 | }) 330 | .promise.then(function () { 331 | expect(OUTPUT_TYPES.STDOUT).toHaveBeenCalledWith('testidlaunch'); 332 | }); 333 | }); 334 | 335 | it('should log Launch UUID into STDERR if enabled', () => { 336 | jest.spyOn(OUTPUT_TYPES, 'STDERR').mockImplementation(); 337 | const client = new RPClient({ 338 | apiKey: 'startLaunchTest', 339 | endpoint: 'https://rp.us/api/v1', 340 | project: 'tst', 341 | launchUuidPrint: true, 342 | launchUuidPrintOutput: 'stderr', 343 | }); 344 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 345 | const time = 12345734; 346 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 347 | return client 348 | .startLaunch({ 349 | startTime: time, 350 | }) 351 | .promise.then(function () { 352 | expect(OUTPUT_TYPES.STDERR).toHaveBeenCalledWith('testidlaunch'); 353 | }); 354 | }); 355 | 356 | it('should log Launch UUID into STDOUT if invalid output is set', () => { 357 | jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); 358 | const client = new RPClient({ 359 | apiKey: 'startLaunchTest', 360 | endpoint: 'https://rp.us/api/v1', 361 | project: 'tst', 362 | launchUuidPrint: true, 363 | launchUuidPrintOutput: 'asdfgh', 364 | }); 365 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 366 | const time = 12345734; 367 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 368 | return client 369 | .startLaunch({ 370 | startTime: time, 371 | }) 372 | .promise.then(function () { 373 | expect(OUTPUT_TYPES.STDOUT).toHaveBeenCalledWith('testidlaunch'); 374 | }); 375 | }); 376 | 377 | it('should log Launch UUID into ENVIRONMENT if enabled', () => { 378 | jest.spyOn(OUTPUT_TYPES, 'ENVIRONMENT').mockImplementation(); 379 | const client = new RPClient({ 380 | apiKey: 'startLaunchTest', 381 | endpoint: 'https://rp.us/api/v1', 382 | project: 'tst', 383 | launchUuidPrint: true, 384 | launchUuidPrintOutput: 'environment', 385 | }); 386 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 387 | const time = 12345734; 388 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 389 | return client 390 | .startLaunch({ 391 | startTime: time, 392 | }) 393 | .promise.then(function () { 394 | expect(OUTPUT_TYPES.ENVIRONMENT).toHaveBeenCalledWith('testidlaunch'); 395 | }); 396 | }); 397 | 398 | it('should log Launch UUID into FILE if enabled', () => { 399 | jest.spyOn(OUTPUT_TYPES, 'FILE').mockImplementation(); 400 | const client = new RPClient({ 401 | apiKey: 'startLaunchTest', 402 | endpoint: 'https://rp.us/api/v1', 403 | project: 'tst', 404 | launchUuidPrint: true, 405 | launchUuidPrintOutput: 'file', 406 | }); 407 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 408 | const time = 12345734; 409 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 410 | return client 411 | .startLaunch({ 412 | startTime: time, 413 | }) 414 | .promise.then(function () { 415 | expect(OUTPUT_TYPES.FILE).toHaveBeenCalledWith('testidlaunch'); 416 | }); 417 | }); 418 | 419 | it('should not log Launch UUID if not enabled', () => { 420 | jest.spyOn(OUTPUT_TYPES, 'STDOUT').mockImplementation(); 421 | const client = new RPClient({ 422 | apiKey: 'startLaunchTest', 423 | endpoint: 'https://rp.us/api/v1', 424 | project: 'tst', 425 | }); 426 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 427 | const time = 12345734; 428 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 429 | return client 430 | .startLaunch({ 431 | startTime: time, 432 | }) 433 | .promise.then(function () { 434 | expect(OUTPUT_TYPES.STDOUT).not.toHaveBeenCalled(); 435 | }); 436 | }); 437 | }); 438 | 439 | describe('finishLaunch', () => { 440 | it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { 441 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 442 | client.map = { 443 | id1: { 444 | children: ['child1'], 445 | }, 446 | }; 447 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 448 | 449 | client.finishLaunch('id2', { some: 'data' }); 450 | 451 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 452 | 'id2', 453 | new Error('Launch with tempId "id2" not found'), 454 | ); 455 | }); 456 | 457 | it('should trigger promiseFinish', async () => { 458 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 459 | client.map = { 460 | id1: { 461 | children: ['child1'], 462 | promiseStart: Promise.resolve(), 463 | resolveFinish: jest.fn().mockResolvedValue(), 464 | }, 465 | child1: { 466 | promiseFinish: jest.fn().mockResolvedValue(), 467 | }, 468 | }; 469 | 470 | jest.spyOn(client.restClient, 'update').mockResolvedValue({ link: 'link' }); 471 | 472 | await client.finishLaunch('id1', { some: 'data' }).promise; 473 | 474 | expect(client.map.child1.promiseFinish().then).toBeDefined(); 475 | }); 476 | }); 477 | 478 | describe('getMergeLaunchesRequest', () => { 479 | it('should return object which contains a data for merge launches with default launch name', () => { 480 | const expectedMergeLaunches = { 481 | description: 'Merged launch', 482 | endTime: 12345734, 483 | extendSuitesDescription: true, 484 | launches: ['12345', '12346'], 485 | mergeType: 'BASIC', 486 | mode: 'DEFAULT', 487 | name: 'Test launch name', 488 | attributes: [{ value: 'value' }], 489 | }; 490 | const client = new RPClient({ 491 | apiKey: 'test', 492 | project: 'test', 493 | endpoint: 'https://abc.com', 494 | attributes: [{ value: 'value' }], 495 | }); 496 | jest.spyOn(client.helpers, 'now').mockReturnValue(12345734); 497 | 498 | const mergeLaunches = client.getMergeLaunchesRequest(['12345', '12346']); 499 | 500 | expect(mergeLaunches).toEqual(expectedMergeLaunches); 501 | }); 502 | 503 | it('should return object which contains a data for merge launches', () => { 504 | const expectedMergeLaunches = { 505 | description: 'Merged launch', 506 | endTime: 12345734, 507 | extendSuitesDescription: true, 508 | launches: ['12345', '12346'], 509 | mergeType: 'BASIC', 510 | mode: 'DEFAULT', 511 | name: 'launch', 512 | attributes: [{ value: 'value' }], 513 | }; 514 | const client = new RPClient({ 515 | apiKey: 'test', 516 | project: 'test', 517 | endpoint: 'https://abc.com', 518 | launch: 'launch', 519 | attributes: [{ value: 'value' }], 520 | }); 521 | jest.spyOn(client.helpers, 'now').mockReturnValue(12345734); 522 | 523 | const mergeLaunches = client.getMergeLaunchesRequest(['12345', '12346']); 524 | 525 | expect(mergeLaunches).toEqual(expectedMergeLaunches); 526 | }); 527 | }); 528 | 529 | describe('mergeLaunches', () => { 530 | const fakeLaunchIds = ['12345-gfdgfdg-gfdgdf-fdfd45', '12345-gfdgfdg-gfdgdf-fdfd45', '']; 531 | const fakeEndTime = 12345734; 532 | const fakeMergeDataRQ = { 533 | description: 'Merged launch', 534 | endTime: fakeEndTime, 535 | extendSuitesDescription: true, 536 | launches: fakeLaunchIds, 537 | mergeType: 'BASIC', 538 | mode: 'DEFAULT', 539 | name: 'Test launch name', 540 | }; 541 | 542 | it('should call rest client with required parameters', async () => { 543 | const client = new RPClient({ 544 | apiKey: 'startLaunchTest', 545 | endpoint: 'https://rp.us/api/v1', 546 | project: 'tst', 547 | isLaunchMergeRequired: true, 548 | }); 549 | 550 | const myPromise = Promise.resolve({ id: 'testidlaunch' }); 551 | jest.spyOn(client.restClient, 'create').mockReturnValue(myPromise); 552 | jest.spyOn(helpers, 'readLaunchesFromFile').mockReturnValue(fakeLaunchIds); 553 | jest.spyOn(client, 'getMergeLaunchesRequest').mockReturnValue(fakeMergeDataRQ); 554 | jest.spyOn(client.restClient, 'retrieveSyncAPI').mockReturnValue( 555 | Promise.resolve({ 556 | content: [{ id: 'id1' }], 557 | }), 558 | ); 559 | 560 | const promise = client.mergeLaunches(); 561 | 562 | expect(promise.then).toBeDefined(); 563 | await promise; 564 | expect(client.restClient.create).toHaveBeenCalledWith('launch/merge', fakeMergeDataRQ); 565 | }); 566 | 567 | it('should not call rest client if something went wrong', async () => { 568 | const client = new RPClient({ 569 | apiKey: 'startLaunchTest', 570 | endpoint: 'https://rp.us/api/v1', 571 | project: 'tst', 572 | isLaunchMergeRequired: true, 573 | }); 574 | 575 | jest.spyOn(client.helpers, 'readLaunchesFromFile').mockReturnValue('launchUUid'); 576 | jest.spyOn(client.restClient, 'retrieveSyncAPI').mockResolvedValue(); 577 | jest.spyOn(client.restClient, 'create').mockRejectedValue(); 578 | await client.mergeLaunches(); 579 | 580 | expect(client.restClient.create).not.toHaveBeenCalled(); 581 | }); 582 | 583 | it('should return undefined if isLaunchMergeRequired is false', () => { 584 | const client = new RPClient({ 585 | apiKey: 'startLaunchTest', 586 | endpoint: 'https://rp.us/api/v1', 587 | project: 'tst', 588 | isLaunchMergeRequired: false, 589 | }); 590 | 591 | const result = client.mergeLaunches(); 592 | 593 | expect(result).toEqual(undefined); 594 | }); 595 | }); 596 | 597 | describe('getPromiseFinishAllItems', () => { 598 | it('should return promise', (done) => { 599 | const client = new RPClient({ 600 | apiKey: 'startLaunchTest', 601 | endpoint: 'https://rp.us/api/v1', 602 | project: 'tst', 603 | }); 604 | client.map = { 605 | id1: { 606 | children: ['child1'], 607 | }, 608 | child1: { 609 | promiseFinish: Promise.resolve(), 610 | }, 611 | }; 612 | 613 | const promise = client.getPromiseFinishAllItems('id1'); 614 | 615 | expect(promise).toBeInstanceOf(Promise); 616 | done(); 617 | }); 618 | }); 619 | 620 | describe('updateLaunch', () => { 621 | it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { 622 | const client = new RPClient({ 623 | apiKey: 'startLaunchTest', 624 | endpoint: 'https://rp.us/api/v1', 625 | project: 'tst', 626 | }); 627 | client.map = { 628 | id1: { 629 | children: ['child1'], 630 | }, 631 | }; 632 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 633 | 634 | client.updateLaunch('id2', { some: 'data' }); 635 | 636 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 637 | 'id2', 638 | new Error('Launch with tempId "id2" not found'), 639 | ); 640 | }); 641 | 642 | it('should return object with tempId and promise', () => { 643 | const client = new RPClient({ 644 | apiKey: 'startLaunchTest', 645 | endpoint: 'https://rp.us/api/v1', 646 | project: 'tst', 647 | }); 648 | client.map = { 649 | id1: { 650 | children: ['child1'], 651 | promiseFinish: Promise.resolve(), 652 | }, 653 | }; 654 | jest.spyOn(client.restClient, 'update').mockResolvedValue(); 655 | 656 | const result = client.updateLaunch('id1', { some: 'data' }); 657 | 658 | expect(result.tempId).toEqual('id1'); 659 | return expect(result.promise).resolves.toBeUndefined(); 660 | }); 661 | }); 662 | 663 | describe('startTestItem', () => { 664 | it('should call getRejectAnswer if there is no launchTempId with suitable launchTempId', () => { 665 | const client = new RPClient({ 666 | apiKey: 'startLaunchTest', 667 | endpoint: 'https://rp.us/api/v1', 668 | project: 'tst', 669 | }); 670 | client.map = { 671 | id1: { 672 | children: ['child1'], 673 | }, 674 | }; 675 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 676 | 677 | client.startTestItem({}, 'id2'); 678 | 679 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 680 | 'id2', 681 | new Error('Launch with tempId "id2" not found'), 682 | ); 683 | }); 684 | 685 | it('should call getRejectAnswer if launchObj.finishSend is true', () => { 686 | const client = new RPClient({ 687 | apiKey: 'startLaunchTest', 688 | endpoint: 'https://rp.us/api/v1', 689 | project: 'tst', 690 | }); 691 | client.map = { 692 | id1: { 693 | children: ['child1'], 694 | finishSend: true, 695 | }, 696 | }; 697 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 698 | const error = new Error( 699 | 'Launch with tempId "id1" is already finished, you can not add an item to it', 700 | ); 701 | 702 | client.startTestItem({}, 'id1'); 703 | 704 | expect(client.getRejectAnswer).toHaveBeenCalledWith('id1', error); 705 | }); 706 | 707 | it('should call getRejectAnswer if there is no parentObj with suitable parentTempId', () => { 708 | const client = new RPClient({ 709 | apiKey: 'startLaunchTest', 710 | endpoint: 'https://rp.us/api/v1', 711 | project: 'tst', 712 | }); 713 | client.map = { 714 | id: { 715 | children: ['id1'], 716 | }, 717 | id1: { 718 | children: ['child1'], 719 | }, 720 | }; 721 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 722 | const error = new Error('Item with tempId "id3" not found'); 723 | 724 | client.startTestItem({ testCaseId: 'testCaseId' }, 'id1', 'id3'); 725 | 726 | expect(client.getRejectAnswer).toHaveBeenCalledWith('id1', error); 727 | }); 728 | 729 | it('should return object with tempId and promise', () => { 730 | const client = new RPClient({ 731 | apiKey: 'startLaunchTest', 732 | endpoint: 'https://rp.us/api/v1', 733 | project: 'tst', 734 | }); 735 | client.map = { 736 | id: { 737 | children: ['id1', '4n5pxq24kpiob12og9'], 738 | promiseStart: Promise.resolve(), 739 | }, 740 | id1: { 741 | children: ['child1'], 742 | promiseStart: Promise.resolve(), 743 | }, 744 | '4n5pxq24kpiob12og9': { 745 | promiseStart: Promise.resolve(), 746 | }, 747 | }; 748 | jest.spyOn(client.itemRetriesChainMap, 'get').mockResolvedValue(); 749 | jest.spyOn(client.restClient, 'create').mockResolvedValue({}); 750 | jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); 751 | 752 | const result = client.startTestItem({ retry: false }, 'id1', 'id'); 753 | 754 | expect(result.tempId).toEqual('4n5pxq24kpiob12og9'); 755 | return expect(result.promise).resolves.toBeDefined(); 756 | }); 757 | 758 | it('should get previous try promise from itemRetriesChainMap if retry is true', async () => { 759 | const client = new RPClient({ 760 | apiKey: 'startLaunchTest', 761 | endpoint: 'https://rp.us/api/v1', 762 | project: 'tst', 763 | }); 764 | client.map = { 765 | id: { 766 | children: ['id1', '4n5pxq24kpiob12og9'], 767 | promiseStart: Promise.resolve(), 768 | }, 769 | id1: { 770 | children: ['child1'], 771 | promiseStart: Promise.resolve(), 772 | }, 773 | '4n5pxq24kpiob12og9': { 774 | promiseStart: Promise.resolve(), 775 | }, 776 | }; 777 | jest.spyOn(client, 'calculateItemRetriesChainMapKey').mockReturnValue('id1__name__'); 778 | jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); 779 | jest.spyOn(client.itemRetriesChainMap, 'get').mockImplementation(); 780 | jest.spyOn(client.restClient, 'create').mockResolvedValue({}); 781 | 782 | await client.startTestItem({ retry: true }, 'id1').promise; 783 | 784 | expect(client.itemRetriesChainMap.get).toHaveBeenCalledWith('id1__name__'); 785 | }); 786 | }); 787 | 788 | describe('finishTestItem', () => { 789 | it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { 790 | const client = new RPClient({ 791 | apiKey: 'startLaunchTest', 792 | endpoint: 'https://rp.us/api/v1', 793 | project: 'tst', 794 | }); 795 | client.map = { 796 | id1: { 797 | children: ['child1'], 798 | }, 799 | }; 800 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 801 | 802 | client.finishTestItem('id2', {}); 803 | 804 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 805 | 'id2', 806 | new Error('Item with tempId "id2" not found'), 807 | ); 808 | }); 809 | 810 | it('should call finishTestItemPromiseStart with correct parameters', (done) => { 811 | const client = new RPClient({ 812 | apiKey: 'startLaunchTest', 813 | endpoint: 'https://rp.us/api/v1', 814 | project: 'tst', 815 | }); 816 | client.map = { 817 | id: { 818 | children: ['id1'], 819 | promiseFinish: Promise.resolve(), 820 | }, 821 | id1: { 822 | children: ['child1'], 823 | promiseFinish: Promise.resolve(), 824 | }, 825 | }; 826 | client.launchUuid = 'launchUuid'; 827 | jest.spyOn(client, 'cleanMap').mockImplementation(); 828 | jest.spyOn(client, 'finishTestItemPromiseStart').mockImplementation(); 829 | jest.spyOn(client.helpers, 'now').mockReturnValue(1234567); 830 | 831 | client.finishTestItem('id', {}); 832 | 833 | setTimeout(() => { 834 | expect(client.cleanMap).toHaveBeenCalledWith(['id1']); 835 | expect(client.finishTestItemPromiseStart).toHaveBeenCalledWith( 836 | Object.assign(client.map.id, { finishSend: true }), 837 | 'id', 838 | { endTime: 1234567, launchUuid: 'launchUuid' }, 839 | ); 840 | done(); 841 | }, 100); 842 | }); 843 | 844 | it('should call finishTestItemPromiseStart with correct parameters if smt went wrong', (done) => { 845 | const client = new RPClient({ 846 | apiKey: 'startLaunchTest', 847 | endpoint: 'https://rp.us/api/v1', 848 | project: 'tst', 849 | }); 850 | client.map = { 851 | id: { 852 | children: ['id1'], 853 | promiseFinish: Promise.resolve(), 854 | }, 855 | id1: { 856 | children: ['child1'], 857 | promiseFinish: Promise.reject(), 858 | }, 859 | }; 860 | client.launchUuid = 'launchUuid'; 861 | jest.spyOn(client, 'cleanMap').mockImplementation(); 862 | jest.spyOn(client, 'finishTestItemPromiseStart').mockImplementation(); 863 | jest.spyOn(client.helpers, 'now').mockReturnValue(1234567); 864 | 865 | client.finishTestItem('id', {}); 866 | 867 | setTimeout(() => { 868 | expect(client.cleanMap).toHaveBeenCalledWith(['id1']); 869 | expect(client.finishTestItemPromiseStart).toHaveBeenCalledWith( 870 | Object.assign(client.map.id, { finishSend: true }), 871 | 'id', 872 | { endTime: 1234567, launchUuid: 'launchUuid' }, 873 | ); 874 | done(); 875 | }, 100); 876 | }); 877 | }); 878 | 879 | describe('saveLog', () => { 880 | it('should return object with tempId and promise', () => { 881 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 882 | client.map = { 883 | id1: { 884 | children: ['child1'], 885 | }, 886 | }; 887 | jest.spyOn(client, 'getUniqId').mockReturnValue('4n5pxq24kpiob12og9'); 888 | jest.spyOn(client.restClient, 'create').mockResolvedValue(); 889 | 890 | const result = client.saveLog( 891 | { 892 | promiseStart: Promise.resolve(), 893 | realId: 'realId', 894 | children: [], 895 | }, 896 | client.restClient.create, 897 | ); 898 | 899 | expect(result.tempId).toEqual('4n5pxq24kpiob12og9'); 900 | return expect(result.promise).resolves.toBeUndefined(); 901 | }); 902 | }); 903 | 904 | describe('sendLog', () => { 905 | it('should return sendLogWithFile if fileObj is not empty', () => { 906 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 907 | jest.spyOn(client, 'sendLogWithFile').mockReturnValue('sendLogWithFile'); 908 | 909 | const result = client.sendLog('itemTempId', { message: 'message' }, { name: 'name' }); 910 | 911 | expect(result).toEqual('sendLogWithFile'); 912 | }); 913 | 914 | it('should return sendLogWithoutFile if fileObj is empty', () => { 915 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 916 | jest.spyOn(client, 'sendLogWithoutFile').mockReturnValue('sendLogWithoutFile'); 917 | 918 | const result = client.sendLog('itemTempId', { message: 'message' }); 919 | 920 | expect(result).toEqual('sendLogWithoutFile'); 921 | }); 922 | }); 923 | 924 | describe('sendLogWithoutFile', () => { 925 | it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { 926 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 927 | client.map = { 928 | id1: { 929 | children: ['child1'], 930 | }, 931 | }; 932 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 933 | 934 | client.sendLogWithoutFile('itemTempId', {}); 935 | 936 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 937 | 'itemTempId', 938 | new Error('Item with tempId "itemTempId" not found'), 939 | ); 940 | }); 941 | 942 | it('should return saveLog function', () => { 943 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 944 | client.map = { 945 | itemTempId: { 946 | children: ['child1'], 947 | }, 948 | }; 949 | jest.spyOn(client, 'saveLog').mockReturnValue('saveLog'); 950 | 951 | const result = client.sendLogWithoutFile('itemTempId', {}); 952 | 953 | expect(result).toEqual('saveLog'); 954 | }); 955 | }); 956 | 957 | describe('sendLogWithFile', () => { 958 | it('should call getRejectAnswer if there is no itemObj with suitable itemTempId', () => { 959 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 960 | client.map = { 961 | id1: { 962 | children: ['child1'], 963 | }, 964 | }; 965 | jest.spyOn(client, 'getRejectAnswer').mockImplementation(); 966 | 967 | client.sendLogWithFile('itemTempId', {}); 968 | 969 | expect(client.getRejectAnswer).toHaveBeenCalledWith( 970 | 'itemTempId', 971 | new Error('Item with tempId "itemTempId" not found'), 972 | ); 973 | }); 974 | 975 | it('should return saveLog function', () => { 976 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 977 | client.map = { 978 | itemTempId: { 979 | children: ['child1'], 980 | }, 981 | }; 982 | jest.spyOn(client, 'saveLog').mockReturnValue('saveLog'); 983 | 984 | const result = client.sendLogWithFile('itemTempId', {}); 985 | 986 | expect(result).toEqual('saveLog'); 987 | }); 988 | }); 989 | 990 | describe('getRequestLogWithFile', () => { 991 | it('should return restClient.create', () => { 992 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 993 | client.map = { 994 | id1: { 995 | children: ['child1'], 996 | }, 997 | }; 998 | jest.spyOn(client, 'buildMultiPartStream').mockReturnValue(); 999 | jest.spyOn(client.restClient, 'create').mockResolvedValue('value'); 1000 | 1001 | const result = client.getRequestLogWithFile({}, { name: 'name' }); 1002 | 1003 | return expect(result).resolves.toBe('value'); 1004 | }); 1005 | 1006 | it('should return restClient.create with error', () => { 1007 | const client = new RPClient({ apiKey: 'any', endpoint: 'https://rp.api', project: 'prj' }); 1008 | client.map = { 1009 | id1: { 1010 | children: ['child1'], 1011 | }, 1012 | }; 1013 | jest.spyOn(client, 'buildMultiPartStream').mockReturnValue(); 1014 | jest.spyOn(client.restClient, 'create').mockRejectedValue(); 1015 | 1016 | const result = client.getRequestLogWithFile({}, { name: 'name' }); 1017 | 1018 | expect(result.catch).toBeDefined(); 1019 | }); 1020 | }); 1021 | }); 1022 | -------------------------------------------------------------------------------- /__tests__/rest.spec.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock'); 2 | const isEqual = require('lodash/isEqual'); 3 | const http = require('http'); 4 | const RestClient = require('../lib/rest'); 5 | const logger = require('../lib/logger'); 6 | 7 | describe('RestClient', () => { 8 | const options = { 9 | baseURL: 'http://report-portal-host:8080/api/v1', 10 | headers: { 11 | 'User-Agent': 'NodeJS', 12 | Authorization: 'Bearer 00000000-0000-0000-0000-000000000000', 13 | }, 14 | restClientConfig: { 15 | agent: { 16 | rejectUnauthorized: false, 17 | }, 18 | timeout: 0, 19 | }, 20 | }; 21 | const noOptions = {}; 22 | const restClient = new RestClient(options); 23 | 24 | const unathorizedError = { 25 | error: 'unauthorized', 26 | error_description: 'Full authentication is required to access this resource', 27 | }; 28 | const unauthorizedErrorMessage = 29 | 'Request failed with status code 403: ' + 30 | '{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}'; 31 | const netErrConnectionResetError = { code: 'ECONNABORTED', message: 'connection reset' }; 32 | 33 | describe('constructor', () => { 34 | it('creates object with correct properties', () => { 35 | expect(restClient.baseURL).toBe(options.baseURL); 36 | expect(restClient.headers).toEqual(options.headers); 37 | expect(restClient.restClientConfig).toEqual(options.restClientConfig); 38 | expect(restClient.axiosInstance).toBeDefined(); 39 | }); 40 | 41 | it('adds Logger to axios instance if enabled', () => { 42 | const spyLogger = jest.spyOn(logger, 'addLogger').mockReturnValue(); 43 | const optionsWithLoggerEnabled = { 44 | ...options, 45 | restClientConfig: { 46 | ...options.restClientConfig, 47 | debug: true, 48 | }, 49 | }; 50 | const client = new RestClient(optionsWithLoggerEnabled); 51 | 52 | expect(spyLogger).toHaveBeenCalledWith(client.axiosInstance); 53 | }); 54 | }); 55 | 56 | describe('buildPath', () => { 57 | it('compose path basing on base', () => { 58 | expect(restClient.buildPath('users')).toBe(`${options.baseURL}/users`); 59 | expect(restClient.buildPath('users/123')).toBe(`${options.baseURL}/users/123`); 60 | expect(restClient.buildPath()).toBe(`${options.baseURL}/`); 61 | }); 62 | }); 63 | 64 | describe('getRestConfig', () => { 65 | it("return {} in case agent property is doesn't exist", () => { 66 | restClient.restClientConfig = {}; 67 | expect(restClient.getRestConfig()).toEqual({}); 68 | }); 69 | 70 | it('creates object with correct properties with http(s) agent', () => { 71 | restClient.restClientConfig = { 72 | agent: { 73 | rejectUnauthorized: false, 74 | }, 75 | timeout: 10000, 76 | }; 77 | expect(restClient.getRestConfig().httpAgent).toBeDefined(); 78 | expect(restClient.getRestConfig().httpAgent).toBeInstanceOf(http.Agent); 79 | expect(restClient.getRestConfig().timeout).toBe(10000); 80 | expect(restClient.getRestConfig().agent).toBeUndefined(); 81 | }); 82 | }); 83 | 84 | describe('retrieve', () => { 85 | it('performs GET request for resource', (done) => { 86 | const listOfUsers = [{ id: 1 }, { id: 2 }, { id: 3 }]; 87 | 88 | const scope = nock(options.baseURL).get('/users').reply(200, listOfUsers); 89 | 90 | restClient.retrieve('users').then((result) => { 91 | expect(result).toEqual(listOfUsers); 92 | expect(scope.isDone()).toBeTruthy(); 93 | 94 | done(); 95 | }); 96 | }); 97 | 98 | it('catches NETWORK errors', (done) => { 99 | const scope = nock(options.baseURL).get('/users').replyWithError(netErrConnectionResetError); 100 | 101 | restClient.retrieve('users', noOptions).catch((error) => { 102 | expect(error instanceof Error).toBeTruthy(); 103 | expect(error.message).toMatch(netErrConnectionResetError.message); 104 | expect(scope.isDone()).toBeTruthy(); 105 | 106 | done(); 107 | }); 108 | }); 109 | 110 | it('catches API errors', (done) => { 111 | const scope = nock(options.baseURL).get('/users').reply(403, unathorizedError); 112 | 113 | restClient.retrieve('users', noOptions).catch((error) => { 114 | expect(error instanceof Error).toBeTruthy(); 115 | expect(error.message).toMatch(unauthorizedErrorMessage); 116 | expect(scope.isDone()).toBeTruthy(); 117 | 118 | done(); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('create', () => { 124 | it('performs POST request to resource', (done) => { 125 | const newUser = { username: 'John' }; 126 | const userCreated = { id: 1 }; 127 | 128 | const scope = nock(options.baseURL) 129 | .post('/users', (body) => isEqual(body, newUser)) 130 | .reply(201, userCreated); 131 | 132 | restClient.create('users', newUser).then((result) => { 133 | expect(result).toEqual(userCreated); 134 | expect(scope.isDone()).toBeTruthy(); 135 | 136 | done(); 137 | }); 138 | }); 139 | 140 | it('catches NETWORK errors', (done) => { 141 | const newUser = { username: 'John' }; 142 | 143 | const scope = nock(options.baseURL) 144 | .post('/users', (body) => isEqual(body, newUser)) 145 | .replyWithError(netErrConnectionResetError); 146 | 147 | restClient.create('users', newUser, noOptions).catch((error) => { 148 | expect(error instanceof Error).toBeTruthy(); 149 | expect(error.message).toMatch(netErrConnectionResetError.message); 150 | expect(scope.isDone()).toBeTruthy(); 151 | 152 | done(); 153 | }); 154 | }); 155 | 156 | it('catches API errors', (done) => { 157 | const newUser = { username: 'John' }; 158 | 159 | const scope = nock(options.baseURL) 160 | .post('/users', (body) => isEqual(body, newUser)) 161 | .reply(403, unathorizedError); 162 | 163 | restClient.create('users', newUser, noOptions).catch((error) => { 164 | expect(error instanceof Error).toBeTruthy(); 165 | expect(error.message).toMatch(unauthorizedErrorMessage); 166 | expect(scope.isDone()).toBeTruthy(); 167 | 168 | done(); 169 | }); 170 | }); 171 | }); 172 | 173 | describe('update', () => { 174 | it('performs PUT request to resource', (done) => { 175 | const newUserInfo = { username: 'Mike' }; 176 | const userUpdated = { id: 1 }; 177 | 178 | const scope = nock(options.baseURL) 179 | .put('/users/1', (body) => isEqual(body, newUserInfo)) 180 | .reply(200, userUpdated); 181 | 182 | restClient.update('users/1', newUserInfo).then((result) => { 183 | expect(result).toEqual(userUpdated); 184 | expect(scope.isDone()).toBeTruthy(); 185 | 186 | done(); 187 | }); 188 | }); 189 | 190 | it('catches NETWORK errors', (done) => { 191 | const newUserInfo = { username: 'Mike' }; 192 | 193 | const scope = nock(options.baseURL) 194 | .put('/users/1', (body) => isEqual(body, newUserInfo)) 195 | .replyWithError(netErrConnectionResetError); 196 | 197 | restClient.update('users/1', newUserInfo, noOptions).catch((error) => { 198 | expect(error instanceof Error).toBeTruthy(); 199 | expect(error.message).toMatch(netErrConnectionResetError.message); 200 | expect(scope.isDone()).toBeTruthy(); 201 | 202 | done(); 203 | }); 204 | }); 205 | 206 | it('catches API errors', (done) => { 207 | const newUserInfo = { username: 'Mike' }; 208 | 209 | const scope = nock(options.baseURL) 210 | .put('/users/1', (body) => isEqual(body, newUserInfo)) 211 | .reply(403, unathorizedError); 212 | 213 | restClient.update('users/1', newUserInfo, noOptions).catch((error) => { 214 | expect(error instanceof Error).toBeTruthy(); 215 | expect(error.message).toMatch(unauthorizedErrorMessage); 216 | expect(scope.isDone()).toBeTruthy(); 217 | 218 | done(); 219 | }); 220 | }); 221 | }); 222 | 223 | describe('delete', () => { 224 | it('performs DELETE request to resource', (done) => { 225 | const emptyBody = {}; 226 | const userDeleted = {}; 227 | 228 | const scope = nock(options.baseURL).delete('/users/1').reply(200, userDeleted); 229 | 230 | restClient.delete('users/1', emptyBody).then((result) => { 231 | expect(result).toEqual(userDeleted); 232 | expect(scope.isDone()).toBeTruthy(); 233 | 234 | done(); 235 | }); 236 | }); 237 | 238 | it('catches NETWORK errors', (done) => { 239 | const emptyBody = {}; 240 | 241 | const scope = nock(options.baseURL) 242 | .delete('/users/1') 243 | .replyWithError(netErrConnectionResetError); 244 | 245 | restClient.delete('users/1', emptyBody, noOptions).catch((error) => { 246 | expect(error instanceof Error).toBeTruthy(); 247 | expect(error.message).toMatch(netErrConnectionResetError.message); 248 | expect(scope.isDone()).toBeTruthy(); 249 | 250 | done(); 251 | }); 252 | }); 253 | 254 | it('catches API errors', (done) => { 255 | const emptyBody = {}; 256 | 257 | const scope = nock(options.baseURL).delete('/users/1').reply(403, unathorizedError); 258 | 259 | restClient.delete('users/1', emptyBody, noOptions).catch((error) => { 260 | expect(error instanceof Error).toBeTruthy(); 261 | expect(error.message).toMatch(unauthorizedErrorMessage); 262 | expect(scope.isDone()).toBeTruthy(); 263 | 264 | done(); 265 | }); 266 | }); 267 | }); 268 | 269 | describe('retrieveSyncAPI', () => { 270 | it('should retrieve SyncAPI', (done) => { 271 | const listOfUsers = [{ id: 1 }, { id: 2 }, { id: 3 }]; 272 | 273 | const scope = nock(options.baseURL).get('/users').reply(200, listOfUsers); 274 | 275 | restClient.retrieveSyncAPI('users').then((result) => { 276 | expect(result).toEqual(listOfUsers); 277 | expect(scope.isDone()).toBeTruthy(); 278 | 279 | done(); 280 | }); 281 | }); 282 | 283 | it('catches NETWORK errors', (done) => { 284 | const scope = nock(options.baseURL).get('/users').replyWithError(netErrConnectionResetError); 285 | 286 | restClient.retrieveSyncAPI('users', noOptions).catch((error) => { 287 | expect(error instanceof Error).toBeTruthy(); 288 | expect(error.message).toMatch(netErrConnectionResetError.message); 289 | expect(scope.isDone()).toBeTruthy(); 290 | 291 | done(); 292 | }); 293 | }); 294 | 295 | it('catches API errors', (done) => { 296 | const scope = nock(options.baseURL).get('/users').reply(403, unathorizedError); 297 | 298 | restClient.retrieveSyncAPI('users', noOptions).catch((error) => { 299 | expect(error instanceof Error).toBeTruthy(); 300 | expect(error.message).toMatch(unauthorizedErrorMessage); 301 | expect(scope.isDone()).toBeTruthy(); 302 | 303 | done(); 304 | }); 305 | }); 306 | }); 307 | }); 308 | -------------------------------------------------------------------------------- /__tests__/statistics.spec.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const Statistics = require('../statistics/statistics'); 3 | const { MEASUREMENT_ID, API_KEY } = require('../statistics/constants'); 4 | 5 | const uuidv4Validation = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 6 | 7 | const agentParams = { 8 | name: 'AgentName', 9 | version: 'AgentVersion', 10 | }; 11 | 12 | const eventName = 'start_launch'; 13 | 14 | const url = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_KEY}`; 15 | const baseParamsValidationObject = { 16 | interpreter: expect.stringMatching(/Node\.js \d{2}\.\d+\.\d+/), 17 | client_name: '@reportportal/client-javascript', 18 | client_version: expect.stringMatching(/\d+\.\d+\.\d+/), 19 | }; 20 | const agentParamsValidationObject = { 21 | ...baseParamsValidationObject, 22 | agent_name: agentParams.name, 23 | agent_version: agentParams.version, 24 | }; 25 | const baseParamsValidation = expect.objectContaining(baseParamsValidationObject); 26 | const agentParamsValidation = expect.objectContaining(agentParamsValidationObject); 27 | const baseEventValidationObject = { 28 | name: eventName, 29 | params: baseParamsValidation, 30 | }; 31 | const agentEventValidationObject = { 32 | name: eventName, 33 | params: agentParamsValidation, 34 | }; 35 | const baseRequestValidationObject = { 36 | client_id: expect.stringMatching(uuidv4Validation), 37 | events: expect.arrayContaining([expect.objectContaining(baseEventValidationObject)]), 38 | }; 39 | const baseRequestValidation = expect.objectContaining(baseRequestValidationObject); 40 | const agentRequestValidation = expect.objectContaining({ 41 | ...baseRequestValidationObject, 42 | events: expect.arrayContaining([expect.objectContaining(agentEventValidationObject)]), 43 | }); 44 | 45 | describe('Statistics', () => { 46 | afterEach(() => { 47 | jest.clearAllMocks(); 48 | }); 49 | 50 | it('should send proper event to axios', async () => { 51 | jest.spyOn(axios, 'post').mockReturnValue({ 52 | send: () => {}, // eslint-disable-line 53 | }); 54 | 55 | const statistics = new Statistics(eventName, agentParams); 56 | await statistics.trackEvent(); 57 | 58 | expect(axios.post).toHaveBeenCalledTimes(1); 59 | expect(axios.post).toHaveBeenCalledWith(url, agentRequestValidation); 60 | }); 61 | 62 | [ 63 | undefined, 64 | {}, 65 | { 66 | name: null, 67 | version: null, 68 | }, 69 | ].forEach((params) => { 70 | it(`should not fail if agent params: ${JSON.stringify(params)}`, async () => { 71 | jest.spyOn(axios, 'post').mockReturnValue({ 72 | send: () => {}, // eslint-disable-line 73 | }); 74 | 75 | const statistics = new Statistics(eventName, params); 76 | await statistics.trackEvent(); 77 | 78 | expect(axios.post).toHaveBeenCalledTimes(1); 79 | expect(axios.post).toHaveBeenCalledWith(url, baseRequestValidation); 80 | }); 81 | 82 | it('Should properly handle errors if any', async () => { 83 | const statistics = new Statistics(eventName, agentParams); 84 | const errorMessage = 'Error message'; 85 | 86 | jest.spyOn(axios, 'post').mockRejectedValue(new Error(errorMessage)); 87 | jest.spyOn(console, 'error').mockImplementation(); 88 | 89 | await statistics.trackEvent(); 90 | 91 | expect(console.error).toHaveBeenCalledWith(errorMessage); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js'], 3 | testRegex: '/__tests__/.*\\.(test|spec).js$', 4 | testEnvironment: 'node', 5 | collectCoverageFrom: ['lib/**/*.js', '!lib/logger.js'], 6 | coverageThreshold: { 7 | global: { 8 | branches: 80, 9 | functions: 75, 10 | lines: 80, 11 | statements: 80, 12 | }, 13 | }, 14 | bail: false, 15 | }; 16 | -------------------------------------------------------------------------------- /lib/commons/config.js: -------------------------------------------------------------------------------- 1 | const { ReportPortalRequiredOptionError, ReportPortalValidationError } = require('./errors'); 2 | const { OUTPUT_TYPES } = require('../constants/outputs'); 3 | 4 | const getOption = (options, optionName, defaultValue) => { 5 | if (!Object.prototype.hasOwnProperty.call(options, optionName) || !options[optionName]) { 6 | return defaultValue; 7 | } 8 | 9 | return options[optionName]; 10 | }; 11 | 12 | const getRequiredOption = (options, optionName) => { 13 | if (!Object.prototype.hasOwnProperty.call(options, optionName) || !options[optionName]) { 14 | throw new ReportPortalRequiredOptionError(optionName); 15 | } 16 | 17 | return options[optionName]; 18 | }; 19 | 20 | const getApiKey = ({ apiKey, token }) => { 21 | let calculatedApiKey = apiKey; 22 | if (!calculatedApiKey) { 23 | calculatedApiKey = token; 24 | if (!calculatedApiKey) { 25 | throw new ReportPortalRequiredOptionError('apiKey'); 26 | } else { 27 | console.warn(`Option 'token' is deprecated. Use 'apiKey' instead.`); 28 | } 29 | } 30 | 31 | return calculatedApiKey; 32 | }; 33 | 34 | const getClientConfig = (options) => { 35 | let calculatedOptions = options; 36 | try { 37 | if (typeof options !== 'object') { 38 | throw new ReportPortalValidationError('`options` must be an object.'); 39 | } 40 | const apiKey = getApiKey(options); 41 | const project = getRequiredOption(options, 'project'); 42 | const endpoint = getRequiredOption(options, 'endpoint'); 43 | 44 | const launchUuidPrintOutputType = getOption(options, 'launchUuidPrintOutput', 'STDOUT') 45 | .toString() 46 | .toUpperCase(); 47 | const launchUuidPrintOutput = getOption( 48 | OUTPUT_TYPES, 49 | launchUuidPrintOutputType, 50 | OUTPUT_TYPES.STDOUT, 51 | ); 52 | 53 | calculatedOptions = { 54 | apiKey, 55 | project, 56 | endpoint, 57 | launch: options.launch, 58 | debug: options.debug, 59 | isLaunchMergeRequired: 60 | options.isLaunchMergeRequired === undefined ? false : options.isLaunchMergeRequired, 61 | headers: options.headers, 62 | restClientConfig: options.restClientConfig, 63 | attributes: options.attributes, 64 | mode: options.mode, 65 | description: options.description, 66 | launchUuidPrint: options.launchUuidPrint, 67 | launchUuidPrintOutput, 68 | }; 69 | } catch (error) { 70 | // don't throw the error up to not break the entire process 71 | console.dir(error); 72 | } 73 | 74 | return calculatedOptions; 75 | }; 76 | 77 | module.exports = { 78 | getClientConfig, 79 | getRequiredOption, 80 | getApiKey, 81 | }; 82 | -------------------------------------------------------------------------------- /lib/commons/errors.js: -------------------------------------------------------------------------------- 1 | class ReportPortalError extends Error { 2 | constructor(message) { 3 | const basicMessage = `\nReportPortal client error: ${message}`; 4 | super(basicMessage); 5 | this.name = 'ReportPortalError'; 6 | } 7 | } 8 | 9 | class ReportPortalValidationError extends ReportPortalError { 10 | constructor(message) { 11 | const basicMessage = `\nValidation failed. Please, check the specified parameters: ${message}`; 12 | super(basicMessage); 13 | this.name = 'ReportPortalValidationError'; 14 | } 15 | } 16 | 17 | class ReportPortalRequiredOptionError extends ReportPortalValidationError { 18 | constructor(propertyName) { 19 | const basicMessage = `\nProperty '${propertyName}' must not be empty.`; 20 | super(basicMessage); 21 | this.name = 'ReportPortalRequiredOptionError'; 22 | } 23 | } 24 | 25 | module.exports = { 26 | ReportPortalError, 27 | ReportPortalValidationError, 28 | ReportPortalRequiredOptionError, 29 | }; 30 | -------------------------------------------------------------------------------- /lib/constants/events.js: -------------------------------------------------------------------------------- 1 | const EVENTS = { 2 | SET_DESCRIPTION: 'rp:setDescription', 3 | SET_TEST_CASE_ID: 'rp:setTestCaseId', 4 | SET_STATUS: 'rp:setStatus', 5 | SET_LAUNCH_STATUS: 'rp:setLaunchStatus', 6 | ADD_ATTRIBUTES: 'rp:addAttributes', 7 | ADD_LOG: 'rp:addLog', 8 | ADD_LAUNCH_LOG: 'rp:addLaunchLog', 9 | }; 10 | 11 | module.exports = { EVENTS }; 12 | -------------------------------------------------------------------------------- /lib/constants/outputs.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../helpers'); 2 | 3 | const OUTPUT_TYPES = { 4 | // eslint-disable-next-line no-console 5 | STDOUT: (launchUuid) => console.log(`Report Portal Launch UUID: ${launchUuid}`), 6 | // eslint-disable-next-line no-console 7 | STDERR: (launchUuid) => console.error(`Report Portal Launch UUID: ${launchUuid}`), 8 | // eslint-disable-next-line no-return-assign 9 | ENVIRONMENT: (launchUuid) => (process.env.RP_LAUNCH_UUID = launchUuid), 10 | FILE: helpers.saveLaunchUuidToFile, 11 | }; 12 | 13 | module.exports = { OUTPUT_TYPES }; 14 | -------------------------------------------------------------------------------- /lib/constants/statuses.js: -------------------------------------------------------------------------------- 1 | const RP_STATUSES = { 2 | PASSED: 'passed', 3 | FAILED: 'failed', 4 | SKIPPED: 'skipped', 5 | STOPPED: 'stopped', 6 | INTERRUPTED: 'interrupted', 7 | CANCELLED: 'cancelled', 8 | INFO: 'info', 9 | WARN: 'warn', 10 | }; 11 | 12 | module.exports = { RP_STATUSES }; 13 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const glob = require('glob'); 3 | const os = require('os'); 4 | const RestClient = require('./rest'); 5 | const pjson = require('../package.json'); 6 | 7 | const MIN = 3; 8 | const MAX = 256; 9 | const PJSON_VERSION = pjson.version; 10 | const PJSON_NAME = pjson.name; 11 | 12 | const getUUIDFromFileName = (filename) => filename.match(/rplaunch-(.*)\.tmp/)[1]; 13 | 14 | const formatName = (name) => { 15 | const len = name.length; 16 | // eslint-disable-next-line no-mixed-operators 17 | return (len < MIN ? name + new Array(MIN - len + 1).join('.') : name).slice(-MAX); 18 | }; 19 | 20 | const now = () => { 21 | return new Date().valueOf(); 22 | }; 23 | 24 | // TODO: deprecate and remove 25 | const getServerResult = (url, request, options, method) => { 26 | return new RestClient(options).request(method, url, request, options); 27 | }; 28 | 29 | const readLaunchesFromFile = () => { 30 | const files = glob.sync('rplaunch-*.tmp'); 31 | const ids = files.map(getUUIDFromFileName); 32 | 33 | return ids; 34 | }; 35 | 36 | const saveLaunchIdToFile = (launchId) => { 37 | const filename = `rplaunch-${launchId}.tmp`; 38 | fs.open(filename, 'w', (err) => { 39 | if (err) { 40 | throw err; 41 | } 42 | }); 43 | }; 44 | 45 | const getSystemAttribute = () => { 46 | const osType = os.type(); 47 | const osArchitecture = os.arch(); 48 | const RAMSize = os.totalmem(); 49 | const nodeVersion = process.version; 50 | const systemAttr = [ 51 | { 52 | key: 'client', 53 | value: `${PJSON_NAME}|${PJSON_VERSION}`, 54 | system: true, 55 | }, 56 | { 57 | key: 'os', 58 | value: `${osType}|${osArchitecture}`, 59 | system: true, 60 | }, 61 | { 62 | key: 'RAMSize', 63 | value: RAMSize, 64 | system: true, 65 | }, 66 | { 67 | key: 'nodeJS', 68 | value: nodeVersion, 69 | system: true, 70 | }, 71 | ]; 72 | 73 | return systemAttr; 74 | }; 75 | 76 | const generateTestCaseId = (codeRef, params) => { 77 | if (!codeRef) { 78 | return; 79 | } 80 | 81 | if (!params) { 82 | return codeRef; 83 | } 84 | 85 | const parameters = params.reduce( 86 | (result, item) => (item.value ? result.concat(item.value) : result), 87 | [], 88 | ); 89 | 90 | return `${codeRef}[${parameters}]`; 91 | }; 92 | 93 | const saveLaunchUuidToFile = (launchUuid) => { 94 | const filename = `rp-launch-uuid-${launchUuid}.tmp`; 95 | fs.open(filename, 'w', (err) => { 96 | if (err) { 97 | throw err; 98 | } 99 | }); 100 | }; 101 | 102 | module.exports = { 103 | formatName, 104 | now, 105 | getServerResult, 106 | readLaunchesFromFile, 107 | saveLaunchIdToFile, 108 | getSystemAttribute, 109 | generateTestCaseId, 110 | saveLaunchUuidToFile, 111 | }; 112 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | const addLogger = (axiosInstance) => { 2 | axiosInstance.interceptors.request.use((config) => { 3 | const startDate = new Date(); 4 | // eslint-disable-next-line no-param-reassign 5 | config.startTime = startDate.valueOf(); 6 | 7 | console.log(`Request method=${config.method} url=${config.url} [${startDate.toISOString()}]`); 8 | 9 | return config; 10 | }); 11 | 12 | axiosInstance.interceptors.response.use( 13 | (response) => { 14 | const date = new Date(); 15 | const { status, config } = response; 16 | 17 | console.log( 18 | `Response status=${status} url=${config.url} time=${ 19 | date.valueOf() - config.startTime 20 | }ms [${date.toISOString()}]`, 21 | ); 22 | 23 | return response; 24 | }, 25 | (error) => { 26 | const date = new Date(); 27 | const { response, config } = error; 28 | const status = response ? response.status : null; 29 | 30 | console.log( 31 | `Response ${status ? `status=${status}` : `message='${error.message}'`} url=${ 32 | config.url 33 | } time=${date.valueOf() - config.startTime}ms [${date.toISOString()}]`, 34 | ); 35 | 36 | return Promise.reject(error); 37 | }, 38 | ); 39 | }; 40 | 41 | module.exports = { addLogger }; 42 | -------------------------------------------------------------------------------- /lib/publicReportingAPI.js: -------------------------------------------------------------------------------- 1 | const { EVENTS } = require('./constants/events'); 2 | 3 | /** 4 | * Public API to emit additional events to RP JS agents. 5 | */ 6 | class PublicReportingAPI { 7 | /** 8 | * Emit set description event. 9 | * @param {String} text - description of current test/suite. 10 | * @param {String} suite - suite description, optional. 11 | */ 12 | static setDescription(text, suite) { 13 | process.emit(EVENTS.SET_DESCRIPTION, { text, suite }); 14 | } 15 | 16 | /** 17 | * Emit add attributes event. 18 | * @param {Array} attributes - array of attributes, should looks like this: 19 | * [{ 20 | * key: "attrKey", 21 | * value: "attrValue", 22 | * }] 23 | * 24 | * @param {String} suite - suite description, optional. 25 | */ 26 | static addAttributes(attributes, suite) { 27 | process.emit(EVENTS.ADD_ATTRIBUTES, { attributes, suite }); 28 | } 29 | 30 | /** 31 | * Emit send log to test item event. 32 | * @param {Object} log - log object should looks like this: 33 | * { 34 | * level: "INFO", 35 | * message: "log message", 36 | * file: { 37 | * name: "filename", 38 | * type: "image/png", // media type 39 | * content: data, // file content represented as 64base string 40 | * }, 41 | * } 42 | * @param {String} suite - suite description, optional. 43 | */ 44 | static addLog(log, suite) { 45 | process.emit(EVENTS.ADD_LOG, { log, suite }); 46 | } 47 | 48 | /** 49 | * Emit send log to current launch event. 50 | * @param {Object} log - log object should looks like this: 51 | * { 52 | * level: "INFO", 53 | * message: "log message", 54 | * file: { 55 | * name: "filename", 56 | * type: "image/png", // media type 57 | * content: data, // file content represented as 64base string 58 | * }, 59 | * } 60 | */ 61 | static addLaunchLog(log) { 62 | process.emit(EVENTS.ADD_LAUNCH_LOG, log); 63 | } 64 | 65 | /** 66 | * Emit set testCaseId event. 67 | * @param {String} testCaseId - testCaseId of current test/suite. 68 | * @param {String} suite - suite description, optional. 69 | */ 70 | static setTestCaseId(testCaseId, suite) { 71 | process.emit(EVENTS.SET_TEST_CASE_ID, { testCaseId, suite }); 72 | } 73 | 74 | /** 75 | * Emit set status to current launch event. 76 | * @param {String} status - status of current launch. 77 | */ 78 | static setLaunchStatus(status) { 79 | process.emit(EVENTS.SET_LAUNCH_STATUS, status); 80 | } 81 | 82 | /** 83 | * Emit set status event. 84 | * @param {String} status - status of current test/suite. 85 | * @param {String} suite - suite description, optional. 86 | */ 87 | static setStatus(status, suite) { 88 | process.emit(EVENTS.SET_STATUS, { status, suite }); 89 | } 90 | } 91 | 92 | module.exports = PublicReportingAPI; 93 | -------------------------------------------------------------------------------- /lib/report-portal-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quotes,no-console,class-methods-use-this */ 2 | const UniqId = require('uniqid'); 3 | const { URLSearchParams } = require('url'); 4 | const helpers = require('./helpers'); 5 | const RestClient = require('./rest'); 6 | const { getClientConfig } = require('./commons/config'); 7 | const Statistics = require('../statistics/statistics'); 8 | const { EVENT_NAME } = require('../statistics/constants'); 9 | const { RP_STATUSES } = require('./constants/statuses'); 10 | 11 | const MULTIPART_BOUNDARY = Math.floor(Math.random() * 10000000000).toString(); 12 | 13 | class RPClient { 14 | /** 15 | * Create a client for RP. 16 | * @param {Object} options - config object. 17 | * options should look like this 18 | * { 19 | * apiKey: "reportportalApiKey", 20 | * endpoint: "http://localhost:8080/api/v1", 21 | * launch: "YOUR LAUNCH NAME", 22 | * project: "PROJECT NAME", 23 | * } 24 | * 25 | * @param {Object} agentParams - agent's info object. 26 | * agentParams should look like this 27 | * { 28 | * name: "AGENT NAME", 29 | * version: "AGENT VERSION", 30 | * } 31 | */ 32 | constructor(options, agentParams) { 33 | this.config = getClientConfig(options); 34 | this.debug = this.config.debug; 35 | this.isLaunchMergeRequired = this.config.isLaunchMergeRequired; 36 | this.apiKey = this.config.apiKey; 37 | // deprecated 38 | this.token = this.apiKey; 39 | 40 | this.map = {}; 41 | this.baseURL = [this.config.endpoint, this.config.project].join('/'); 42 | this.headers = { 43 | 'User-Agent': 'NodeJS', 44 | 'Content-Type': 'application/json; charset=UTF-8', 45 | Authorization: `Bearer ${this.apiKey}`, 46 | ...(this.config.headers || {}), 47 | }; 48 | this.helpers = helpers; 49 | this.restClient = new RestClient({ 50 | baseURL: this.baseURL, 51 | headers: this.headers, 52 | restClientConfig: this.config.restClientConfig, 53 | }); 54 | this.statistics = new Statistics(EVENT_NAME, agentParams); 55 | this.launchUuid = ''; 56 | this.itemRetriesChainMap = new Map(); 57 | this.itemRetriesChainKeyMapByTempId = new Map(); 58 | } 59 | 60 | // eslint-disable-next-line valid-jsdoc 61 | /** 62 | * 63 | * @Private 64 | */ 65 | logDebug(msg, dataMsg = '') { 66 | if (this.debug) { 67 | console.log(msg, dataMsg); 68 | } 69 | } 70 | 71 | calculateItemRetriesChainMapKey(launchId, parentId, name, itemId = '') { 72 | return `${launchId}__${parentId}__${name}__${itemId}`; 73 | } 74 | 75 | // eslint-disable-next-line valid-jsdoc 76 | /** 77 | * 78 | * @Private 79 | */ 80 | cleanItemRetriesChain(tempIds) { 81 | tempIds.forEach((id) => { 82 | const key = this.itemRetriesChainKeyMapByTempId.get(id); 83 | 84 | if (key) { 85 | this.itemRetriesChainMap.delete(key); 86 | } 87 | 88 | this.itemRetriesChainKeyMapByTempId.delete(id); 89 | }); 90 | } 91 | 92 | getUniqId() { 93 | return UniqId(); 94 | } 95 | 96 | getRejectAnswer(tempId, error) { 97 | return { 98 | tempId, 99 | promise: Promise.reject(error), 100 | }; 101 | } 102 | 103 | getNewItemObj(startPromiseFunc) { 104 | let resolveFinish; 105 | let rejectFinish; 106 | const obj = { 107 | promiseStart: new Promise(startPromiseFunc), 108 | realId: '', 109 | children: [], 110 | finishSend: false, 111 | promiseFinish: new Promise((resolve, reject) => { 112 | resolveFinish = resolve; 113 | rejectFinish = reject; 114 | }), 115 | }; 116 | obj.resolveFinish = resolveFinish; 117 | obj.rejectFinish = rejectFinish; 118 | return obj; 119 | } 120 | 121 | // eslint-disable-next-line valid-jsdoc 122 | /** 123 | * 124 | * @Private 125 | */ 126 | cleanMap(ids) { 127 | ids.forEach((id) => { 128 | delete this.map[id]; 129 | }); 130 | } 131 | 132 | checkConnect() { 133 | const url = [this.config.endpoint.replace('/v2', '/v1'), this.config.project, 'launch'] 134 | .join('/') 135 | .concat('?page.page=1&page.size=1'); 136 | return this.restClient.request('GET', url, {}); 137 | } 138 | 139 | async triggerStatisticsEvent() { 140 | if (process.env.REPORTPORTAL_CLIENT_JS_NO_ANALYTICS) { 141 | return; 142 | } 143 | await this.statistics.trackEvent(); 144 | } 145 | 146 | /** 147 | * Start launch and report it. 148 | * @param {Object} launchDataRQ - request object. 149 | * launchDataRQ should look like this 150 | * { 151 | "description": "string" (support markdown), 152 | "mode": "DEFAULT" or "DEBUG", 153 | "name": "string", 154 | "startTime": this.helper.now(), 155 | "attributes": [ 156 | { 157 | "key": "string", 158 | "value": "string" 159 | }, 160 | { 161 | "value": "string" 162 | } 163 | ] 164 | * } 165 | * @Returns an object which contains a tempID and a promise 166 | * 167 | * As system attributes, this method sends the following data (these data are not for public use): 168 | * client name, version; 169 | * agent name, version (if given); 170 | * browser name, version (if given); 171 | * OS type, architecture; 172 | * RAMSize; 173 | * nodeJS version; 174 | * 175 | * This method works in two ways: 176 | * First - If launchDataRQ object doesn't contain ID field, 177 | * it would create a new Launch instance at the Report Portal with it ID. 178 | * Second - If launchDataRQ would contain ID field, 179 | * client would connect to the existing Launch which ID 180 | * has been sent , and would send all data to it. 181 | * Notice that Launch which ID has been sent must be 'IN PROGRESS' state at the Report Portal 182 | * or it would throw an error. 183 | * @Returns {Object} - an object which contains a tempID and a promise 184 | */ 185 | startLaunch(launchDataRQ) { 186 | const tempId = this.getUniqId(); 187 | 188 | if (launchDataRQ.id) { 189 | this.map[tempId] = this.getNewItemObj((resolve) => resolve(launchDataRQ)); 190 | this.map[tempId].realId = launchDataRQ.id; 191 | this.launchUuid = launchDataRQ.id; 192 | } else { 193 | const systemAttr = helpers.getSystemAttribute(); 194 | const attributes = Array.isArray(launchDataRQ.attributes) 195 | ? launchDataRQ.attributes.concat(systemAttr) 196 | : systemAttr; 197 | const launchData = { 198 | name: this.config.launch || 'Test launch name', 199 | startTime: this.helpers.now(), 200 | ...launchDataRQ, 201 | attributes, 202 | }; 203 | 204 | this.map[tempId] = this.getNewItemObj((resolve, reject) => { 205 | const url = 'launch'; 206 | this.logDebug(`Start launch with tempId ${tempId}`, launchDataRQ); 207 | this.restClient.create(url, launchData).then( 208 | (response) => { 209 | this.map[tempId].realId = response.id; 210 | this.launchUuid = response.id; 211 | if (this.config.launchUuidPrint) { 212 | this.config.launchUuidPrintOutput(this.launchUuid); 213 | } 214 | 215 | if (this.isLaunchMergeRequired) { 216 | helpers.saveLaunchIdToFile(response.id); 217 | } 218 | 219 | this.logDebug(`Success start launch with tempId ${tempId}`, response); 220 | resolve(response); 221 | }, 222 | (error) => { 223 | this.logDebug(`Error start launch with tempId ${tempId}`, error); 224 | console.dir(error); 225 | reject(error); 226 | }, 227 | ); 228 | }); 229 | } 230 | this.triggerStatisticsEvent().catch(console.error); 231 | return { 232 | tempId, 233 | promise: this.map[tempId].promiseStart, 234 | }; 235 | } 236 | 237 | /** 238 | * Finish launch. 239 | * @param {string} launchTempId - temp launch id (returned in the query "startLaunch"). 240 | * @param {Object} finishExecutionRQ - finish launch info should include time and status. 241 | * finishExecutionRQ should look like this 242 | * { 243 | * "endTime": this.helper.now(), 244 | * "status": "passed" or one of ‘passed’, ‘failed’, ‘stopped’, ‘skipped’, ‘interrupted’, ‘cancelled’ 245 | * } 246 | * @Returns {Object} - an object which contains a tempID and a promise 247 | */ 248 | finishLaunch(launchTempId, finishExecutionRQ) { 249 | const launchObj = this.map[launchTempId]; 250 | if (!launchObj) { 251 | return this.getRejectAnswer( 252 | launchTempId, 253 | new Error(`Launch with tempId "${launchTempId}" not found`), 254 | ); 255 | } 256 | 257 | const finishExecutionData = { endTime: this.helpers.now(), ...finishExecutionRQ }; 258 | 259 | launchObj.finishSend = true; 260 | Promise.all(launchObj.children.map((itemId) => this.map[itemId].promiseFinish)).then( 261 | () => { 262 | launchObj.promiseStart.then( 263 | () => { 264 | this.logDebug(`Finish launch with tempId ${launchTempId}`, finishExecutionData); 265 | const url = ['launch', launchObj.realId, 'finish'].join('/'); 266 | this.restClient.update(url, finishExecutionData).then( 267 | (response) => { 268 | this.logDebug(`Success finish launch with tempId ${launchTempId}`, response); 269 | console.log(`\nReportPortal Launch Link: ${response.link}`); 270 | launchObj.resolveFinish(response); 271 | }, 272 | (error) => { 273 | this.logDebug(`Error finish launch with tempId ${launchTempId}`, error); 274 | console.dir(error); 275 | launchObj.rejectFinish(error); 276 | }, 277 | ); 278 | }, 279 | (error) => { 280 | console.dir(error); 281 | launchObj.rejectFinish(error); 282 | }, 283 | ); 284 | }, 285 | (error) => { 286 | console.dir(error); 287 | launchObj.rejectFinish(error); 288 | }, 289 | ); 290 | 291 | return { 292 | tempId: launchTempId, 293 | promise: launchObj.promiseFinish, 294 | }; 295 | } 296 | 297 | /* 298 | * This method is used to create data object for merge request to ReportPortal. 299 | * 300 | * @Returns {Object} - an object which contains a data for merge launches in ReportPortal. 301 | */ 302 | getMergeLaunchesRequest(launchIds, mergeOptions = {}) { 303 | return { 304 | launches: launchIds, 305 | mergeType: 'BASIC', 306 | description: this.config.description || 'Merged launch', 307 | mode: this.config.mode || 'DEFAULT', 308 | name: this.config.launch || 'Test launch name', 309 | attributes: this.config.attributes, 310 | endTime: this.helpers.now(), 311 | extendSuitesDescription: true, 312 | ...mergeOptions, 313 | }; 314 | } 315 | 316 | /** 317 | * This method is used for merge launches in ReportPortal. 318 | * @param {Object} mergeOptions - options for merge request, can override default options. 319 | * mergeOptions should look like this 320 | * { 321 | * "extendSuitesDescription": boolean, 322 | * "description": string, 323 | * "mergeType": 'BASIC' | 'DEEP', 324 | * "name": string 325 | * } 326 | * Please, keep in mind that this method is work only in case 327 | * the option isLaunchMergeRequired is true. 328 | * 329 | * @returns {Promise} - action promise 330 | */ 331 | mergeLaunches(mergeOptions = {}) { 332 | if (this.isLaunchMergeRequired) { 333 | const launchUUIds = helpers.readLaunchesFromFile(); 334 | const params = new URLSearchParams({ 335 | 'filter.in.uuid': launchUUIds, 336 | 'page.size': launchUUIds.length, 337 | }); 338 | const launchSearchUrl = 339 | this.config.mode === 'DEBUG' 340 | ? `launch/mode?${params.toString()}` 341 | : `launch?${params.toString()}`; 342 | this.logDebug(`Find launches with UUIDs to merge: ${launchUUIds}`); 343 | return this.restClient 344 | .retrieveSyncAPI(launchSearchUrl) 345 | .then( 346 | (response) => { 347 | const launchIds = response.content.map((launch) => launch.id); 348 | this.logDebug(`Found launches: ${launchIds}`, response.content); 349 | return launchIds; 350 | }, 351 | (error) => { 352 | this.logDebug(`Error during launches search with UUIDs: ${launchUUIds}`, error); 353 | console.dir(error); 354 | }, 355 | ) 356 | .then((launchIds) => { 357 | const request = this.getMergeLaunchesRequest(launchIds, mergeOptions); 358 | this.logDebug(`Merge launches with ids: ${launchIds}`, request); 359 | const mergeURL = 'launch/merge'; 360 | return this.restClient.create(mergeURL, request); 361 | }) 362 | .then((response) => { 363 | this.logDebug(`Launches with UUIDs: ${launchUUIds} were successfully merged!`); 364 | if (this.config.launchUuidPrint) { 365 | this.config.launchUuidPrintOutput(response.uuid); 366 | } 367 | }) 368 | .catch((error) => { 369 | this.logDebug(`Error merging launches with UUIDs: ${launchUUIds}`, error); 370 | console.dir(error); 371 | }); 372 | } 373 | this.logDebug( 374 | 'Option isLaunchMergeRequired is false, merge process cannot be done as no launch UUIDs where saved.', 375 | ); 376 | } 377 | 378 | /* 379 | * This method is used for frameworks as Jasmine. There is problem when 380 | * it doesn't wait for promise resolve and stop the process. So it better to call 381 | * this method at the spec's function as @afterAll() and manually resolve this promise. 382 | * 383 | * @return Promise 384 | */ 385 | getPromiseFinishAllItems(launchTempId) { 386 | const launchObj = this.map[launchTempId]; 387 | return Promise.all(launchObj.children.map((itemId) => this.map[itemId].promiseFinish)); 388 | } 389 | 390 | /** 391 | * Update launch. 392 | * @param {string} launchTempId - temp launch id (returned in the query "startLaunch"). 393 | * @param {Object} launchData - new launch data 394 | * launchData should look like this 395 | * { 396 | "description": "string" (support markdown), 397 | "mode": "DEFAULT" or "DEBUG", 398 | "attributes": [ 399 | { 400 | "key": "string", 401 | "value": "string" 402 | }, 403 | { 404 | "value": "string" 405 | } 406 | ] 407 | } 408 | * @Returns {Object} - an object which contains a tempId and a promise 409 | */ 410 | updateLaunch(launchTempId, launchData) { 411 | const launchObj = this.map[launchTempId]; 412 | if (!launchObj) { 413 | return this.getRejectAnswer( 414 | launchTempId, 415 | new Error(`Launch with tempId "${launchTempId}" not found`), 416 | ); 417 | } 418 | let resolvePromise; 419 | let rejectPromise; 420 | const promise = new Promise((resolve, reject) => { 421 | resolvePromise = resolve; 422 | rejectPromise = reject; 423 | }); 424 | 425 | launchObj.promiseFinish.then( 426 | () => { 427 | const url = ['launch', launchObj.realId, 'update'].join('/'); 428 | this.logDebug(`Update launch with tempId ${launchTempId}`, launchData); 429 | this.restClient.update(url, launchData).then( 430 | (response) => { 431 | this.logDebug(`Launch with tempId ${launchTempId} were successfully updated`, response); 432 | resolvePromise(response); 433 | }, 434 | (error) => { 435 | this.logDebug(`Error when updating launch with tempId ${launchTempId}`, error); 436 | console.dir(error); 437 | rejectPromise(error); 438 | }, 439 | ); 440 | }, 441 | (error) => { 442 | rejectPromise(error); 443 | }, 444 | ); 445 | return { 446 | tempId: launchTempId, 447 | promise, 448 | }; 449 | } 450 | 451 | /** 452 | * If there is no parentItemId starts Suite, else starts test or item. 453 | * @param {Object} testItemDataRQ - object with item parameters 454 | * testItemDataRQ should look like this 455 | * { 456 | "description": "string" (support markdown), 457 | "name": "string", 458 | "startTime": this.helper.now(), 459 | "attributes": [ 460 | { 461 | "key": "string", 462 | "value": "string" 463 | }, 464 | { 465 | "value": "string" 466 | } 467 | ], 468 | "type": 'SUITE' or one of 'SUITE', 'STORY', 'TEST', 469 | 'SCENARIO', 'STEP', 'BEFORE_CLASS', 'BEFORE_GROUPS', 470 | 'BEFORE_METHOD', 'BEFORE_SUITE', 'BEFORE_TEST', 471 | 'AFTER_CLASS', 'AFTER_GROUPS', 'AFTER_METHOD', 472 | 'AFTER_SUITE', 'AFTER_TEST' 473 | } 474 | * @param {string} launchTempId - temp launch id (returned in the query "startLaunch"). 475 | * @param {string} parentTempId (optional) - temp item id (returned in the query "startTestItem"). 476 | * @Returns {Object} - an object which contains a tempId and a promise 477 | */ 478 | startTestItem(testItemDataRQ, launchTempId, parentTempId) { 479 | let parentMapId = launchTempId; 480 | const launchObj = this.map[launchTempId]; 481 | if (!launchObj) { 482 | return this.getRejectAnswer( 483 | launchTempId, 484 | new Error(`Launch with tempId "${launchTempId}" not found`), 485 | ); 486 | } 487 | // TODO: Allow items reporting to finished launch 488 | if (launchObj.finishSend) { 489 | const err = new Error( 490 | `Launch with tempId "${launchTempId}" is already finished, you can not add an item to it`, 491 | ); 492 | return this.getRejectAnswer(launchTempId, err); 493 | } 494 | 495 | const testCaseId = 496 | testItemDataRQ.testCaseId || 497 | helpers.generateTestCaseId(testItemDataRQ.codeRef, testItemDataRQ.parameters); 498 | const testItemData = { 499 | startTime: this.helpers.now(), 500 | ...testItemDataRQ, 501 | ...(testCaseId && { testCaseId }), 502 | }; 503 | 504 | let parentPromise = launchObj.promiseStart; 505 | if (parentTempId) { 506 | parentMapId = parentTempId; 507 | const parentObj = this.map[parentTempId]; 508 | if (!parentObj) { 509 | return this.getRejectAnswer( 510 | launchTempId, 511 | new Error(`Item with tempId "${parentTempId}" not found`), 512 | ); 513 | } 514 | parentPromise = parentObj.promiseStart; 515 | } 516 | 517 | const itemKey = this.calculateItemRetriesChainMapKey( 518 | launchTempId, 519 | parentTempId, 520 | testItemDataRQ.name, 521 | testItemDataRQ.uniqueId, 522 | ); 523 | const executionItemPromise = testItemDataRQ.retry && this.itemRetriesChainMap.get(itemKey); 524 | 525 | const tempId = this.getUniqId(); 526 | this.map[tempId] = this.getNewItemObj((resolve, reject) => { 527 | (executionItemPromise || parentPromise).then( 528 | () => { 529 | const realLaunchId = this.map[launchTempId].realId; 530 | let url = 'item/'; 531 | if (parentTempId) { 532 | const realParentId = this.map[parentTempId].realId; 533 | url += `${realParentId}`; 534 | } 535 | testItemData.launchUuid = realLaunchId; 536 | this.logDebug(`Start test item with tempId ${tempId}`, testItemData); 537 | this.restClient.create(url, testItemData).then( 538 | (response) => { 539 | this.logDebug(`Success start item with tempId ${tempId}`, response); 540 | this.map[tempId].realId = response.id; 541 | resolve(response); 542 | }, 543 | (error) => { 544 | this.logDebug(`Error start item with tempId ${tempId}`, error); 545 | console.dir(error); 546 | reject(error); 547 | }, 548 | ); 549 | }, 550 | (error) => { 551 | reject(error); 552 | }, 553 | ); 554 | }); 555 | this.map[parentMapId].children.push(tempId); 556 | this.itemRetriesChainKeyMapByTempId.set(tempId, itemKey); 557 | this.itemRetriesChainMap.set(itemKey, this.map[tempId].promiseStart); 558 | 559 | return { 560 | tempId, 561 | promise: this.map[tempId].promiseStart, 562 | }; 563 | } 564 | 565 | /** 566 | * Finish Suite or Step level. 567 | * @param {string} itemTempId - temp item id (returned in the query "startTestItem"). 568 | * @param {Object} finishTestItemRQ - object with item parameters. 569 | * finishTestItemRQ should look like this 570 | { 571 | "endTime": this.helper.now(), 572 | "issue": { 573 | "comment": "string", 574 | "externalSystemIssues": [ 575 | { 576 | "submitDate": 0, 577 | "submitter": "string", 578 | "systemId": "string", 579 | "ticketId": "string", 580 | "url": "string" 581 | } 582 | ], 583 | "issueType": "string" 584 | }, 585 | "status": "passed" or one of 'passed', 'failed', 'stopped', 'skipped', 'interrupted', 'cancelled' 586 | } 587 | * @Returns {Object} - an object which contains a tempId and a promise 588 | */ 589 | finishTestItem(itemTempId, finishTestItemRQ) { 590 | const itemObj = this.map[itemTempId]; 591 | if (!itemObj) { 592 | return this.getRejectAnswer( 593 | itemTempId, 594 | new Error(`Item with tempId "${itemTempId}" not found`), 595 | ); 596 | } 597 | 598 | const finishTestItemData = { 599 | endTime: this.helpers.now(), 600 | ...(itemObj.children.length ? {} : { status: RP_STATUSES.PASSED }), 601 | ...finishTestItemRQ, 602 | }; 603 | 604 | itemObj.finishSend = true; 605 | this.logDebug(`Finish all children for test item with tempId ${itemTempId}`); 606 | Promise.allSettled( 607 | itemObj.children.map((itemId) => this.map[itemId] && this.map[itemId].promiseFinish), 608 | ) 609 | .then((results) => { 610 | if (this.debug) { 611 | results.forEach((result, index) => { 612 | if (result.status === 'fulfilled') { 613 | this.logDebug( 614 | `Successfully finish child with tempId ${itemObj.children[index]} 615 | of test item with tempId ${itemTempId}`, 616 | ); 617 | } else { 618 | this.logDebug( 619 | `Failed to finish child with tempId ${itemObj.children[index]} 620 | of test item with tempId ${itemTempId}`, 621 | ); 622 | } 623 | }); 624 | } 625 | this.cleanItemRetriesChain(itemObj.children); 626 | this.cleanMap(itemObj.children); 627 | 628 | this.logDebug(`Finish test item with tempId ${itemTempId}`, finishTestItemRQ); 629 | this.finishTestItemPromiseStart( 630 | itemObj, 631 | itemTempId, 632 | Object.assign(finishTestItemData, { launchUuid: this.launchUuid }), 633 | ); 634 | }) 635 | .catch(() => { 636 | this.logDebug(`Error finish children of test item with tempId ${itemTempId}`); 637 | }); 638 | 639 | return { 640 | tempId: itemTempId, 641 | promise: itemObj.promiseFinish, 642 | }; 643 | } 644 | 645 | saveLog(itemObj, requestPromiseFunc) { 646 | const tempId = this.getUniqId(); 647 | this.map[tempId] = this.getNewItemObj((resolve, reject) => { 648 | itemObj.promiseStart.then( 649 | () => { 650 | this.logDebug(`Save log with tempId ${tempId}`, itemObj); 651 | requestPromiseFunc(itemObj.realId, this.launchUuid).then( 652 | (response) => { 653 | this.logDebug(`Successfully save log with tempId ${tempId}`, response); 654 | resolve(response); 655 | }, 656 | (error) => { 657 | this.logDebug(`Error save log with tempId ${tempId}`, error); 658 | console.dir(error); 659 | reject(error); 660 | }, 661 | ); 662 | }, 663 | (error) => { 664 | reject(error); 665 | }, 666 | ); 667 | }); 668 | itemObj.children.push(tempId); 669 | 670 | const logObj = this.map[tempId]; 671 | logObj.finishSend = true; 672 | logObj.promiseStart.then( 673 | (response) => logObj.resolveFinish(response), 674 | (error) => logObj.rejectFinish(error), 675 | ); 676 | 677 | return { 678 | tempId, 679 | promise: this.map[tempId].promiseFinish, 680 | }; 681 | } 682 | 683 | sendLog(itemTempId, saveLogRQ, fileObj) { 684 | const saveLogData = { 685 | time: this.helpers.now(), 686 | message: '', 687 | level: '', 688 | ...saveLogRQ, 689 | }; 690 | 691 | if (fileObj) { 692 | return this.sendLogWithFile(itemTempId, saveLogData, fileObj); 693 | } 694 | return this.sendLogWithoutFile(itemTempId, saveLogData); 695 | } 696 | 697 | /** 698 | * Send log of test results. 699 | * @param {string} itemTempId - temp item id (returned in the query "startTestItem"). 700 | * @param {Object} saveLogRQ - object with data of test result. 701 | * saveLogRQ should look like this 702 | * { 703 | * level: 'error' or one of 'trace', 'debug', 'info', 'warn', 'error', '', 704 | * message: 'string' (support markdown), 705 | * time: this.helpers.now() 706 | * } 707 | * @Returns {Object} - an object which contains a tempId and a promise 708 | */ 709 | sendLogWithoutFile(itemTempId, saveLogRQ) { 710 | const itemObj = this.map[itemTempId]; 711 | if (!itemObj) { 712 | return this.getRejectAnswer( 713 | itemTempId, 714 | new Error(`Item with tempId "${itemTempId}" not found`), 715 | ); 716 | } 717 | 718 | const requestPromise = (itemUuid, launchUuid) => { 719 | const url = 'log'; 720 | const isItemUuid = itemUuid !== launchUuid; 721 | return this.restClient.create( 722 | url, 723 | Object.assign(saveLogRQ, { launchUuid }, isItemUuid && { itemUuid }), 724 | ); 725 | }; 726 | return this.saveLog(itemObj, requestPromise); 727 | } 728 | 729 | /** 730 | * Send log of test results with file. 731 | * @param {string} itemTempId - temp item id (returned in the query "startTestItem"). 732 | * @param {Object} saveLogRQ - object with data of test result. 733 | * saveLogRQ should look like this 734 | * { 735 | * level: 'error' or one of 'trace', 'debug', 'info', 'warn', 'error', '', 736 | * message: 'string' (support markdown), 737 | * time: this.helpers.now() 738 | * } 739 | * @param {Object} fileObj - object with file data. 740 | * fileObj should look like this 741 | * { 742 | name: 'string', 743 | type: "image/png" or your file mimeType 744 | (supported types: 'image/*', application/ ['xml', 'javascript', 'json', 'css', 'php'], 745 | another format will be opened in a new browser tab ), 746 | content: file 747 | * } 748 | * @Returns {Object} - an object which contains a tempId and a promise 749 | */ 750 | sendLogWithFile(itemTempId, saveLogRQ, fileObj) { 751 | const itemObj = this.map[itemTempId]; 752 | if (!itemObj) { 753 | return this.getRejectAnswer( 754 | itemTempId, 755 | new Error(`Item with tempId "${itemTempId}" not found`), 756 | ); 757 | } 758 | 759 | const requestPromise = (itemUuid, launchUuid) => { 760 | const isItemUuid = itemUuid !== launchUuid; 761 | 762 | return this.getRequestLogWithFile( 763 | Object.assign(saveLogRQ, { launchUuid }, isItemUuid && { itemUuid }), 764 | fileObj, 765 | ); 766 | }; 767 | 768 | return this.saveLog(itemObj, requestPromise); 769 | } 770 | 771 | getRequestLogWithFile(saveLogRQ, fileObj) { 772 | const url = 'log'; 773 | // eslint-disable-next-line no-param-reassign 774 | saveLogRQ.file = { name: fileObj.name }; 775 | this.logDebug(`Save log with file: ${fileObj.name}`, saveLogRQ); 776 | return this.restClient 777 | .create(url, this.buildMultiPartStream([saveLogRQ], fileObj, MULTIPART_BOUNDARY), { 778 | headers: { 779 | 'Content-Type': `multipart/form-data; boundary=${MULTIPART_BOUNDARY}`, 780 | }, 781 | }) 782 | .then((response) => { 783 | this.logDebug(`Success save log with file: ${fileObj.name}`, response); 784 | return response; 785 | }) 786 | .catch((error) => { 787 | this.logDebug(`Error save log with file: ${fileObj.name}`, error); 788 | console.dir(error); 789 | }); 790 | } 791 | 792 | // eslint-disable-next-line valid-jsdoc 793 | /** 794 | * 795 | * @Private 796 | */ 797 | buildMultiPartStream(jsonPart, filePart, boundary) { 798 | const eol = '\r\n'; 799 | const bx = `--${boundary}`; 800 | const buffers = [ 801 | // eslint-disable-next-line function-paren-newline 802 | Buffer.from( 803 | // eslint-disable-next-line prefer-template 804 | bx + 805 | eol + 806 | 'Content-Disposition: form-data; name="json_request_part"' + 807 | eol + 808 | 'Content-Type: application/json' + 809 | eol + 810 | eol + 811 | eol + 812 | JSON.stringify(jsonPart) + 813 | eol, 814 | ), 815 | // eslint-disable-next-line function-paren-newline 816 | Buffer.from( 817 | // eslint-disable-next-line prefer-template 818 | bx + 819 | eol + 820 | 'Content-Disposition: form-data; name="file"; filename="' + 821 | filePart.name + 822 | '"' + 823 | eol + 824 | 'Content-Type: ' + 825 | filePart.type + 826 | eol + 827 | eol, 828 | ), 829 | Buffer.from(filePart.content, 'base64'), 830 | Buffer.from(`${eol + bx}--${eol}`), 831 | ]; 832 | return Buffer.concat(buffers); 833 | } 834 | 835 | finishTestItemPromiseStart(itemObj, itemTempId, finishTestItemData) { 836 | itemObj.promiseStart.then( 837 | () => { 838 | const url = ['item', itemObj.realId].join('/'); 839 | this.logDebug(`Finish test item with tempId ${itemTempId}`, itemObj); 840 | this.restClient 841 | .update(url, Object.assign(finishTestItemData, { launchUuid: this.launchUuid })) 842 | .then( 843 | (response) => { 844 | this.logDebug(`Success finish item with tempId ${itemTempId}`, response); 845 | itemObj.resolveFinish(response); 846 | }, 847 | (error) => { 848 | this.logDebug(`Error finish test item with tempId ${itemTempId}`, error); 849 | console.dir(error); 850 | itemObj.rejectFinish(error); 851 | }, 852 | ); 853 | }, 854 | (error) => { 855 | itemObj.rejectFinish(error); 856 | }, 857 | ); 858 | } 859 | } 860 | 861 | module.exports = RPClient; 862 | -------------------------------------------------------------------------------- /lib/rest.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const axiosRetry = require('axios-retry').default; 3 | const http = require('http'); 4 | const https = require('https'); 5 | const logger = require('./logger'); 6 | 7 | const DEFAULT_MAX_CONNECTION_TIME_MS = 30000; 8 | 9 | axiosRetry(axios, { 10 | retryDelay: () => 100, 11 | retries: 10, 12 | retryCondition: axiosRetry.isRetryableError, 13 | }); 14 | 15 | class RestClient { 16 | constructor(options) { 17 | this.baseURL = options.baseURL; 18 | this.headers = options.headers; 19 | this.restClientConfig = options.restClientConfig; 20 | 21 | this.axiosInstance = axios.create({ 22 | timeout: DEFAULT_MAX_CONNECTION_TIME_MS, 23 | headers: this.headers, 24 | ...this.getRestConfig(this.restClientConfig), 25 | }); 26 | 27 | if (this.restClientConfig?.debug) { 28 | logger.addLogger(this.axiosInstance); 29 | } 30 | } 31 | 32 | buildPath(path) { 33 | return [this.baseURL, path].join('/'); 34 | } 35 | 36 | buildPathToSyncAPI(path) { 37 | return [this.baseURL.replace('/v2', '/v1'), path].join('/'); 38 | } 39 | 40 | request(method, url, data, options = {}) { 41 | return this.axiosInstance 42 | .request({ 43 | method, 44 | url, 45 | data, 46 | ...options, 47 | headers: { 48 | HOST: new URL(url).host, 49 | ...options.headers, 50 | }, 51 | }) 52 | .then((response) => response.data) 53 | .catch((error) => { 54 | const errorMessage = error.message; 55 | const responseData = error.response && error.response.data; 56 | throw new Error( 57 | `${errorMessage}${ 58 | responseData && typeof responseData === 'object' 59 | ? `: ${JSON.stringify(responseData)}` 60 | : '' 61 | } 62 | URL: ${url} 63 | method: ${method}`, 64 | ); 65 | }); 66 | } 67 | 68 | getRestConfig() { 69 | if (!this.restClientConfig) return {}; 70 | 71 | const config = Object.keys(this.restClientConfig).reduce((acc, key) => { 72 | if (key !== 'agent') { 73 | acc[key] = this.restClientConfig[key]; 74 | } 75 | return acc; 76 | }, {}); 77 | 78 | if ('agent' in this.restClientConfig) { 79 | const { protocol } = new URL(this.baseURL); 80 | const isHttps = /https:?/; 81 | const isHttpsRequest = isHttps.test(protocol); 82 | config[isHttpsRequest ? 'httpsAgent' : 'httpAgent'] = isHttpsRequest 83 | ? new https.Agent(this.restClientConfig.agent) 84 | : new http.Agent(this.restClientConfig.agent); 85 | } 86 | 87 | return config; 88 | } 89 | 90 | create(path, data, options = {}) { 91 | return this.request('POST', this.buildPath(path), data, { 92 | ...options, 93 | }); 94 | } 95 | 96 | retrieve(path, options = {}) { 97 | return this.request( 98 | 'GET', 99 | this.buildPath(path), 100 | {}, 101 | { 102 | ...options, 103 | }, 104 | ); 105 | } 106 | 107 | update(path, data, options = {}) { 108 | return this.request('PUT', this.buildPath(path), data, { 109 | ...options, 110 | }); 111 | } 112 | 113 | delete(path, data, options = {}) { 114 | return this.request('DELETE', this.buildPath(path), data, { 115 | ...options, 116 | }); 117 | } 118 | 119 | retrieveSyncAPI(path, options = {}) { 120 | return this.request( 121 | 'GET', 122 | this.buildPathToSyncAPI(path), 123 | {}, 124 | { 125 | ...options, 126 | }, 127 | ); 128 | } 129 | } 130 | 131 | module.exports = RestClient; 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reportportal/client-javascript", 3 | "version": "5.4.0", 4 | "description": "ReportPortal client for Node.js", 5 | "author": "ReportPortal.io", 6 | "scripts": { 7 | "build": "npm run clean && tsc", 8 | "clean": "rimraf ./build", 9 | "lint": "eslint ./statistics/**/* ./lib/**/* ./__tests__/**/*", 10 | "format": "npm run lint -- --fix", 11 | "test": "jest", 12 | "test:coverage": "jest --coverage" 13 | }, 14 | "directories": { 15 | "lib": "./lib" 16 | }, 17 | "files": [ 18 | "/lib", 19 | "/statistics", 20 | "/VERSION" 21 | ], 22 | "main": "./lib/report-portal-client", 23 | "engines": { 24 | "node": ">=14.x" 25 | }, 26 | "dependencies": { 27 | "axios": "^1.8.4", 28 | "axios-retry": "^4.1.0", 29 | "glob": "^8.1.0", 30 | "ini": "^2.0.0", 31 | "uniqid": "^5.4.0", 32 | "uuid": "^9.0.1" 33 | }, 34 | "license": "Apache-2.0", 35 | "devDependencies": { 36 | "@types/jest": "^29.5.12", 37 | "@types/node": "^18.19.8", 38 | "@typescript-eslint/eslint-plugin": "5.62.0", 39 | "@typescript-eslint/parser": "^5.62.0", 40 | "eslint": "^7.32.0", 41 | "eslint-config-airbnb-base": "^15.0.0", 42 | "eslint-config-airbnb-typescript": "^17.1.0", 43 | "eslint-config-prettier": "^8.10.0", 44 | "eslint-plugin-import": "^2.29.1", 45 | "eslint-plugin-node": "^11.1.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "jest": "^29.7.0", 48 | "lodash": "^4.17.21", 49 | "nock": "^13.5.0", 50 | "prettier": "^2.8.8", 51 | "rimraf": "^3.0.2", 52 | "ts-jest": "^29.1.5", 53 | "ts-node": "^10.9.2", 54 | "typescript": "^4.9.5" 55 | }, 56 | "contributors": [ 57 | { 58 | "name": "Artsiom Tkachou", 59 | "email": "artsiom_tkachou@epam.com" 60 | }, 61 | { 62 | "name": "Alexey Krylov", 63 | "email": "lexecon117@gmail.com" 64 | } 65 | ], 66 | "homepage": "https://github.com/reportportal/client-javascript", 67 | "repository": { 68 | "type": "git", 69 | "url": "https://github.com/reportportal/client-javascript.git" 70 | }, 71 | "bugs": { 72 | "url": "https://github.com/reportportal/client-javascript/issues" 73 | }, 74 | "keywords": [ 75 | "epam", 76 | "reportportal", 77 | "rp" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /statistics/client-id.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const ini = require('ini'); 4 | const { v4: uuidv4 } = require('uuid'); 5 | const { ENCODING, CLIENT_ID_KEY, RP_FOLDER_PATH, RP_PROPERTIES_FILE_PATH } = require('./constants'); 6 | 7 | const exists = util.promisify(fs.exists); 8 | const readFile = util.promisify(fs.readFile); 9 | const mkdir = util.promisify(fs.mkdir); 10 | const writeFile = util.promisify(fs.writeFile); 11 | 12 | async function readClientId() { 13 | if (await exists(RP_PROPERTIES_FILE_PATH)) { 14 | const propertiesContent = await readFile(RP_PROPERTIES_FILE_PATH, ENCODING); 15 | const properties = ini.parse(propertiesContent); 16 | return properties[CLIENT_ID_KEY]; 17 | } 18 | return null; 19 | } 20 | 21 | async function storeClientId(clientId) { 22 | const properties = {}; 23 | if (await exists(RP_PROPERTIES_FILE_PATH)) { 24 | const propertiesContent = await readFile(RP_PROPERTIES_FILE_PATH, ENCODING); 25 | Object.assign(properties, ini.parse(propertiesContent)); 26 | } 27 | properties[CLIENT_ID_KEY] = clientId; 28 | const propertiesContent = ini.stringify(properties); 29 | await mkdir(RP_FOLDER_PATH, { recursive: true }); 30 | await writeFile(RP_PROPERTIES_FILE_PATH, propertiesContent, ENCODING); 31 | } 32 | 33 | async function getClientId() { 34 | let clientId = await readClientId(); 35 | if (!clientId) { 36 | clientId = uuidv4(undefined, undefined, 0); 37 | try { 38 | await storeClientId(clientId); 39 | } catch (ignore) { 40 | // do nothing on saving error, client ID will be always new 41 | } 42 | } 43 | return clientId; 44 | } 45 | 46 | module.exports = { getClientId }; 47 | -------------------------------------------------------------------------------- /statistics/constants.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const pjson = require('../package.json'); 4 | 5 | const ENCODING = 'utf-8'; 6 | const PJSON_VERSION = pjson.version; 7 | const PJSON_NAME = pjson.name; 8 | const CLIENT_ID_KEY = 'client.id'; 9 | const RP_FOLDER = '.rp'; 10 | const RP_PROPERTIES_FILE = 'rp.properties'; 11 | const RP_FOLDER_PATH = path.join(os.homedir(), RP_FOLDER); 12 | const RP_PROPERTIES_FILE_PATH = path.join(RP_FOLDER_PATH, RP_PROPERTIES_FILE); 13 | const CLIENT_INFO = Buffer.from( 14 | 'Ry1XUDU3UlNHOFhMOmVFazhPMGJ0UXZ5MmI2VXVRT19TOFE=', 15 | 'base64', 16 | ).toString('binary'); 17 | const [MEASUREMENT_ID, API_KEY] = CLIENT_INFO.split(':'); 18 | const EVENT_NAME = 'start_launch'; 19 | 20 | function getNodeVersion() { 21 | // A workaround to avoid reference error in case this is not a Node.js application 22 | if (typeof process !== 'undefined') { 23 | if (process.versions) { 24 | const version = process.versions.node; 25 | if (version) { 26 | return `Node.js ${version}`; 27 | } 28 | } 29 | } 30 | return null; 31 | } 32 | 33 | const INTERPRETER = getNodeVersion(); 34 | 35 | module.exports = { 36 | ENCODING, 37 | EVENT_NAME, 38 | PJSON_VERSION, 39 | PJSON_NAME, 40 | CLIENT_ID_KEY, 41 | RP_FOLDER, 42 | RP_FOLDER_PATH, 43 | RP_PROPERTIES_FILE, 44 | RP_PROPERTIES_FILE_PATH, 45 | MEASUREMENT_ID, 46 | API_KEY, 47 | INTERPRETER, 48 | }; 49 | -------------------------------------------------------------------------------- /statistics/statistics.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { MEASUREMENT_ID, API_KEY, PJSON_NAME, PJSON_VERSION, INTERPRETER } = require('./constants'); 3 | const { getClientId } = require('./client-id'); 4 | 5 | const hasOption = (options, optionName) => { 6 | return Object.prototype.hasOwnProperty.call(options, optionName); 7 | }; 8 | 9 | class Statistics { 10 | constructor(eventName, agentParams) { 11 | this.eventName = eventName; 12 | this.eventParams = this.getEventParams(agentParams); 13 | } 14 | 15 | getEventParams(agentParams) { 16 | const params = { 17 | interpreter: INTERPRETER, 18 | client_name: PJSON_NAME, 19 | client_version: PJSON_VERSION, 20 | }; 21 | if (agentParams && hasOption(agentParams, 'name') && agentParams.name) { 22 | params.agent_name = agentParams.name; 23 | } 24 | if (agentParams && hasOption(agentParams, 'version') && agentParams.version) { 25 | params.agent_version = agentParams.version; 26 | } 27 | return params; 28 | } 29 | 30 | async trackEvent() { 31 | try { 32 | const requestBody = { 33 | client_id: await getClientId(), 34 | events: [ 35 | { 36 | name: this.eventName, 37 | params: this.eventParams, 38 | }, 39 | ], 40 | }; 41 | 42 | await axios.post( 43 | `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_KEY}`, 44 | requestBody, 45 | ); 46 | } catch (error) { 47 | console.error(error.message); 48 | } 49 | } 50 | } 51 | 52 | module.exports = Statistics; 53 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "esModuleInterop": true, 5 | "allowJs": true, 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "strictNullChecks": true, 9 | "downlevelIteration": true, 10 | "declaration": true, 11 | "lib": ["es2016", "es2016.array.include"], 12 | "module": "commonjs", 13 | "target": "ES2015", 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": ["node_modules/*"] 17 | }, 18 | "typeRoots": ["node_modules/@types"], 19 | "outDir": "./build" 20 | }, 21 | "include": [ 22 | "statistics/**/*", "lib/**/*", "__tests__/**/*"], 23 | "exclude": ["node_modules", "__tests__"] 24 | } 25 | -------------------------------------------------------------------------------- /version_fragment: -------------------------------------------------------------------------------- 1 | patch 2 | --------------------------------------------------------------------------------