diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f42edae5..6c03c4d80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: run: npm run test end2end-tests: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout code @@ -32,16 +32,29 @@ jobs: - name: Install run: npm install + - name: Compile + run: npx tsc -p tests\tsconfig.json + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Build static data run: npm run build-mock-static-data - name: Start mock server run: npm run serve-website & + shell: bash - name: Wait for the server run: ./.github/scripts/wait-for-server.ps1 -HostName "http://localhost:8080" shell: pwsh - name: Run tests - run: npm run test-e2e - \ No newline at end of file + run: npx playwright test tests/ --workers 1 + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: playwright-report + path: test-results/ + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6a98625f..fe62fd755 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules/ src/config.design.json src/config.publish.json src/config.runtime.json - +test-results/ +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fd2553228..60a6edd22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ }, "devDependencies": { "@azure/storage-blob": "12.14.0", + "@playwright/test": "1.35.1", "@types/chai": "^4.3.5", "@types/google-maps": "^3.2.3", "@types/knockout": "^3.4.72", @@ -80,6 +81,7 @@ "mini-css-extract-plugin": "^2.7.6", "mocha": "^10.2.0", "path": "^0.12.7", + "playwright": "1.35.1", "postcss-loader": "^7.3.3", "puppeteer": "19.7.5", "querystring-es3": "^0.2.1", @@ -92,7 +94,6 @@ "ts-loader": "^9.4.3", "ts-node": "10.9.1", "typescript": "^4.9.5", - "url-loader": "^4.1.1", "webpack": "5.88.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", @@ -102,6 +103,15 @@ "node": ">=14.18.2" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ardatan/sync-fetch": { "version": "0.0.1", "license": "MIT", @@ -422,20 +432,20 @@ } }, "node_modules/@azure/msal-browser": { - "version": "2.37.1", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.37.1.tgz", - "integrity": "sha512-EoKQISEpIY39Ru1OpWkeFZBcwp6Y0bG81bVmdyy4QJebPPDdVzfm62PSU0XFIRc3bqjZ4PBKBLMYLuo9NZYAow==", + "version": "2.38.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.38.0.tgz", + "integrity": "sha512-gxBh83IumHgEP9uMCm9pJLKLRwICMQTxG9TX3AytdNt3oLUI3tytm/szYD5u5zKJgSkhHvwFSM+NPnM04hYw3w==", "dependencies": { - "@azure/msal-common": "13.1.0" + "@azure/msal-common": "13.2.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.1.0.tgz", - "integrity": "sha512-wj+ULrRB0HTuMmtrMjg8j3guCx32GE2BCPbsMCZkHgL1BZetC3o/Su5UJEQMX1HNc9CrIaQNx5WaKWHygYDe0g==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.2.0.tgz", + "integrity": "sha512-rnstQ7Zgn3fSTKNQO+/YNV34/QXJs0vni7IA0/3QB1EEyrJg14xyRmTqlw9ta+pdSuT5OJwUP8kI3D/rBwUIBw==", "engines": { "node": ">=0.8.0" } @@ -589,9 +599,9 @@ "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==" }, "node_modules/@codemirror/autocomplete": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.8.1.tgz", - "integrity": "sha512-HpphvDcTdOx+9R3eUw9hZK9JA77jlaBF0kOt2McbyfvY0rX9pnMoO8rkkZc0GzSbzhIY4m5xJ0uHHgjfqHNmXQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz", + "integrity": "sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -630,9 +640,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.3.0.tgz", - "integrity": "sha512-tzxOVQNoDhhwFNfcTO2IB74wQoWarARcH6gv3YufPpiJ9yhcb7zD6JCkO5+FWARskqRFc8GFa6E+wUyOvADl5A==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.0.tgz", + "integrity": "sha512-6VZ44Ysh/Zn07xrGkdtNfmHCbGSHZzFBdzWi0pbd7chAQ/iUcpLGX99NYRZTa7Ugqg4kEHCqiHhcZnH0gLIgSg==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -655,9 +665,9 @@ "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" }, "node_modules/@codemirror/view": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.14.0.tgz", - "integrity": "sha512-I263FPs4In42MNmrdwN2DfmYPFMVMXgT7o/mxdGp4jv5LPs8i0FOxzmxF5yeeQdYSTztb2ZhmPIu0ahveInVTg==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.15.3.tgz", + "integrity": "sha512-chNgR8H7Ipx7AZUt0+Kknk7BCow/ron3mHd1VZdM7hQXiI79+UlWqcxpCiexTxZQ+iSkqndk3HHAclJOcjSuog==", "dependencies": { "@codemirror/state": "^6.1.4", "style-mod": "^4.0.0", @@ -717,14 +727,14 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", + "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -740,9 +750,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", - "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", + "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -983,9 +993,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -1027,9 +1037,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.7.tgz", - "integrity": "sha512-ssHKb3p0MxhJXT2i7UBmgAY1BIM3Uq/D772Qutu3EVmxWIyNMU12nQ0rL3Fhu+MiFtiTzyTmd3xGwEf3ON5PSA==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.9.tgz", + "integrity": "sha512-XPz6dzuTHlnsbA5M2DZgjflNQ+9Hi5Swhic0RULdp3oOs3rh6bqGZolosVqN/fQIT8uNiepzINJDnS39oweTHQ==", "dependencies": { "@lezer/common": "^1.0.0" } @@ -1219,6 +1229,11 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" + }, "node_modules/@opentelemetry/api": { "version": "1.0.3", "license": "Apache-2.0", @@ -1363,6 +1378,25 @@ "node": ">=10.12.0" } }, + "node_modules/@playwright/test": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.35.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "license": "MIT", @@ -1631,9 +1665,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + "version": "20.4.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", + "integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==" }, "node_modules/@types/node-fetch": { "version": "2.5.12", @@ -1760,17 +1794,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.0.tgz", - "integrity": "sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/type-utils": "5.60.0", - "@typescript-eslint/utils": "5.60.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", @@ -1794,14 +1828,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.0.tgz", - "integrity": "sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "engines": { @@ -1821,13 +1855,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz", - "integrity": "sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1838,13 +1872,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz", - "integrity": "sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.60.0", - "@typescript-eslint/utils": "5.60.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1865,9 +1899,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.0.tgz", - "integrity": "sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1878,13 +1912,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz", - "integrity": "sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/visitor-keys": "5.60.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1905,17 +1939,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.0.tgz", - "integrity": "sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.60.0", - "@typescript-eslint/types": "5.60.0", - "@typescript-eslint/typescript-estree": "5.60.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, @@ -1931,12 +1965,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.60.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.0.tgz", - "integrity": "sha512-wm9Uz71SbCyhUKgcaPRauBdTegUyY/ZWl8gLwD/i/ybJqscrrdVSFImpvUz16BLPChIeKBK5Fa9s6KDQjsjyWw==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.60.0", + "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -2204,9 +2238,9 @@ } }, "node_modules/acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "bin": { "acorn": "bin/acorn" }, @@ -3516,9 +3550,9 @@ } }, "node_modules/core-js": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", - "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.1.tgz", + "integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -3596,26 +3630,6 @@ "node-fetch": "2.6.7" } }, - "node_modules/cross-fetch/node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -4381,36 +4395,51 @@ } }, "node_modules/editorconfig": { - "version": "0.15.3", - "license": "MIT", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "4.1.5", - "license": "ISC", + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "balanced-match": "^1.0.0" } }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.1", - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "2.1.2", - "license": "ISC" + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/ee-first": { "version": "1.1.1", @@ -4540,15 +4569,15 @@ } }, "node_modules/eslint": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", - "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", + "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", + "@eslint/eslintrc": "^2.1.0", + "@eslint/js": "8.44.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4560,7 +4589,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "espree": "^9.6.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4570,7 +4599,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -4580,9 +4608,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -4620,9 +4647,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", + "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4665,8 +4692,9 @@ }, "node_modules/eslint/node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4676,16 +4704,17 @@ } }, "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, - "license": "MIT", "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -4693,16 +4722,18 @@ }, "node_modules/eslint/node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/eslint/node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -4711,12 +4742,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -5362,11 +5393,6 @@ "dev": true, "license": "ISC" }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -6459,21 +6485,21 @@ } }, "node_modules/jiti": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", - "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", + "integrity": "sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==", "dev": true, "bin": { "jiti": "bin/jiti.js" } }, "node_modules/js-beautify": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.8.tgz", - "integrity": "sha512-4S7HFeI9YfRvRgKnEweohs0tgJj28InHVIj4Nl8Htf96Y6pHg3+tJrmo4ucAM9f7l4SHbFI3IvFAZ2a1eQPbyg==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.9.tgz", + "integrity": "sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==", "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", + "editorconfig": "^1.0.3", "glob": "^8.1.0", "nopt": "^6.0.0" }, @@ -6897,9 +6923,9 @@ "license": "MIT" }, "node_modules/liquidjs": { - "version": "10.8.3", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.8.3.tgz", - "integrity": "sha512-LqHLYtH3vrkT3LyfOhPU0FJX5KPO4aB6SzGa4HRI29yz8pS0ZxqIe/fWtic8qiust1+qrHI92J67tdt92V4WOA==", + "version": "10.8.4", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.8.4.tgz", + "integrity": "sha512-HSpYAFBVWxhwWsTKPBJgPm3bnwwIzAZjy17XhX7uJCKJ8H6A1YstZSFmPqMmWfSuJOg43RSx+qWVSA1Fu3+B2w==", "dependencies": { "commander": "^10.0.0" }, @@ -7934,9 +7960,9 @@ } }, "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", "dev": true, "engines": { "node": ">=8" @@ -8139,9 +8165,8 @@ } }, "node_modules/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "version": "2.6.7", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8528,37 +8553,28 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.1.tgz", + "integrity": "sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^7.14.1", + "minipass": "^4.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", - "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.1.tgz", - "integrity": "sha512-NQ8MCKimInjVlaIqx51RKJJB7mINVkLTJbsZKmto4UAAOC/CWXES8PGaOgoBZyqoUsUA/U3DToGK7GJkkHbjJw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" } }, "node_modules/path-to-regexp": { @@ -8692,6 +8708,34 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", + "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.35.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright-core": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/plumb": { "version": "0.1.0", "license": "MIT" @@ -8772,9 +8816,9 @@ } }, "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "version": "8.4.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", + "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", "dev": true, "funding": [ { @@ -8953,9 +8997,8 @@ }, "node_modules/progress": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -9008,9 +9051,9 @@ } }, "node_modules/prosemirror-model": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", - "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.3.tgz", + "integrity": "sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==", "dependencies": { "orderedmap": "^2.0.0" } @@ -9044,9 +9087,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.31.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.5.tgz", - "integrity": "sha512-tobRCDeCp61elR1d97XE/JTL9FDIfswZpWeNs7GKJjAJvWyMGHWYFCq29850p6bbG2bckP+i9n1vT56RifosbA==", + "version": "1.31.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.6.tgz", + "integrity": "sha512-wwgErp+EWnuW4kGAYKrt90hhOetaoWpYNdOpnuQMXo1m4x/+uhauFeQoCCm8J30ZqAa4LgIER4yzKSO545gRfA==", "dependencies": { "prosemirror-model": "^1.16.0", "prosemirror-state": "^1.0.0", @@ -9075,10 +9118,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/pseudomap": { - "version": "1.0.2", - "license": "ISC" - }, "node_modules/psl": { "version": "1.8.0", "license": "MIT" @@ -9154,13 +9193,13 @@ } }, "node_modules/puppeteer-core/node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.0.tgz", + "integrity": "sha512-EAZejC7JvnQINayvB/7BJbpZpNOJ8Lrw2OZNEvQxe0vaLn1SuwMcfV7/MNaX8L/T0wmptBFI4YMtDvSBxYDc7w==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", + "minimatch": "^7.4.1", "minipass": "^4.2.4", "path-scurry": "^1.6.1" }, @@ -9172,15 +9211,15 @@ } }, "node_modules/puppeteer-core/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.2.tgz", + "integrity": "sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9204,27 +9243,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/puppeteer/node_modules/cosmiconfig": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.0.tgz", @@ -9699,9 +9717,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.63.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", - "integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==", + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", + "integrity": "sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -9805,9 +9823,9 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9994,10 +10012,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sigmund": { - "version": "1.0.1", - "license": "ISC" - }, "node_modules/signal-exit": { "version": "3.0.6", "license": "ISC" @@ -10394,9 +10408,9 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz", - "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==", + "version": "5.19.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", + "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -10515,9 +10529,9 @@ "license": "MIT" }, "node_modules/ts-loader": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.3.tgz", - "integrity": "sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==", + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -10881,32 +10895,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-loader": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "file-loader": "*", - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "file-loader": { - "optional": true - } - } - }, "node_modules/util": { "version": "0.10.4", "dev": true, @@ -11386,6 +11374,27 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-merge": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", @@ -11514,9 +11523,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 85638b6e1..b62c2320d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "build-publisher": "webpack --config webpack.publisher.js", "build-runtime": "webpack --config webpack.runtime.js", "build-function": "webpack --config webpack.function.js", - "test-e2e": "node node_modules/mocha/bin/_mocha -r mocha.js tests/e2e/**/*.spec.ts --timeout 3000000", "test": "node node_modules/mocha/bin/_mocha -r mocha.js src/**/*.spec.ts", "deploy-function": "npm run build-function && cd dist/function && func azure functionapp publish < function app name >", "publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js && npm run serve-website", @@ -38,7 +37,6 @@ "@types/mime": "^3.0.1", "@types/mocha": "10.0.1", "@types/node": "^20.3.1", - "@types/puppeteer": "5.4.7", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "autoprefixer": "^10.4.14", @@ -54,7 +52,6 @@ "mocha": "^10.2.0", "path": "^0.12.7", "postcss-loader": "^7.3.3", - "puppeteer": "19.7.5", "querystring-es3": "^0.2.1", "raw-loader": "^4.0.2", "sass": "^1.63.6", @@ -65,11 +62,12 @@ "ts-loader": "^9.4.3", "ts-node": "10.9.1", "typescript": "^4.9.5", - "url-loader": "^4.1.1", "webpack": "5.88.0", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", - "webpack-merge": "5.9.0" + "webpack-merge": "5.9.0", + "playwright": "1.35.1", + "@playwright/test": "1.35.1" }, "dependencies": { "@azure/api-management-custom-widgets-scaffolder": "^1.0.0-beta.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..60cb5ee52 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + retries: 2, + use: { + video: 'retain-on-failure' + } +}); \ No newline at end of file diff --git a/src/config.validate.json b/src/config.validate.json index c92daf9f1..3cae59bb4 100644 --- a/src/config.validate.json +++ b/src/config.validate.json @@ -1,5 +1,6 @@ { "environment": "validation", + "isLocalRun": true, "root": "http://localhost:8080", "urls": { "home": "/", diff --git a/src/services/usersService.ts b/src/services/usersService.ts index bb18faf8c..3c2cca2fd 100644 --- a/src/services/usersService.ts +++ b/src/services/usersService.ts @@ -255,7 +255,6 @@ export class UsersService { */ public async ensureSignedIn(): Promise { const userId = await this.getCurrentUserId(); - if (!userId) { this.navigateToSignin(); return; // intentionally exiting without resolving the promise. diff --git a/tests/constants.ts b/tests/constants.ts deleted file mode 100644 index 2ebf3f5f2..000000000 --- a/tests/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { LaunchOptions, BrowserLaunchArgumentOptions, BrowserConnectOptions } from "puppeteer"; - -export const BrowserLaunchOptions: LaunchOptions & BrowserLaunchArgumentOptions & BrowserConnectOptions = { - headless: true, - ignoreHTTPSErrors: true, - product: "chrome", - devtools: true, - userDataDir: "/puppeteer-data-dir", // necessary for persistent user preferences - args: [ '--incognito' ] -}; \ No newline at end of file diff --git a/tests/e2e/maps/apis.ts b/tests/e2e/maps/apis.ts index 3e8ad5041..a78f62ca9 100644 --- a/tests/e2e/maps/apis.ts +++ b/tests/e2e/maps/apis.ts @@ -1,10 +1,15 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; export class ApisWidget { constructor(private readonly page: Page) { } - public async apis(): Promise { - await this.page.waitForSelector("api-list div.table div.table-body div.table-row"); + public async waitRuntimeInit(): Promise { + await this.page.locator("api-list").waitFor(); + + } + + public async getApiByName(apiName: string): Promise { + return await this.page.locator('api-list div.table div.table-body div.table-row a').filter({ hasText: apiName }).first().innerText(); } public async getApisCount(): Promise { diff --git a/tests/e2e/maps/home.ts b/tests/e2e/maps/home.ts new file mode 100644 index 000000000..a438a0860 --- /dev/null +++ b/tests/e2e/maps/home.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from "playwright"; + +export class HomePageWidget { + constructor(private readonly page: Page) { } + + public async waitRuntimeInit(): Promise { + await this.getWelcomeMessageLocator().waitFor(); + } + + public getWelcomeMessageLocator(): Locator { + return this.page.locator("h1 span").filter({ hasText: "Welcome to Contoso!" }); + } +} \ No newline at end of file diff --git a/tests/e2e/maps/products.ts b/tests/e2e/maps/products.ts index 09f5885d3..e71ce179c 100644 --- a/tests/e2e/maps/products.ts +++ b/tests/e2e/maps/products.ts @@ -1,15 +1,24 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; export class ProductseWidget { constructor(private readonly page: Page) { } - public async products(): Promise { - await this.page.waitForSelector("product-list-runtime div.table div.table-body div.table-row"); + public async waitRuntimeInit(): Promise { + await this.page.locator("product-list-runtime").waitFor(); } - public async getProductsCount(): Promise { - return await this.page.evaluate(() => - document.querySelector("product-list-runtime div.table div.table-body div.table-row")?.parentElement?.childElementCount - ); + public async getProductByName(productName: string): Promise { + return await this.page.locator('product-list-runtime div.table div.table-body div.table-row a').filter({ hasText: productName }).first().innerText(); + } + + public async goToProductPage(baseUrl, productId: string): Promise{ + await this.page.goto(`${baseUrl}/product#product=${productId}`, { waitUntil: 'domcontentloaded' }); + } + + public async subscribeToProduct(baseUrl, productId: string, subscriptionName: string): Promise { + await this.goToProductPage(baseUrl, productId); + await this.page.waitForSelector("product-subscribe-runtime form button"); + await this.page.type("product-subscribe-runtime form input", subscriptionName); + await this.page.click("product-subscribe-runtime form button"); } } \ No newline at end of file diff --git a/tests/e2e/maps/profile.ts b/tests/e2e/maps/profile.ts index 5f01a092c..1d4e535d5 100644 --- a/tests/e2e/maps/profile.ts +++ b/tests/e2e/maps/profile.ts @@ -1,15 +1,86 @@ -import { Page } from "puppeteer"; +import { Locator, Page } from "playwright"; export class ProfileWidget { constructor(private readonly page: Page) { } - public async profile(): Promise { + public async waitRuntimeInit(): Promise { await this.page.waitForSelector("profile-runtime .row"); await this.page.waitForSelector("subscriptions-runtime .table-row"); } - public async getUserEmail(): Promise { - await this.page.waitForSelector("[data-bind='text: user().email']"); - return await this.page.evaluate(() =>document.querySelector("[data-bind='text: user().email']")?.textContent); + public getUserEmailLocator(): Locator { + return this.page.locator("profile-runtime [data-bind='text: user().email']").first(); + } + + public async getUserEmail(): Promise { + return await this.getUserEmailLocator().innerText(); + } + + public getUserFirstNameLocator(): Locator { + return this.page.locator("profile-runtime [data-bind='text: user().firstName']").first(); + } + + public async getUserFirstName(): Promise { + return await this.getUserFirstNameLocator().innerText(); + } + + public getUserLastNameLocator(): Locator { + return this.page.locator("profile-runtime [data-bind='text: user().lastName']").first(); + } + + public async getUserLastName(): Promise { + return await this.getUserLastNameLocator().innerText(); + } + + public getUserRegistrationDataLocator(): Locator { + return this.page.locator("profile-runtime [data-bind='text: registrationDate']").first(); + } + + public async getUserRegistrationDate(): Promise { + return await this.getUserRegistrationDataLocator().innerText(); + } + + public getSubscriptionRow(subscriptionName: string): Locator { + return this.page.locator("subscriptions-runtime div.table div.table-body div.table-row", { has: this.page.locator("div.row span[data-bind='text: model.name']").filter({ hasText: subscriptionName })}); + } + + public async getSubscriptioPrimarynKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + const primaryKeyElement = subscriptionRow.locator('code[data-bind="text: primaryKey"]').first(); + return await primaryKeyElement.textContent(); + } + + public async getSubscriptioSecondarynKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + const primaryKeyElement = subscriptionRow?.locator('code[data-bind="text: secondaryKey"]').first(); + return await primaryKeyElement.textContent(); + } + + public async togglePrimarySubscriptionKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + await subscriptionRow?.locator("a.btn-link[aria-label='Show primary key']", { hasText: "Show" }).click(); + } + + public async toggleSecondarySubscriptionKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + await subscriptionRow?.locator("a.btn-link[aria-label='Show Secondary key']", { hasText: "Show" }).click(); + } + + public async getListOfLocatorsToHide(): Promise { + const primaryKeyElements = await this.page.locator('code[data-bind="text: primaryKey"]').all(); + const secondaryKeyElements = await this.page.locator('code[data-bind="text: secondaryKey"]').all(); + const productNames = this.page.locator('span[data-bind="text: model.productName"]'); + const subscriptionNames = this.page.locator('span[data-bind="text: model.name"]'); + const subscriptionStartDates = this.page.locator('span[data-bind="text: $parent.timeToString(model.startDate)"]'); + return primaryKeyElements.concat(secondaryKeyElements).concat(productNames).concat(this.getUserProfileData()).concat(subscriptionNames).concat(subscriptionStartDates); + } + + public getUserProfileData(): Locator[] { + return [ + this.getUserEmailLocator(), + this.getUserFirstNameLocator(), + this.getUserLastNameLocator(), + this.getUserRegistrationDataLocator() + ]; } } \ No newline at end of file diff --git a/tests/e2e/maps/signin-basic.ts b/tests/e2e/maps/signin-basic.ts index d661c3ddb..0ae6368cc 100644 --- a/tests/e2e/maps/signin-basic.ts +++ b/tests/e2e/maps/signin-basic.ts @@ -1,12 +1,14 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; +import { User } from "../../mocks/collection/user"; export class SignInBasicWidget { - constructor(private readonly page: Page) { } + constructor(private readonly page: Page, private readonly configuration: object) { } - public async signInWithBasic(): Promise { - await this.page.type("#email", "foo@bar.com"); - await this.page.type("#password", "password"); + public async signInWithBasic(userInfo: User): Promise { + await this.page.goto(this.configuration['urls']['signin'], { waitUntil: 'domcontentloaded' }); + + await this.page.type("#email", userInfo.email); + await this.page.type("#password", userInfo.password); await this.page.click("#signin"); - await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); } } \ No newline at end of file diff --git a/tests/e2e/maps/signin-social.ts b/tests/e2e/maps/signin-social.ts deleted file mode 100644 index 2dff54add..000000000 --- a/tests/e2e/maps/signin-social.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Page } from "puppeteer"; - -export class SignInSocialWidget { - constructor(private readonly page: Page) { } - - public async signInWitAadB2C(): Promise { - await this.page.click("#signinB2C"); - - await new Promise((resolve) => { - this.page.once("popup", async (popup) => { - await popup.waitForSelector("[type=email]"); - await popup.type("[type=email]", "foo@bar.com"); - await popup.type("[type=password]", "password"); - await popup.click("#next"); - - popup.on("close", () => resolve()); - - await new Promise(resolve => setTimeout(resolve, 50000000)); // just long wait - }); - }); - - await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); - } -} \ No newline at end of file diff --git a/tests/e2e/maps/signup-basic.ts b/tests/e2e/maps/signup-basic.ts index 5d3153688..c559a23d5 100644 --- a/tests/e2e/maps/signup-basic.ts +++ b/tests/e2e/maps/signup-basic.ts @@ -1,14 +1,21 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; +import { User } from "../../mocks/collection/user"; export class SignupBasicWidget { constructor(private readonly page: Page) { } - public async signUpWithBasic(): Promise { - await this.page.type("#email", "foo@bar.com"); - await this.page.type("#password", "password"); - await this.page.type("#confirmPassword", "password"); - await this.page.type("#firstName", "Foo"); - await this.page.type("#lastName", "Bar"); + public async signUpWithBasic(user: User): Promise { + await this.page.type("#email", user.email); + await this.page.type("#password", user.password); + await this.page.type("#confirmPassword", user.password); + await this.page.type("#firstName", user.firstName); + await this.page.type("#lastName", user.lastName); + + var captchaTextBox = await this.page.evaluate(() => document.getElementById("captchaValue")); + if (captchaTextBox) { + console.log("Captcha is enabled and should be passed with the sign up request."); + } + await this.page.click("#signup"); } diff --git a/tests/e2e/playwright-test.ts b/tests/e2e/playwright-test.ts new file mode 100644 index 000000000..ebc79997a --- /dev/null +++ b/tests/e2e/playwright-test.ts @@ -0,0 +1,80 @@ +import { test as base } from '@playwright/test'; +import { TestUtils } from '../testUtils'; +import { TestApiService } from '../services/testApiService'; +import { TestUserService } from '../services/testUserService'; +import { TestProductService } from '../services/testProductService'; +import { ITestRunner } from '../services/ITestRunner'; +import { TestRunnerMock } from '../services/testRunnerMock'; +import { TestRunner } from '../services/testRunner'; + +let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array, apiService: TestApiService, userService: TestUserService, productService: TestProductService, testRunner: ITestRunner }>({ + configuration: [async ({}, use) => { + let configuration = {}; + configuration = await TestUtils.getConfigAsync(); + await use(configuration); + }, { scope: 'worker' }], + + testRunner: [async ({}, use) => { + let testRunner: ITestRunner; + if (!(await TestUtils.IsLocalEnv())){ + testRunner = new TestRunner(); + }else{ + testRunner = new TestRunnerMock(); + } + await use(testRunner); + }, { scope: 'worker' }], + + apiService: [async ({}, use) => { + let apiService = new TestApiService(); + await use(apiService); + }, { scope: 'worker' }], + + productService: [async ({}, use) => { + let productService = new TestProductService(); + await use(productService); + }, { scope: 'worker' }], + + userService: [async ({}, use) => { + let userService = new TestUserService(); + await use(userService); + }, { scope: 'worker' }], + + cleanUp: [async ({}, use) => { + let cleanUp: Array = []; + await use(cleanUp); + }, { scope: 'worker' }], + + page: async ({ page }, use) => { + page.on("console", (message) => { + if(message.type() === "error"){ + console.error(message.text()); + } + }); + await use(page); + }, +}); + + +export const test = configurationTest.extend({ + mockedData: async ({ }, use, testInfo) => { + let testTitle = `${testInfo.titlePath[1]}-${testInfo.titlePath[2]}`; + var dataToUse = TestUtils.getTestData(testTitle); + let mockedData = {}; + mockedData["data"] = dataToUse; + mockedData["testName"] = testTitle; + await use(mockedData); + }, +}); + +test.beforeEach(async ( { cleanUp } ) => { + cleanUp = []; +}); + +test.afterEach(async ( { cleanUp } ) => { + for (const cleanUpFunction of cleanUp) { + await cleanUpFunction(); + } +}); + + +export { expect } from '@playwright/test'; \ No newline at end of file diff --git a/tests/e2e/runtime/apis.spec.ts b/tests/e2e/runtime/apis.spec.ts index e56becc3e..8ab587d2f 100644 --- a/tests/e2e/runtime/apis.spec.ts +++ b/tests/e2e/runtime/apis.spec.ts @@ -1,43 +1,36 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { Server } from "http"; -import { Apis } from "../../mocks/collection/apis"; -import { Api } from "../../mocks/collection/api"; -import { ApisWidget } from "../maps/apis"; - -describe("Apis page", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can see apis on the page", async () =>{ - var apis = new Apis(); - apis.addApi(Api.getRandomApi()); - apis.addApi(Api.getRandomApi()); - server = Utils.createMockServer([apis.getApisListResponse()]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - - await page.goto(config.urls.apis); - - const apiWidget = new ApisWidget(page); - await apiWidget.apis(); - - expect(await apiWidget.getApisCount()).to.equal(apis.apiList.length); - } - - await Utils.startTest(server, validate); - }); +import { Product } from "../../mocks/collection/product"; +import { ApisWidget } from "../maps/apis"; +import { test, expect } from '../playwright-test'; +import { Api } from "../../mocks/collection/api"; +import { Templating } from "../../templating"; + +test.describe("apis-page", async () => { + test("published-apis-visible-to-guests", async function ({page, configuration, cleanUp, mockedData, productService, apiService, testRunner}) { + var product1: Product = Product.getRandomProduct("product1"); + var api: Api = Api.getRandomApi("api1"); + + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), api); + + async function populateData(): Promise{ + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/guests"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + + await apiService.putApi("apis/"+api.apiId, api.getContract()); + await apiService.putApiProduct("products/"+product1.productId, "apis/"+api.apiId); + cleanUp.push(async () => apiService.deleteApi("apis/"+api.apiId)); + } + + async function validate(){ + await page.goto(configuration['urls']['apis'], { waitUntil: 'domcontentloaded' }); + + const apiWidget = new ApisWidget(page); + await apiWidget.waitRuntimeInit(); + + var apiHtml = await apiWidget.getApiByName(api.apiName); + expect(apiHtml).not.toBe(null); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); }); \ No newline at end of file diff --git a/tests/e2e/runtime/products.spec.ts b/tests/e2e/runtime/products.spec.ts index bff2b0f66..2d8b4bc53 100644 --- a/tests/e2e/runtime/products.spec.ts +++ b/tests/e2e/runtime/products.spec.ts @@ -1,42 +1,35 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { Server } from "http"; -import { Products } from "../../mocks/collection/products"; import { Product } from "../../mocks/collection/product"; import { ProductseWidget } from "../maps/products"; +import { test, expect } from '../playwright-test'; +import { Templating } from "../../templating"; -describe("Products page", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can see producst on the page", async () => { - var products = new Products(); - products.addProduct(Product.getStartedProduct()); - products.addProduct(Product.getUnlimitedProduct()); +test.describe("products-page", async () => { + test("published-products-visible-to-guests", async function ({page, configuration, cleanUp, mockedData, productService, testRunner}) { + var product1: Product = Product.getRandomProduct("product1"); + var product2: Product = Product.getRandomProduct("product2"); - server = Utils.createMockServer([products.getProductListResponse()]); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), product1, product2); - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await page.goto(config.urls.products); + async function populateData(): Promise{ + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/guests"); + await productService.putProduct("products/"+product2.productId, product2.getContract()); + await productService.putProductGroup("products/"+product2.productId, "groups/guests"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + cleanUp.push(async () => productService.deleteProduct("products/"+product2.productId, true)); + } + + async function validate(){ + await page.goto(configuration['urls']['products'], { waitUntil: 'domcontentloaded' }); const productWidget = new ProductseWidget(page); - await productWidget.products(); - - expect(await productWidget.getProductsCount()).to.equal(products.productList.length); + await productWidget.waitRuntimeInit(); + var product1Html = await productWidget.getProductByName(product1.productName); + var product2Html = await productWidget.getProductByName(product2.productName); + expect(product1Html).not.toBe(null); + expect(product2Html).not.toBe(null); } - await Utils.startTest(server, validate); + + await testRunner.runTest(validate, populateData, mockedData.data); }); }); \ No newline at end of file diff --git a/tests/e2e/runtime/profile.spec.ts b/tests/e2e/runtime/profile.spec.ts deleted file mode 100644 index d52a4e646..000000000 --- a/tests/e2e/runtime/profile.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { ProfileWidget } from "../maps/profile"; -import { signIn } from "./signin.spec"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -describe("User profile", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can visit his profile page", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([ userInfo.getSignInResponse(), userInfo.getUserInfoResponse()]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - - await signIn(page, config); - expect(page.url()).to.equal(config.urls.home); - - await page.goto(config.urls.profile); - - const profileWidget = new ProfileWidget(page); - await profileWidget.profile(); - - expect(await profileWidget.getUserEmail()).to.equal(userInfo.email); - } - await Utils.startTest(server, validate); - }); -}); \ No newline at end of file diff --git a/tests/e2e/runtime/signin.spec.ts b/tests/e2e/runtime/signin.spec.ts deleted file mode 100644 index b8c77f2eb..000000000 --- a/tests/e2e/runtime/signin.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { SignInBasicWidget } from "../maps/signin-basic"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -export async function signIn(page: puppeteer.Page, config: any): Promise { - await page.goto(config.urls.signin); - - const signInWidget = new SignInBasicWidget(page); - await signInWidget.signInWithBasic(); -} - -describe("User sign-in flow", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - Utils.closeServer(server); - await browser.close(); - }); - - it("User can sign-in with basic credentials", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([userInfo.getSignInResponse()]); - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await signIn(page, config); - expect(page.url()).to.equal(config.urls.home); - } - - await Utils.startTest(server, validate); - }); -}); \ No newline at end of file diff --git a/tests/e2e/runtime/signup.spec.ts b/tests/e2e/runtime/signup.spec.ts deleted file mode 100644 index ff892e5f7..000000000 --- a/tests/e2e/runtime/signup.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { BrowserLaunchOptions } from "../../constants"; -import { Utils } from "../../utils"; -import { SignupBasicWidget } from "../maps/signup-basic"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -describe("User sign-up flow", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can sign-up with basic credentials", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([userInfo.getUserRegisterResponse("email", "name", "lastname")]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await page.goto(config.urls.signup); - - const signUpWidget = new SignupBasicWidget(page); - await signUpWidget.signUpWithBasic(); - - expect(await signUpWidget.getConfirmationMessageValue()) - .to.equal("Follow the instructions from the email to verify your account."); - } - - await Utils.startTest(server, validate); - }); - -}); \ No newline at end of file diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts b/tests/e2e/runtime/user-subscriptions.spec.ts new file mode 100644 index 000000000..c120a493c --- /dev/null +++ b/tests/e2e/runtime/user-subscriptions.spec.ts @@ -0,0 +1,64 @@ +import { test, expect } from "../playwright-test"; +import { SignInBasicWidget } from "../maps/signin-basic"; +import { ProfileWidget } from "../maps/profile"; +import { HomePageWidget } from "../maps/home"; +import { ProductseWidget } from "../maps/products"; +import { User } from "../../mocks/collection/user"; +import { Subscription } from "../../mocks/collection/subscription"; +import { Templating } from "../../templating"; +import { Product } from "../../mocks/collection/product"; + +test.describe("user-resources", async () => { + test("user-can-subscribe-to-product-and-see-subscription-key", async function ({page, configuration, cleanUp, mockedData, productService, userService, testRunner}) { + // data init + var userInfo: User = User.getRandomUser("user1"); + var product1: Product = Product.getRandomProduct("product1"); + var subscription: Subscription = Subscription.getRandomSubscription("subscription1", userInfo, product1); + + //mocked data for local runtime + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo, product1, subscription); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/developers"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + } + + async function validate(){ + // widgets init + const signInWidget = new SignInBasicWidget(page, configuration); + const profileWidget = new ProfileWidget(page); + const productsWidget = new ProductseWidget(page); + const homePageWidget = new HomePageWidget(page); + + //sign in + await signInWidget.signInWithBasic(userInfo); + await homePageWidget.waitRuntimeInit(); + + // subscribe to product + await page.goto(configuration['urls']['products']+"/"+product1.productId, { waitUntil: 'domcontentloaded' }); + await productsWidget.subscribeToProduct(configuration['root'], product1.productId, subscription.displayName); + await profileWidget.waitRuntimeInit(); + + // check subscription primary key + var subscriptionPrimaryKeyHidden = await profileWidget.getSubscriptioPrimarynKey(subscription.displayName); + await profileWidget.togglePrimarySubscriptionKey(subscription.displayName); + var subscriptionPrimaryKeyShown = await profileWidget.getSubscriptioPrimarynKey(subscription.displayName); + expect(subscriptionPrimaryKeyHidden).not.toBe(subscriptionPrimaryKeyShown); + + // check subscription secondary key + var subscriptionSecondaryKeyHidden = await profileWidget.getSubscriptioSecondarynKey(subscription.displayName); + await profileWidget.toggleSecondarySubscriptionKey(subscription.displayName); + var subscriptionSecondaryKeyShown = await profileWidget.getSubscriptioSecondarynKey(subscription.displayName); + expect(subscriptionSecondaryKeyHidden).not.toBe(subscriptionSecondaryKeyShown); + + // check profile page screenshot with mocked data for profile page + expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: [configuration['isLocalRun'] === true ? 'self-hosted': 'deployed', 'user-resources.jpeg'], maxDiffPixels: 20}); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); +}); \ No newline at end of file diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg new file mode 100644 index 000000000..6ee9f2a38 Binary files /dev/null and b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg differ diff --git a/tests/e2e/runtime/user.spec.ts b/tests/e2e/runtime/user.spec.ts new file mode 100644 index 000000000..e89c41086 --- /dev/null +++ b/tests/e2e/runtime/user.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from "../playwright-test"; +import { SignInBasicWidget } from "../maps/signin-basic"; +import { ProfileWidget } from "../maps/profile"; +import { HomePageWidget } from "../maps/home"; +import { SignupBasicWidget } from "../maps/signup-basic"; +import { User } from "../../mocks/collection/user"; +import { Templating } from "../../templating"; + +test.describe("user-sign-in", async () => { + test("user-can-sign-in-with-basic-credentials", async function ({page, configuration, cleanUp, mockedData, userService, testRunner}) { + var userInfo = User.getRandomUser("user1"); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + const signInWidget = new SignInBasicWidget(page, configuration); + const homePageWidget = new HomePageWidget(page); + + await signInWidget.signInWithBasic(userInfo); + await homePageWidget.waitRuntimeInit(); + await page.close(); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); + + + test("user-can-visit-his-profile-page", async ({page, configuration, cleanUp, mockedData, userService, testRunner}) => { + var userInfo = User.getRandomUser("user1"); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + const signInWidget = new SignInBasicWidget(page, configuration); + const homePageWidget = new HomePageWidget(page); + + await signInWidget.signInWithBasic(userInfo); + await homePageWidget.waitRuntimeInit(); + + await page.goto(configuration['urls']['profile'], { waitUntil: 'domcontentloaded' }); + + const profileWidget = new ProfileWidget(page); + await profileWidget.waitRuntimeInit(); + + expect(await profileWidget.getUserEmail()).toBe(userInfo.email); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); + + + test.skip("user-can-sign-up-with-basic-credentials", async ({page, configuration, cleanUp, mockedData, userService, testRunner}) => { + var userInfo = User.getRandomUser("user1"); + + async function populateData(): Promise{ + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + await page.goto(configuration['urls']['signup']); + + await page.waitForTimeout(5000); + const signUpWidget = new SignupBasicWidget(page); + await signUpWidget.signUpWithBasic(userInfo); + expect(await signUpWidget.getConfirmationMessageValue()).toBe("Follow the instructions from the email to verify your account.") + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); +}); \ No newline at end of file diff --git a/tests/mapiClient.ts b/tests/mapiClient.ts new file mode 100644 index 000000000..4d6a12389 --- /dev/null +++ b/tests/mapiClient.ts @@ -0,0 +1,113 @@ +import * as Constants from "../src/constants"; +import { TestUtils } from "./testUtils"; +import { HttpClient, HttpRequest, HttpResponse, HttpMethod, HttpHeader } from "@paperbits/common/http"; +import { XmlHttpRequestClient } from "@paperbits/common/http"; +import { KnownHttpHeaders } from "../src/models/knownHttpHeaders"; +import { KnownMimeTypes } from "../src/models/knownMimeTypes"; + +export class MapiClient { + private managementApiUrl: string; + private static _instance: MapiClient; + private token: string; + private constructor( + private readonly httpClient: HttpClient + ) { + + } + + private async initialize(): Promise { + const settings = await TestUtils.getConfigAsync(); + this.token = settings["accessToken"]; + this.managementApiUrl = settings["managementUrl"]; + } + + public static get Instance() + { + return this._instance || (this._instance = new this(new XmlHttpRequestClient())); + } + + private async requestInternal(httpRequest: HttpRequest): Promise { + await this.initialize(); + if (!httpRequest.url) { + throw new Error("Request URL cannot be empty."); + } + httpRequest.url = this.managementApiUrl + httpRequest.url; + + httpRequest.headers = httpRequest.headers || []; + + if (httpRequest.body && !httpRequest.headers.some(x => x.name === KnownHttpHeaders.ContentType)) { + httpRequest.headers.push({ name: KnownHttpHeaders.ContentType, value: KnownMimeTypes.Json }); + } + + httpRequest.headers.push({ name: KnownHttpHeaders.Authorization, value: `${this.token}` }); + + if (!httpRequest.headers.some(x => x.name === "Accept")) { + httpRequest.headers.push({ name: "Accept", value: "*/*" }); + } + + if (typeof (httpRequest.body) === "object") { + httpRequest.body = JSON.stringify(httpRequest.body); + } + + const call = async () => this.makeRequest(httpRequest); + return await call(); + } + + protected async makeRequest(httpRequest: HttpRequest): Promise { + const url = new URL(httpRequest.url); + if (!url.searchParams.has("api-version")) { + httpRequest.url = TestUtils.addQueryParameter(httpRequest.url, `api-version=${Constants.managementApiVersion}`); + } + + let response: HttpResponse; + + try { + response = await this.httpClient.send(httpRequest); + } + catch (error) { + throw new Error(`Unable to complete request. Error: ${error.message}`); + } + + return await this.handleResponse(response, httpRequest.url); + } + + private async handleResponse(response: HttpResponse, url: string): Promise { + let contentType = ""; + + if (response.headers) { + const contentTypeHeader = response.headers.find(h => h.name.toLowerCase() === "content-type"); + contentType = contentTypeHeader ? contentTypeHeader.value.toLowerCase() : ""; + } + + const text = response.toText(); + if (response.statusCode >= 200 && response.statusCode < 300) { + if (contentType.includes("json") && text.length > 0) { + return JSON.parse(text) as T; + } + else { + return text; + } + }else{ + throw new Error(`Unable to complete request. Status: ${response.statusCode}. Error: ${text}`); + } + return null; + } + + public async put(url: string, headers?: HttpHeader[], body?: any): Promise { + return await this.requestInternal({ + method: HttpMethod.put, + url: url, + headers: headers, + body: body + }); + } + + public async delete(url: string, headers?: HttpHeader[], body?: any): Promise { + return await this.requestInternal({ + method: HttpMethod.delete, + url: url, + headers: headers, + body: body + }); + } +} \ No newline at end of file diff --git a/tests/mocks/collection/api.ts b/tests/mocks/collection/api.ts index 9789f6039..88ecacce2 100644 --- a/tests/mocks/collection/api.ts +++ b/tests/mocks/collection/api.ts @@ -1,58 +1,50 @@ -import { Utils } from "../../utils"; +import { TestUtils } from "../../testUtils"; +import { ApiContract } from "../../../src/contracts/api"; +import { Resource } from "./resource"; -export class Api{ +export class Api extends Resource{ public apiId: string; public apiName: string; + public path: string; + public protocols: string[] = ["https"]; + public responseContract: ApiContract; - public constructor(apiId: string, apiName: string){ + public constructor(testId: string ,apiId: string, apiName: string, path: string, protocols?: string[]){ + super(testId); this.apiId = apiId; this.apiName = apiName; + this.path = path; + this.protocols = protocols || this.protocols; + + this.responseContract = this.getResponseContract(); } - - public getApiResponse(){ - let response = {}; - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": this.getApiBodyResponse() + + private getProperties(): any{ + return { + displayName: this.apiName, + description: "", + subscriptionRequired: true, + path: this.path, + protocols: this.protocols, }; - return response; } - - public getApiBodyResponse(){ + + public getContract(): ApiContract { return { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`, - "type": "Microsoft.ApiManagement/service/apis", - "name": this.apiId, - "properties": { - "displayName": this.apiName, - "apiRevision": "1", - "description": null, - "subscriptionRequired": true, - "serviceUrl": "http://echoapi.cloudapp.net/api", - "path": "echo", - "protocols": [ - "https" - ], - "authenticationSettings": null, - "subscriptionKeyParameterNames": null, - "isCurrent": true - } + properties: this.getProperties() }; } - public static getEchoApi(){ - return new Api("echo-api", "Echo api"); + public getResponseContract(): ApiContract{ + return { + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`, + type: "Microsoft.ApiManagement/service/apis", + name: this.apiId, + properties: this.getProperties() + }; } - public static getRandomApi(){ - return new Api(Utils.randomIdentifier(), Utils.randomIdentifier()); + public static getRandomApi(testId: string){ + return new Api(testId, TestUtils.randomIdentifier(), TestUtils.randomIdentifier(), TestUtils.randomIdentifier()); } } diff --git a/tests/mocks/collection/apis.ts b/tests/mocks/collection/apis.ts deleted file mode 100644 index 0251e83c4..000000000 --- a/tests/mocks/collection/apis.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Api } from "./api"; -export class Apis { - public apiList: Api[] - - public constructor(){ - this.apiList = []; - } - - public addApi(api: Api){ - this.apiList.push(api); - } - - public getApisListResponse(){ - let values: Object[] = []; - - this.apiList.forEach(api => { - values.push(api.getApiBodyResponse()); - }); - - return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - value: values, - count: values.length - } - } - }; - } -} diff --git a/tests/mocks/collection/product.ts b/tests/mocks/collection/product.ts index 056abd08e..1ace6c4d2 100644 --- a/tests/mocks/collection/product.ts +++ b/tests/mocks/collection/product.ts @@ -1,51 +1,52 @@ -export class Product{ +import { ProductContract } from "../../../src/contracts/product"; +import { TestUtils } from "../../testUtils"; +import { Resource } from "./resource"; + +export class Product extends Resource{ public productId: string; public productName: string; + public responseContract: ProductContract; - public constructor(productId: string, productName: string){ + public constructor(testId: string, productId: string, productName: string){ + super(testId); this.productId = productId; this.productName = productName; + this.responseContract = this.getResponseContract(); } - - public getProductResponse(){ - let response = {}; - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": this.getProductBodyResponse() + + private getProperties(): any{ + return { + displayName: this.productName, + description: "", + approvalRequired: false, + state: "published", + subscriptionRequired: true, + subscriptionsLimit: 2, + terms: "" + } + } + + public getContract(): ProductContract{ + return { + properties: this.getProperties() }; - return response; } - public getProductBodyResponse(){ + public getResponseContract(): ProductContract{ return { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`, - "type": "Microsoft.ApiManagement/service/products", - "name": this.productId, - "properties": { - "displayName": this.productName, - "description": "Subscribers will be able to run 5 calls/minute up to a maximum of 100 calls/week.", - "terms": "", - "subscriptionRequired": true, - "approvalRequired": false, - "subscriptionsLimit": 1, - "state": "published" - } + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`, + type: "Microsoft.ApiManagement/service/products", + name: this.productId, + properties: this.getProperties() }; } - public static getStartedProduct(){ - return new Product("starter", "Starter"); + public getRequestId(): string{ + return `products/${this.productId}`; } - public static getUnlimitedProduct(){ - return new Product("unlimited", "Unlimited"); + public static getRandomProduct(testId: string){ + var productName = TestUtils.randomIdentifier(); + return new Product(testId, productName, productName); } } diff --git a/tests/mocks/collection/products.ts b/tests/mocks/collection/products.ts deleted file mode 100644 index 4d286667b..000000000 --- a/tests/mocks/collection/products.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Product } from "./product"; -export class Products { - public productList: Product[] - - public constructor(){ - this.productList = []; - } - - public addProduct(product: Product){ - this.productList.push(product); - } - - public getProductListResponse(){ - let values: Object[] = []; - - this.productList.forEach(product => { - values.push(product.getProductBodyResponse()); - }); - - return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - value: values, - count: values.length - } - } - }; - } -} diff --git a/tests/mocks/collection/resource.ts b/tests/mocks/collection/resource.ts new file mode 100644 index 000000000..ae5d0a962 --- /dev/null +++ b/tests/mocks/collection/resource.ts @@ -0,0 +1,7 @@ +export class Resource{ + public testId: string; + + public constructor(testId: string){ + this.testId = testId; + } +} \ No newline at end of file diff --git a/tests/mocks/collection/subscription.ts b/tests/mocks/collection/subscription.ts new file mode 100644 index 000000000..c51bb463c --- /dev/null +++ b/tests/mocks/collection/subscription.ts @@ -0,0 +1,71 @@ +import { SubscriptionContract } from "../../../src/contracts/subscription"; +import { TestUtils } from "../../testUtils"; +import { Resource } from "./resource"; +import { User } from "./user"; +import { Product } from "./product"; + +export class Subscription extends Resource{ + public displayName: string; + public id: string; + + public user: User; + public product: Product; + + public responseContract: any; + + public constructor(testId: string, user: User, product: Product, id: string, displayName: string){ + super(testId); + this.displayName = displayName; + this.id = id; + + this.user = user; + this.product = product; + + this.responseContract = this.getResponseContract(); + } + + public getResponseContract(): any{ + return { + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.user.publicId}/subscriptions/${this.id}`, + type: "Microsoft.ApiManagement/service/users/subscriptions", + name: this.id, + properties: { + ownerId: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.user.publicId}`, + scope: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.product.productId}`, + displayName: this.displayName, + state: "active", + createdDate: new Date().toISOString(), + startDate: new Date().toISOString(), + expirationDate: new Date().toISOString(), + endDate: null, + notificationDate: new Date().toISOString(), + stateComment: null, + } + } + } + + public getContract(): SubscriptionContract{ + return { + properties: { + displayName: this.displayName, + createdDate: new Date().toISOString(), + endDate: new Date().toISOString(), + expirationDate: new Date().toISOString(), + notificationDate: new Date().toISOString(), + primaryKey: TestUtils.randomIdentifier(10), + scope: TestUtils.randomIdentifier(5), + secondaryKey: TestUtils.randomIdentifier(10), + startDate: new Date().toISOString(), + state: "active", + stateComment: TestUtils.randomIdentifier(10), + ownerId: TestUtils.randomIdentifier(5) + } + }; + } + + public static getRandomSubscription(testId: string, user: User, product: Product){ + var displayName = TestUtils.randomIdentifier(5); + var id = TestUtils.randomIdentifier(5); + return new Subscription(testId, user, product, id, displayName); + } +} diff --git a/tests/mocks/collection/user.ts b/tests/mocks/collection/user.ts index 2e791cf65..d28749ef3 100644 --- a/tests/mocks/collection/user.ts +++ b/tests/mocks/collection/user.ts @@ -1,108 +1,74 @@ -import {Utils} from "../../utils"; -export class UserMockData{ - public email; - public publicId; +import { UserContract } from "../../../src/contracts/user"; +import { TestUtils } from "../../testUtils"; +import { Resource } from "./resource"; - public constructor(){ - this.email = "example@example.example"; - this.publicId = "example-example-example"; - } +export class User extends Resource{ + public email: string; + public publicId: string; + public firstName: string; + public lastName: string; + public password: string; + + public accessToken: string; + public responseContract: any; - public getSignInResponse(){ + public constructor(testId: string, email: string, publicId: string, firstName: string, lastName: string, password: string){ + super(testId); + this.email = email; + this.publicId = publicId; + this.firstName = firstName; + this.lastName = lastName; + this.password = password; + this.accessToken = TestUtils.getSharedAccessToken(this.publicId, TestUtils.randomIdentifier(), 1); + + this.responseContract = this.getResponseContract(); + } + + private getProperties(): any{ return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-9" - }, - { - "name": "ocp-apim-sas-token", - "value": Utils.getSharedAccessToken(this.publicId, "accesskey", 1) - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - "id": this.publicId - } - } + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + state: "active", + password: this.password, + appType: "developerPortal" }; } - public getUserInfoResponse(){ - let response = {}; - - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`, - "type": "Microsoft.ApiManagement/service/users", - "name": this.publicId, - "properties": { - "firstName": "name", - "lastName": "surname", - "email": this.email, - "state": "active", - "registrationDate": "2021-11-08T15:45:18.01Z", - "note": null, - "groups": [], - "identities": [ - { - "provider": "Basic", - "id": this.publicId - } - ] - } - } + public getRequestContract(): UserContract{ + return { + properties: this.getProperties() }; - return response; } - public getUserRegisterResponse(email: string, firstName: string, lastName: string){ + public getResponseContract(): any{ return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - }, - { - "name": "location", - "value": `/users/${email}?api-version=2021-04-01-preview` - } - ], - "statusCode": 201, - "statusText": "Created", - "body": { - "id": `/users/${email}`, - "firstName": firstName, - "lastName": lastName, - "email": email, - "state": "pending", - "registrationDate": "2022-02-04T13:42:26.36Z", - "note": null, - "groups": [ + id: `/subscriptions/000/resourceGroups/000/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`, + type: "Microsoft.ApiManagement/service/users", + name: this.publicId, + properties: { + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + state: "active", + registrationDate : "2021-01-17T19:07:23.67Z", + note: null, + identities: [ { - "id": "/groups/developers", - "name": "Developers", - "description": "Developers is a built-in group. Its membership is managed by the system. Signed-in users fall into this group.", - "builtIn": true, - "type": "system", - "externalId": null + provider: "Basic", + id: this.email } - ], - "identities": [] - } + ] } - } + }; + } + + public static getRandomUser(testId: string){ + var email = `${TestUtils.randomIdentifier(4, false)}@${TestUtils.randomIdentifier(4, false)}.${TestUtils.randomIdentifier(4, false)}`; + var publicId = `${TestUtils.randomIdentifier(3)}-${TestUtils.randomIdentifier(3)}-${TestUtils.randomIdentifier(3)}`; + var firstName = TestUtils.randomIdentifier(3); + var lastName = TestUtils.randomIdentifier(3); + var password = TestUtils.randomIdentifier(10); + return new User(testId, email, publicId, firstName, lastName, password); } } diff --git a/tests/mocks/mockServerData.json b/tests/mocks/mockServerData.json new file mode 100644 index 000000000..03ba11b76 --- /dev/null +++ b/tests/mocks/mockServerData.json @@ -0,0 +1,133 @@ +{ + "products-page-published-products-visible-to-guests": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ + "statusCode": 200, + "body": { + "value": [ + "object{{product1.responseContract}}", + "object{{product2.responseContract}}" + ], + "count": 2 + } + } + }, + "user-sign-in-user-can-sign-in-with-basic-credentials": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "test-contoso-com" + } + } + }, + "user-sign-in-user-can-visit-his-profile-page": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "{{user1.publicId}}" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}":{ + "statusCode": 200, + "body": "object{{user1.responseContract}}" + } + }, + "apis-page-published-apis-visible-to-guests": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis":{ + "statusCode": 200, + "body": { + "value": [ + "object{{api1.responseContract}}" + ], + "count": 1 + } + } + }, + "user-resources-user-can-subscribe-to-product-and-see-subscription-key":{ + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "{{user1.publicId}}" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}":{ + "statusCode": 200, + "body": "object{{user1.responseContract}}" + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/{{product1.productId}}":{ + "statusCode": 200, + "body": "object{{product1.responseContract}}" + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ + "statusCode": 200, + "body": { + "value": [ + "object{{product1.responseContract}}" + ], + "count": 1 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/{{product1.productId}}/apis":{ + "statusCode": 200, + "body": { + "value": [ + + ], + "count": 0 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/tenant/settings":{ + "statusCode": 200, + "body": { + + "id": "/settings/public", + "settings": { + "CustomPortalSettings.UserRegistrationTerms": null, + "CustomPortalSettings.UserRegistrationTermsEnabled": "False", + "CustomPortalSettings.UserRegistrationTermsConsentRequired": "False", + "CustomPortalSettings.DelegationEnabled": "False", + "CustomPortalSettings.DelegationUrl": "", + "CustomPortalSettings.DelegatedSubscriptionEnabled": "False" + } + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions":{ + "statusCode": 200, + "body": { + "value": [ + "object{{subscription1.responseContract}}" + ], + "count": 1 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions/{{subscription1.id}}/listSecrets":{ + "statusCode": 200, + "body": { + "primaryKey": "primaryKey", + "secondaryKey": "secondaryKey" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions/.*":{ + "methods": ["PUT", "OPTIONS"], + "statusCode": 201, + "body": {} + } + } +} \ No newline at end of file diff --git a/tests/services/ITestApiService.ts b/tests/services/ITestApiService.ts new file mode 100644 index 000000000..f03a35213 --- /dev/null +++ b/tests/services/ITestApiService.ts @@ -0,0 +1,9 @@ +import { ApiContract } from "../../src/contracts/api"; +export interface ITestApiService { + + putApi(apiId: string, apiContract: ApiContract): Promise ; + + putApiProduct(productId: string, apiId: string): Promise ; + + deleteApi(apiId: string): Promise ; +} \ No newline at end of file diff --git a/tests/services/ITestProductService.ts b/tests/services/ITestProductService.ts new file mode 100644 index 000000000..4ceacc76d --- /dev/null +++ b/tests/services/ITestProductService.ts @@ -0,0 +1,9 @@ +import { ProductContract } from "../../src/contracts/product"; +export interface ITestProductService { + + putProduct(productId: string, productContract: ProductContract): Promise; + + putProductGroup(productId: string, groupId: string): Promise; + + deleteProduct(productId: string, deleteSubs: boolean): Promise; +} \ No newline at end of file diff --git a/tests/services/ITestRunner.ts b/tests/services/ITestRunner.ts new file mode 100644 index 000000000..ec498c3c9 --- /dev/null +++ b/tests/services/ITestRunner.ts @@ -0,0 +1,3 @@ +export interface ITestRunner { + runTest(...args: any): Promise; +} \ No newline at end of file diff --git a/tests/services/ITestUserService.ts b/tests/services/ITestUserService.ts new file mode 100644 index 000000000..a6d2a5d02 --- /dev/null +++ b/tests/services/ITestUserService.ts @@ -0,0 +1,7 @@ +import { UserContract } from "../../src/contracts/user"; +export interface ITestUserService { + + putUser(userId: string, userContract: UserContract): Promise ; + + deleteUser(userId: string, deleteSubs: boolean): Promise ; +} \ No newline at end of file diff --git a/tests/services/testApiService.ts b/tests/services/testApiService.ts new file mode 100644 index 000000000..e697de8e1 --- /dev/null +++ b/tests/services/testApiService.ts @@ -0,0 +1,43 @@ +import { ApiContract } from "../../src/contracts/api"; +import { MapiClient } from "../mapiClient"; +import { ITestApiService } from "./ITestApiService"; + +export class TestApiService implements ITestApiService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putApi(apiId: string, apiContract: ApiContract): Promise { + if (!apiId) { + throw new Error(`Parameter "apiId" not specified.`); + } + + const contract = await this.mapiClient.put(apiId, [], apiContract); + return contract; + } + + public async putApiProduct(productId: string, apiId: string): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (!apiId) { + throw new Error(`Parameter "groupId" not specified.`); + } + + var result = await this.mapiClient.put(productId + "/" + apiId, undefined); + return result; + } + + public async deleteApi(apiId: string): Promise { + + if (!apiId) { + throw new Error(`Parameter "productId" not specified.`); + } + + var result = await this.mapiClient.delete(apiId, undefined); + return result; + + } +} \ No newline at end of file diff --git a/tests/services/testProductService.ts b/tests/services/testProductService.ts new file mode 100644 index 000000000..c25005ea2 --- /dev/null +++ b/tests/services/testProductService.ts @@ -0,0 +1,45 @@ +import { MapiClient } from "../mapiClient"; +import { ProductContract } from "../../src/contracts/product"; +import { ITestProductService } from "./ITestProductService"; + +export class TestProductService implements ITestProductService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putProduct(productId: string, productContract: ProductContract): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + const contract = await this.mapiClient.put(productId, undefined, productContract); + return contract; + } + + public async putProductGroup(productId: string, groupId: string): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (!groupId) { + throw new Error(`Parameter "groupId" not specified.`); + } + + var result = await this.mapiClient.put(productId + "/" + groupId, undefined); + return result; + } + + public async deleteProduct(productId: string, deleteSubs: boolean): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (deleteSubs){ + productId = productId + "?deleteSubscriptions=True"; + } + + var result = await this.mapiClient.delete(productId, undefined); + return result; + } +} \ No newline at end of file diff --git a/tests/services/testRunner.ts b/tests/services/testRunner.ts new file mode 100644 index 000000000..7dc8529e0 --- /dev/null +++ b/tests/services/testRunner.ts @@ -0,0 +1,16 @@ +import { ITestRunner } from "./ITestRunner"; +export class TestRunner implements ITestRunner { + public runTest(validate: () => Promise, populateData: () => Promise, testName: string ): Promise { + return new Promise((resolve, reject) => { + populateData().then(() => { + validate().then(() => { + resolve(); + }).catch((err) => { + reject(err); + }); + }).catch((err) => { + reject(err); + }); + }); + } +} \ No newline at end of file diff --git a/tests/services/testRunnerMock.ts b/tests/services/testRunnerMock.ts new file mode 100644 index 000000000..f37d09473 --- /dev/null +++ b/tests/services/testRunnerMock.ts @@ -0,0 +1,38 @@ +import { TestUtils } from "../testUtils"; +import { ITestRunner } from "./ITestRunner"; + +export class TestRunnerMock implements ITestRunner { + public runTest(validate: () => Promise, populateData: () => Promise, data: object ): Promise { + return new Promise(async (resolve, reject) => { + + let server = TestUtils.createMockServer(data); + let error = undefined; + server.on("ready", () => { + validate().then(() => { + console.log("validation done"); + resolve(); + }).catch((err) => { + error = err; + reject(err); + console.log("server error"); + }).finally(() => { + server.close(); + console.log("server closed"); + }); + }); + + server.on("close", () => { + if (error != undefined){ + reject(error); + } else { + resolve(); + } + }); + + server.listen(8181,"127.0.0.1", function(){ + console.log("server started"); + server.emit("ready"); + }); + }); + } +} \ No newline at end of file diff --git a/tests/services/testUserService.ts b/tests/services/testUserService.ts new file mode 100644 index 000000000..a936180a5 --- /dev/null +++ b/tests/services/testUserService.ts @@ -0,0 +1,30 @@ +import { MapiClient } from "../mapiClient"; +import { UserContract } from "../../src/contracts/user"; +import { ITestUserService } from "./ITestUserService"; + +export class TestUserService implements ITestUserService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putUser(userId: string, userContract: UserContract): Promise { + if (!userId) { + throw new Error(`Parameter "userId" not specified.`); + } + + const contract = await this.mapiClient.put(userId, undefined, userContract); + return contract; + } + + public async deleteUser(userId: string, deleteSubs: boolean): Promise { + if (!userId) { + throw new Error(`Parameter "productId" not specified.`); + } + if (deleteSubs === true) { + userId = `${userId}?deleteSubscriptions=true`; + } + var result = await this.mapiClient.delete(userId, undefined); + return result; + } +} \ No newline at end of file diff --git a/tests/templating.ts b/tests/templating.ts new file mode 100644 index 000000000..92d78c85b --- /dev/null +++ b/tests/templating.ts @@ -0,0 +1,20 @@ +import { Resource } from "./mocks/collection/resource"; + +export class Templating { + public static updateTemplate(templateData: string, ...resources: Resource[]): object{ + for(let i = 0; i < resources.length; i++){ + let objectKeys = Object.keys(resources[i]); + + let testId = resources[i].testId; + + for (const key of objectKeys) { + let regex = new RegExp(`"object{{${testId}.${key}}}"`, "g"); + templateData = templateData.replace(regex, JSON.stringify(resources[i][key])); + + let regexString = new RegExp(`{{${testId}.${key}}}`, "g"); + templateData = templateData.replace(regexString, resources[i][key]); + } + } + return JSON.parse(templateData); + } +} \ No newline at end of file diff --git a/tests/testUtils.ts b/tests/testUtils.ts new file mode 100644 index 000000000..dd16506b7 --- /dev/null +++ b/tests/testUtils.ts @@ -0,0 +1,127 @@ +import * as fs from "fs"; +import * as crypto from "crypto"; +import * as http from "http"; + +export class TestUtils { + public static async getConfigAsync(): Promise { + const configFile = await fs.promises.readFile("./src/config.validate.json", { encoding: "utf-8" }); + const validationConfig = JSON.parse(configFile); + Object.keys(validationConfig.urls).forEach(key => { + validationConfig.urls[key] = validationConfig.root + validationConfig.urls[key]; + }); + + return validationConfig; + } + + public static getTestData(testKey: string): object { + const configFile = fs.readFileSync("./tests/mocks/mockServerData.json", { encoding: "utf-8" }); + const validationConfig = JSON.parse(configFile); + if(validationConfig[testKey] == undefined){ + throw new Error(`Test data not found for ${testKey}`); + } + return validationConfig[testKey]; + } + + public static async IsLocalEnv(): Promise { + let config = await TestUtils.getConfigAsync(); + return config["isLocalRun"] === true; + } + + public static addQueryParameter(uri: string, name: string, value?: string): string { + uri += `${uri.indexOf("?") >= 0 ? "&" : "?"}${name}`; + if (value) { + uri += `=${value}`; + } + return uri; + } + + public static getSharedAccessToken(apimUid: string, apimAccessKey: string, validDays: number): string { + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + validDays); + + const expiry = expiryDate.toISOString().replace(/\d+.\d+Z/, "00.0000000Z"); + const expiryShort = expiryDate.toISOString().substr(0, 16).replace(/[^\d]/g, ""); + const signature = crypto.createHmac("sha512", apimAccessKey).update(`${apimUid}\n${expiry}`).digest("base64"); + const sasToken = `SharedAccessSignature ${apimUid}&${expiryShort}&${signature}`; + + return sasToken; + } + + public static randomIdentifier(length: number = 8, includeNumers = true): string { + let result = ""; + let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + if (includeNumers){ + characters = characters + "0123456789"; + } + + const charactersLength = characters.length; + + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + + return result; + } + + public static createMockServer(responses?: Object) { + var obj = {...responses} ?? {}; + for (const key in obj) { + var newKey = key; + + if (obj[key]["methods"] && obj[key]["methods"].length > 0){ + const methods = `(${obj[key]["methods"].join("|")})`; + newKey = `${methods}/${key}`; + }else{ + newKey = `(GET|POST|PUT|DELETE|OPTIONS)/${key}`; + } + obj[key]['regex'] = new RegExp("^" + newKey + "$"); + } + + var server = http + .createServer((req, res) => { + const urlWithoutParameters = req.url?.split("?")[0]; + res.setHeader("Access-Control-Allow-Methods", "*"); + res.setHeader("Access-Control-Allow-Credentials", "true"); + res.setHeader("Access-Control-Allow-Headers", "*"); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Expose-Headers", "*"); + + var urlToSearch = `${req.method}/${urlWithoutParameters}`; + var response = null; + + for (const key in obj) { + if (obj[key]['regex'].test(urlToSearch)) { + response = {...obj[key]}; + delete response['regex']; + break; + } + } + + if (response != null && response != undefined) { + // default header response, the specified header + res.setHeader("Content-Type", "application/json"); + + if (response.headers && response.headers.length > 0){ + response.headers.forEach(element => { + res.setHeader(element.name, element.value); + }); + } + + res.writeHead(response.statusCode); + res.write(Buffer.from(JSON.stringify(response.body))); + res.end(); + } else { + res.writeHead(404); + res.write("Page not found"); + res.end(); + } + }); + + return server; + } + public static closeServer(server){ + if (server != null){ + server.close(); + } + } +} diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 000000000..2b95584ce --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "es2019" + ], + "module": "commonjs", + "moduleResolution": "node", + "noStrictGenericChecks": true, + "ignoreDeprecations": "5.0", + "removeComments": true, + "noLib": false, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowUnreachableCode": true, + "sourceMap": true, + "incremental": false, + "resolveJsonModule": true, + "noEmit": false + }, + "include": [ + "../node_modules/@paperbits/common/http/*", + ] +} diff --git a/tests/utils.ts b/tests/utils.ts deleted file mode 100644 index c0369819f..000000000 --- a/tests/utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as fs from "fs"; -import * as crypto from "crypto"; -import * as http from "http"; -import { ConsoleMessage, Page } from 'puppeteer'; - -export class Utils { - public static async getConfig(): Promise { - const configFile = await fs.promises.readFile("./src/config.validate.json", { encoding: "utf-8" }); - const validationConfig = JSON.parse(configFile); - Object.keys(validationConfig.urls).forEach(key => { - validationConfig.urls[key] = validationConfig.root + validationConfig.urls[key]; - }); - - return validationConfig; - } - - public static getSharedAccessToken(apimUid: string, apimAccessKey: string, validDays: number): string { - const expiryDate = new Date(); - expiryDate.setDate(expiryDate.getDate() + validDays); - - const expiry = expiryDate.toISOString().replace(/\d+.\d+Z/, "00.0000000Z"); - const expiryShort = expiryDate.toISOString().substr(0, 16).replace(/[^\d]/g, ""); - const signature = crypto.createHmac("sha512", apimAccessKey).update(`${apimUid}\n${expiry}`).digest("base64"); - const sasToken = `SharedAccessSignature ${apimUid}&${expiryShort}&${signature}`; - - return sasToken; - } - - public static randomIdentifier(length: number = 8): string { - let result = ""; - const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; - - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - - return result; - } - - public static createMockServer(responses?: Object[]) { - var obj = {}; - if (responses?.length){ - for (let responseObj of responses) { - obj = {...obj, ...responseObj }; - } - } - - var server = http - .createServer((req, res) => { - const urlWithoutParameters = req.url?.split("?")[0]; - res.setHeader("Access-Control-Allow-Methods", "*"); - res.setHeader("Access-Control-Allow-Credentials", "true"); - res.setHeader("Access-Control-Allow-Headers", "*"); - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Expose-Headers", "*"); - if (urlWithoutParameters && obj[urlWithoutParameters] != undefined) { - var response = obj[urlWithoutParameters]; - var headers = {}; - response.headers.forEach(element => { - res.setHeader(element.name, element.value); - headers[element.name] = element.value; - }); - - res.writeHead(response.statusCode); - res.write(Buffer.from(JSON.stringify(response.body))); - res.end(); - } else { - res.writeHead(404); - res.write("Page not found"); - res.end(); - } - }); - - return server; - } - public static closeServer(server){ - if (server != null){ - server.close(); - } - } - - public static startTest(server, validate): Promise{ - return new Promise((resolve, reject) => { - server.on("ready", () => { - validate().then(() => { - resolve(); - }).catch((err) => { - reject(err); - }); - }); - - server.listen(8181,"127.0.0.1", function(){ - server.emit("ready"); - }); - }); - } - - public static async getBrowserNewPage(browser): Promise{ - const page = await browser.newPage(); - - page.on('console', async (message: ConsoleMessage) => { - if (message.type() === 'error') { - console.error(message.text()); - } - }); - - return page; - } - -} diff --git a/tsconfig.json b/tsconfig.json index 3f7a7e655..caae9103f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "module": "commonjs", "moduleResolution": "node", "noStrictGenericChecks": true, + "ignoreDeprecations": "5.0", "removeComments": true, "noLib": false, "skipLibCheck": true,