diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 367cc180a..bec1e9d35 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -80,6 +80,28 @@ jobs: NODE_OPTIONS: --max_old_space_size=16384 run: xvfb-run npm run test:coverage --silent + # Non-cluster UI tests don't need Kind - run them before it starts so they + # don't have to share the runner with an already-running local cluster. + - name: Prepare UI Tests + env: + NODE_OPTIONS: --max_old_space_size=16384 + if: runner.os == 'Linux' + run: | + # Patch vscode:prepublish to reinstall secretlint AFTER prune + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json')); + pkg.scripts['vscode:prepublish'] += ' && npm install --ignore-scripts @secretlint/node'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); + " + npm run ui-test:prepare + + - name: Run Non-Cluster UI Tests + env: + NODE_OPTIONS: --max_old_space_size=16384 + if: runner.os == 'Linux' + run: xvfb-run --server-args="-screen 0 1920x1080x24" npm run public-ui-test + - name: Start KinD cluster if: (success() || failure()) && runner.os == 'Linux' uses: helm/kind-action@v1.14.0 @@ -102,21 +124,15 @@ jobs: nb=`kubectl get pods -n operators --no-headers --ignore-not-found | grep Running | wc -l` done echo CLUSTER_URL=`kubectl cluster-info | sed -n -e "s/\x1B\[[0-9;]*[a-zA-Z]//g" -e '1s/.*running at \(.*\)$/\1/p'` >> $GITHUB_ENV - # UI tests fail under linux - # Run UI tests - - name: Run UI Tests + + # Runs regardless of whether the non-cluster UI tests passed - each npm script + # launches its own fresh VS Code/browser session, so a crash or failure in the + # non-cluster suite doesn't take the cluster suite's results down with it. + - name: Run Cluster UI Tests env: NODE_OPTIONS: --max_old_space_size=16384 - if: runner.os == 'Linux' - run: | - # Patch vscode:prepublish to reinstall secretlint AFTER prune - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('package.json')); - pkg.scripts['vscode:prepublish'] += ' && npm install --ignore-scripts @secretlint/node'; - fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); - " - xvfb-run --server-args="-screen 0 1920x1080x24" npm run public-ui-test + if: (success() || failure()) && runner.os == 'Linux' + run: xvfb-run --server-args="-screen 0 1920x1080x24" npm run public-ui-kind-test - name: Build and run integration tests if: (success() || failure()) && runner.os == 'Linux' diff --git a/.vscode/launch.json b/.vscode/launch.json index 23b3f98eb..0b4fc81b7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -104,6 +104,31 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" }, + { + "name": "Debug UI Kind Tests", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/extest", + "args": [ + "run-tests", + "${workspaceFolder}/out/test/ui/public-ui-kind-test.js", + "-o", + "${workspaceFolder}/test/ui/settings.json", + "-m", + "${workspaceFolder}/test/ui/.mocharc.js", + "-e", + "${workspaceFolder}/test-resources/extensions", + "-c", + "max" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "sourceMaps": true, + "preLaunchTask": "setup-tests", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, { "type": "chrome", "request": "launch", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43c1d3578..eadc4420e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,6 +173,11 @@ then run: npm run public-ui-test ``` +There's also `npm run public-ui-kind-test`, which covers the subset of public-facing +UI tests that need a cluster but don't need real OpenShift-specific features (e.g. +operator-backed services, component commands). CI runs this against a local `kind` +cluster; it's separate from `cluster-ui-test` below, which needs a real OpenShift login. + In order to run the cluster-dependant UI tests, you need an OpenShift cluster where you can create and delete projects. This means that OpenShift Developer Sandbox won't work. diff --git a/package.json b/package.json index d9050e93a..83cda3d29 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,9 @@ "test-integration": "npm run test:prepare && node ./build/run-tests.cjs integration", "test-integration:coverage": "npm run test:prepare && npm run test:instrument && node ./build/run-tests.cjs integration", "cluster-ui-test": ". test/ui/extester.env && extest setup-tests -e ./test-resources/extensions -i && npm run test:prepare && extest run-tests out/test/ui/cluster-ui-test.js -o test/ui/settings.json -m test/ui/.mocharc.js -e ./test-resources/extensions", - "public-ui-test": ". test/ui/extester.env && extest setup-tests -e ./test-resources/extensions -i && npm run test:prepare && extest run-tests out/test/ui/public-ui-test.js -o test/ui/settings.json -m test/ui/.mocharc.js -e ./test-resources/extensions" + "ui-test:prepare": ". test/ui/extester.env && extest setup-tests -e ./test-resources/extensions -i && npm run test:prepare", + "public-ui-test": "extest run-tests out/test/ui/public-ui-test.js -o test/ui/settings.json -m test/ui/.mocharc.js -e ./test-resources/extensions", + "public-ui-kind-test": "extest run-tests out/test/ui/public-ui-kind-test.js -o test/ui/settings.json -m test/ui/.mocharc.js -e ./test-resources/extensions" }, "dependencies": { "jsonc-parser": "^3.3.1", diff --git a/src/explorer.ts b/src/explorer.ts index 0f75f10a3..6fe851331 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -186,7 +186,11 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos // eslint-disable-next-line class-methods-use-this async getTreeItem(element: ExplorerItem): Promise { try { - return await this._getTreeItem(element); + const item = await this._getTreeItem(element); + if (item.id === undefined) { + item.id = OpenShiftExplorer.computeElementId(element); + } + return item; } catch (err) { // eslint-disable-next-line no-console console.error('getTreeItem failed:', err, element); @@ -200,6 +204,19 @@ export class OpenShiftExplorer implements TreeDataProvider, Dispos } } + // Without a stable id, VS Code correlates tree nodes across refreshes by object + // reference; since getChildren() constructs fresh objects on every call, any refresh + // racing a pending context-menu command invalidates its argument to undefined. + private static computeElementId(element: ExplorerItem): string | undefined { + const withKind = element as { kind?: string; metadata?: { name?: string; uid?: string } }; + if (typeof withKind?.kind !== 'string' || typeof withKind.metadata?.name !== 'string') { + return undefined; + } + return withKind.metadata.uid + ? `${withKind.kind}:${withKind.metadata.uid}` + : `${withKind.kind}:${withKind.metadata.name}`; + } + // eslint-disable-next-line class-methods-use-this async _getTreeItem(element: ExplorerItem): Promise { if ('command' in element || ('label' in element && 'iconPath' in element)) { diff --git a/src/openshift/project.ts b/src/openshift/project.ts index 6448600d4..7cfc0f4ad 100644 --- a/src/openshift/project.ts +++ b/src/openshift/project.ts @@ -125,11 +125,15 @@ export class Project extends OpenShiftItem implements Disposable { @vsCommand('openshift.project.delete', false) @vsCommand('openshift.namespace.delete', false) static async del(project: KubernetesObject, context?: { oc?: Oc }): Promise { + const kind = await getNamespaceKind(); + if (!project?.metadata?.name) { + throw new VsCommandError(`Failed to delete ${kind}: no ${kind.toLowerCase()} selected`, `Failed to delete ${kind}`); + } + let result: Promise = null; const oc = context?.oc ?? Oc.Instance; const projectObjects = await oc.getAllKubernetesObjects(project.metadata.name); const isProjectEmpty = !projectObjects || projectObjects.length === 0; - const kind = await getNamespaceKind(); const value = await window.showWarningMessage(`Do you want to delete ${kind} '${project.metadata.name}'${!isProjectEmpty ? ' and all its contents' : ''}?`, 'Yes', 'Cancel'); if (value === 'Yes') { result = Progress.execFunctionWithProgress( diff --git a/test/fixtures/components/nodejs-starter/.gitignore b/test/fixtures/components/nodejs-starter/.gitignore new file mode 100644 index 000000000..67045665d --- /dev/null +++ b/test/fixtures/components/nodejs-starter/.gitignore @@ -0,0 +1,104 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port diff --git a/test/fixtures/components/nodejs-starter/Dockerfile b/test/fixtures/components/nodejs-starter/Dockerfile new file mode 100644 index 000000000..1b0c1340c --- /dev/null +++ b/test/fixtures/components/nodejs-starter/Dockerfile @@ -0,0 +1,23 @@ +# Install the app dependencies in a full Node docker image +FROM registry.access.redhat.com/ubi8/nodejs-18:latest + +# Copy package.json, and optionally package-lock.json if it exists +COPY package.json package-lock.json* ./ + +# Install app dependencies +RUN \ + if [ -f package-lock.json ]; then npm ci; \ + else npm install; \ + fi + +# Copy the dependencies into a Slim Node docker image +FROM registry.access.redhat.com/ubi8/nodejs-18-minimal:latest + +# Install app dependencies +COPY --from=0 /opt/app-root/src/node_modules /opt/app-root/src/node_modules +COPY . /opt/app-root/src + +ENV NODE_ENV production +ENV PORT 3001 + +CMD ["npm", "start"] diff --git a/test/fixtures/components/nodejs-starter/LICENSE b/test/fixtures/components/nodejs-starter/LICENSE new file mode 100644 index 000000000..e48e09634 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/test/fixtures/components/nodejs-starter/README.md b/test/fixtures/components/nodejs-starter/README.md new file mode 100644 index 000000000..48bba8ce7 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/README.md @@ -0,0 +1,22 @@ +# Creating an application with a Node.js code sample + +**Note:** The Node.js code sample uses the **3001** HTTP port. + +Before you begin creating an application with this `devfile` code sample, it's helpful to understand the relationship between the `devfile` and `Dockerfile` and how they contribute to your build. You can find these files at the following URLs: + +* [Node.js `devfile.yaml`](https://github.com/nodeshift-starters/devfile-sample/blob/main/devfile.yaml) +* [Node.js `Dockerfile`](https://github.com/nodeshift-starters/devfile-sample/blob/main/Dockerfile) + +1. The `devfile.yaml` file has an [`image-build` component](https://github.com/nodeshift-starters/devfile-sample/blob/main/devfile.yaml#L18-L24) that points to your `Dockerfile`. +2. The [`Dockerfile`](https://github.com/nodeshift-starters/devfile-sample/blob/main/Dockerfile) contains the instructions you need to build the code sample as a container image. +3. The `devfile.yaml` [`kubernetes-deploy` component](https://github.com/nodeshift-starters/devfile-sample/blob/main/devfile.yaml#L25-L37) points to a `deploy.yaml` file that contains instructions for deploying the built container image. +4. The `devfile.yaml` [`deploy` command](https://github.com/nodeshift-starters/devfile-sample/blob/main/devfile.yaml#L45-L52) completes the [outerloop](https://devfile.io/docs/2.2.0/innerloop-vs-outerloop) deployment phase by pointing to the `image-build` and `kubernetes-deploy` components to create your application. + +## License + +This stack is licensed under the [EPL 2.0](./LICENSE) license. + +### Additional resources +* For more information about Node.js, see [How do I start with Node.js after I installed it?](https://nodejs.org/en/docs/guides/getting-started-guide). +* For more information about devfiles, see [Devfile.io](https://devfile.io/). +* For more information about Dockerfiles, see [Dockerfile reference](https://docs.docker.com/engine/reference/builder/). \ No newline at end of file diff --git a/test/fixtures/components/nodejs-starter/deploy.yaml b/test/fixtures/components/nodejs-starter/deploy.yaml new file mode 100644 index 000000000..601a5d736 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/deploy.yaml @@ -0,0 +1,38 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: my-nodejs +spec: + replicas: 1 + selector: + matchLabels: + app: nodejs-app + template: + metadata: + labels: + app: nodejs-app + spec: + containers: + - name: my-nodejs + image: nodejs-image:latest + ports: + - name: http + containerPort: 3001 + protocol: TCP + resources: + requests: + memory: "50Mi" + cpu: "10m" +--- +kind: Service +apiVersion: v1 +metadata: + name: my-nodejs-svc +spec: + ports: + - name: http-3001 + port: 3001 + protocol: TCP + targetPort: 3001 + selector: + app: nodejs-app \ No newline at end of file diff --git a/test/fixtures/components/nodejs-starter/devfile.yaml b/test/fixtures/components/nodejs-starter/devfile.yaml new file mode 100644 index 000000000..95cbbc7fb --- /dev/null +++ b/test/fixtures/components/nodejs-starter/devfile.yaml @@ -0,0 +1,56 @@ +schemaVersion: 2.2.0 +metadata: + name: nodejs-starter + version: 3.0.0 + displayName: Node.js Runtime + description: Stack with Node.js 18 + tags: + - Node.js + - Express + - ubi8 + projectType: Node.js + language: JavaScript + attributes: + alpha.dockerimage-port: 3001 + provider: Red Hat + supportUrl: https://github.com/devfile-samples/devfile-support#support-information +parent: + id: nodejs + version: 2.2.0 + registryUrl: https://registry.devfile.io +components: + - name: image-build + image: + imageName: nodejs-image:latest + dockerfile: + uri: Dockerfile + buildContext: . + rootRequired: false + - name: kubernetes-deploy + attributes: + deployment/replicas: 1 + deployment/cpuRequest: 10m + deployment/memoryRequest: 50Mi + deployment/container-port: 3001 + kubernetes: + uri: deploy.yaml + endpoints: + - name: http-3001 + targetPort: 3001 + path: / + secure: true +commands: + - id: build-image + apply: + component: image-build + - id: deployk8s + apply: + component: kubernetes-deploy + - id: deploy + composite: + commands: + - build-image + - deployk8s + group: + kind: deploy + isDefault: true diff --git a/test/fixtures/components/nodejs-starter/package-lock.json b/test/fixtures/components/nodejs-starter/package-lock.json new file mode 100644 index 000000000..47e5d7a12 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/package-lock.json @@ -0,0 +1,1374 @@ +{ + "name": "nodejs-starter", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "nodejs-starter", + "version": "2.0.0", + "license": "EPL-2.0", + "dependencies": { + "express": "^4.18.1", + "pino": "^8.5.0", + "pino-http": "^8.2.0", + "prom-client": "^14.1.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-redact": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", + "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "dependencies": { + "punycode": "^1.3.2" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pino": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.5.0.tgz", + "integrity": "sha512-PuD6sOti8Y+p9zRoNB5dibmfjfM/OU2tEtJFICxw5ulXi1d0qnq/Rt3CsR6aBEAOeyCXP+ZUfiNWW+tt55pNzg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.2.0.tgz", + "integrity": "sha512-L5exMoXpqPFgBdCR0ftFRXWu20pt1r+3Kcf7R8A2QGdmkKZihYwB5t4yZ+bGrU/+h8APMAM83pTAPYMR7v84Rg==", + "dependencies": { + "fast-url-parser": "^1.1.3", + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz", + "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==" + }, + "node_modules/process-warning": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", + "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==" + }, + "node_modules/prom-client": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.0.tgz", + "integrity": "sha512-iFWCchQmi4170omLpFXbzz62SQTmPhtBL35v0qGEVRHKcqIeiexaoYeP0vfZTujxEq3tA87iqOdRbC9svS1B9A==", + "dependencies": { + "tdigest": "^0.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.1.0.tgz", + "integrity": "sha512-sVisi3+P2lJ2t0BPbpK629j8wRW06yKGJUcaLAGXPAUhyUxVJm7VsCTit1PFgT4JHUDMrGNR+ZjSKpzGaRF3zw==", + "dependencies": { + "abort-controller": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonic-boom": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz", + "integrity": "sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, + "node_modules/thread-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz", + "integrity": "sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, + "bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "fast-redact": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", + "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==" + }, + "fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "requires": { + "punycode": "^1.3.2" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "pino": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.5.0.tgz", + "integrity": "sha512-PuD6sOti8Y+p9zRoNB5dibmfjfM/OU2tEtJFICxw5ulXi1d0qnq/Rt3CsR6aBEAOeyCXP+ZUfiNWW+tt55pNzg==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + } + }, + "pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "pino-http": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.2.0.tgz", + "integrity": "sha512-L5exMoXpqPFgBdCR0ftFRXWu20pt1r+3Kcf7R8A2QGdmkKZihYwB5t4yZ+bGrU/+h8APMAM83pTAPYMR7v84Rg==", + "requires": { + "fast-url-parser": "^1.1.3", + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" + } + }, + "pino-std-serializers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz", + "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==" + }, + "process-warning": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", + "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==" + }, + "prom-client": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.0.tgz", + "integrity": "sha512-iFWCchQmi4170omLpFXbzz62SQTmPhtBL35v0qGEVRHKcqIeiexaoYeP0vfZTujxEq3tA87iqOdRbC9svS1B9A==", + "requires": { + "tdigest": "^0.1.1" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.1.0.tgz", + "integrity": "sha512-sVisi3+P2lJ2t0BPbpK629j8wRW06yKGJUcaLAGXPAUhyUxVJm7VsCTit1PFgT4JHUDMrGNR+ZjSKpzGaRF3zw==", + "requires": { + "abort-controller": "^3.0.0" + } + }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "sonic-boom": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz", + "integrity": "sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "requires": { + "bintrees": "1.0.2" + } + }, + "thread-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz", + "integrity": "sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ==", + "requires": { + "real-require": "^0.2.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + } + } +} diff --git a/test/fixtures/components/nodejs-starter/package.json b/test/fixtures/components/nodejs-starter/package.json new file mode 100644 index 000000000..e275dedc8 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/package.json @@ -0,0 +1,17 @@ +{ + "name": "nodejs-starter", + "version": "3.0.0", + "description": "Simple Node.js application", + "license": "EPL-2.0", + "scripts": { + "start": "node server.js", + "debug": "node --inspect=${DEBUG_PORT} server.js", + "test": "node test/test.js" + }, + "dependencies": { + "express": "^4.18.1", + "pino": "^8.5.0", + "pino-http": "^8.2.0", + "prom-client": "^14.1.0" + } +} diff --git a/test/fixtures/components/nodejs-starter/server.js b/test/fixtures/components/nodejs-starter/server.js new file mode 100644 index 000000000..aa429e759 --- /dev/null +++ b/test/fixtures/components/nodejs-starter/server.js @@ -0,0 +1,67 @@ +const Prometheus = require('prom-client') +const express = require('express'); +const http = require('http'); + +Prometheus.collectDefaultMetrics(); + +const requestHistogram = new Prometheus.Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['code', 'handler', 'method'], + buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5] +}) + +const requestTimer = (req, res, next) => { + const path = new URL(req.url, `http://${req.hostname}`).pathname + const stop = requestHistogram.startTimer({ + method: req.method, + handler: path + }) + res.on('finish', () => { + stop({ + code: res.statusCode + }) + }) + next() +} + +const app = express(); +const server = http.createServer(app) + +// See: http://expressjs.com/en/4x/api.html#app.settings.table +const PRODUCTION = app.get('env') === 'production'; + +// Administrative routes are not timed or logged, but for non-admin routes, pino +// overhead is included in timing. +app.get('/ready', (req, res) => res.status(200).json({status:"ok"})); +app.get('/live', (req, res) => res.status(200).json({status:"ok"})); +app.get('/metrics', async (req, res, next) => { + const metrics = await Prometheus.register.metrics(); + res.set('Content-Type', Prometheus.register.contentType) + res.end(metrics); +}) + +// Time routes after here. +app.use(requestTimer); + +// Log routes after here. +const pino = require('pino')({ + level: PRODUCTION ? 'info' : 'debug', +}); +app.use(require('pino-http')({logger: pino})); + +app.get('/', (req, res) => { + // Use req.log (a `pino` instance) to log JSON: + req.log.info({message: 'Hello from Node.js Starter Application!'}); + res.send('Hello from Node.js Starter Application!'); +}); + +app.get('*', (req, res) => { + res.status(404).send("Not Found"); +}); + +// Listen and serve. +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + console.log(`App started on PORT ${PORT}`); +}); diff --git a/test/fixtures/components/nodejs-starter/test/test.js b/test/fixtures/components/nodejs-starter/test/test.js new file mode 100644 index 000000000..ca0a2d41a --- /dev/null +++ b/test/fixtures/components/nodejs-starter/test/test.js @@ -0,0 +1,21 @@ +var assert = require('assert'); + +(function(){ + 'use strict'; + + equal('should pass', function() { + assert(1 === 1); + }); + + function equal(desc, fn) { + try { + fn(); + console.log('\x1b[32m%s\x1b[0m', '\u2714 ' + desc); + console.log("Add your tests in this ./test directory"); + } catch (error) { + console.log('\n'); + console.log('\x1b[31m%s\x1b[0m', '\u2718 ' + desc); + console.error(error); + } + } + })(); \ No newline at end of file diff --git a/test/ui/common/command-activator.ts b/test/ui/common/command-activator.ts index fb27b077a..ce8662ab3 100644 --- a/test/ui/common/command-activator.ts +++ b/test/ui/common/command-activator.ts @@ -3,10 +3,22 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { InputBox, Workbench } from 'vscode-extension-tester'; +import { InputBox, VSBrowser, Workbench } from 'vscode-extension-tester'; export async function activateCommand(command: string){ const prompt = await new Workbench().openCommandPrompt() as InputBox; + + // openCommandPrompt() can return a reference to the palette input before it's actually + // interactable (e.g. under CI load) - typing into it immediately then throws + // ElementNotInteractableError. Wait for it to actually be displayed first. + await VSBrowser.instance.driver.wait(async () => { + try { + return await prompt.isDisplayed(); + } catch { + return false; + } + }, 10_000, 'Command prompt did not become interactable'); + await prompt.setText(command); await prompt.confirm(); diff --git a/test/ui/common/overdrives.ts b/test/ui/common/overdrives.ts index 9f6cc5e9d..503d4a4bd 100644 --- a/test/ui/common/overdrives.ts +++ b/test/ui/common/overdrives.ts @@ -2,7 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { ViewSection, By, EditorView, waitForAttributeValue, BottomBarPanel, VSBrowser, ActivityBar } from 'vscode-extension-tester'; +import { ViewSection, By, EditorView, waitForAttributeValue, BottomBarPanel, VSBrowser, ActivityBar, SideBarView } from 'vscode-extension-tester'; import { activateCommand } from './command-activator'; import { VIEWS } from './constants'; @@ -27,6 +27,19 @@ export async function collapse(section: ViewSection) { } } +// Collapses each named section of the given view, best-effort: a section that fails to +// collapse (stale reference, click intercepted by a hover, not currently found, etc.) +// is skipped rather than failing the whole suite. +export async function collapseViews(view: SideBarView, sectionTitles: string[]): Promise { + for (const sectionTitle of sectionTitles) { + try { + await collapse(await view.getContent().getSection(sectionTitle)); + } catch { + // best-effort - a section failing to collapse shouldn't fail the whole suite + } + } +} + /** * Closes BottomBarPanel and reloads window so openshift terminal is not loaded, * then waits until window is loaded. diff --git a/test/ui/common/ui/webview/ServerlessFunctionWebViewEditor.ts b/test/ui/common/ui/webview/ServerlessFunctionWebViewEditor.ts index dd5ff097d..badda9d3c 100644 --- a/test/ui/common/ui/webview/ServerlessFunctionWebViewEditor.ts +++ b/test/ui/common/ui/webview/ServerlessFunctionWebViewEditor.ts @@ -2,7 +2,8 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { By, InputBox, Key, WebElement, WebView } from 'vscode-extension-tester'; +import { expect } from 'chai'; +import { InputBox, Key, WebElement, WebView } from 'vscode-extension-tester'; import { WebViewForm } from './WebViewForm'; export class ServerlessFunctionWebView extends WebViewForm { @@ -14,6 +15,7 @@ export class ServerlessFunctionWebView extends WebViewForm { public async insertFunctionName(name: string): Promise { await this.enterWebView(async (webView) => { const nameField = await this.getInsertFunctionNameInput(webView); + expect(nameField, 'Function name input should be found').to.exist; await nameField.sendKeys(`${name}${Key.ENTER}`); }); } @@ -21,6 +23,7 @@ export class ServerlessFunctionWebView extends WebViewForm { public async selectBuildImage(image: string): Promise { await this.enterWebView(async (webView) => { const buildImageField = await this.getSelectBuildImageInput(webView); + expect(buildImageField, 'Build image input should be found').to.exist; await buildImageField.sendKeys(`${image}${Key.ARROW_DOWN}${Key.ENTER}`); }); } @@ -28,6 +31,7 @@ export class ServerlessFunctionWebView extends WebViewForm { public async selectLanguage(language: string): Promise { await this.enterWebView(async (webView) => { const languageField = await this.getSelectLanguageInput(webView); + expect(languageField, 'Language runtime input should be found').to.exist; await languageField.sendKeys(`${language}${Key.ARROW_DOWN}${Key.ENTER}`); }); } @@ -35,6 +39,7 @@ export class ServerlessFunctionWebView extends WebViewForm { public async selectTemplate(template: string): Promise { await this.enterWebView(async (webView) => { const templateField = await this.getSelectTemplateInput(webView); + expect(templateField, 'Function template input should be found').to.exist; await templateField.sendKeys(`${template}${Key.ARROW_DOWN}${Key.ENTER}`); }); } @@ -42,6 +47,7 @@ export class ServerlessFunctionWebView extends WebViewForm { public async selectFolder(path: string): Promise { await this.enterWebView(async (webView) => { const folderField = await this.getSelectFolderInput(webView); + expect(folderField, 'Folder input should be found').to.exist; await folderField.click(); await folderField.sendKeys(`${Key.ARROW_DOWN}${Key.ENTER}`); }); @@ -54,32 +60,35 @@ export class ServerlessFunctionWebView extends WebViewForm { public async clickCreateButton(): Promise { await this.enterWebView(async (webView) => { const button = await this.getCreateButton(webView); + expect(button, 'Create button should be found').to.exist; await button.click(); }); } private async getInsertFunctionNameInput(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//input[@placeholder="Provide name of the function to be created"]')); + // This is the first webview interaction in the suite, so it can pay the same + // "cold render" cost seen in Add Cluster's first button - give it more room. + return this.findElementSparse(webView, '//input[@placeholder="Provide name of the function to be created"]', 35_000); } private async getSelectBuildImageInput(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//input[@placeholder="Provide full image name (podman, docker, quay)"]')); + return this.findElementSparse(webView, '//input[@placeholder="Provide full image name (podman, docker, quay)"]'); } private async getSelectLanguageInput(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//input[@placeholder="Select the Language Runtime"]')); + return this.findElementSparse(webView, '//input[@placeholder="Select the Language Runtime"]'); } private async getSelectTemplateInput(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//input[@placeholder="Select the Function template"]')); + return this.findElementSparse(webView, '//input[@placeholder="Select the Function template"]'); } private async getSelectFolderInput(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//input[@placeholder="Select the folder to initialise the function at that path"]')); + return this.findElementSparse(webView, '//input[@placeholder="Select the folder to initialise the function at that path"]'); } private async getCreateButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//button[contains(text(), "Create")]')); + return this.findElementSparse(webView, '//button[contains(text(), "Create")]'); } } \ No newline at end of file diff --git a/test/ui/common/ui/webview/WebViewForm.ts b/test/ui/common/ui/webview/WebViewForm.ts index 15bf3d7db..82629eef7 100644 --- a/test/ui/common/ui/webview/WebViewForm.ts +++ b/test/ui/common/ui/webview/WebViewForm.ts @@ -2,7 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { Editor, EditorView, WebView } from 'vscode-extension-tester'; +import { By, Editor, EditorView, WebElement, WebView } from 'vscode-extension-tester'; /** * Web View form representation implementation @@ -53,4 +53,39 @@ export abstract class WebViewForm { } return retValue; } + + /** + * Tries immediately first (so already-rendered pages - e.g. filling in one form field + * after another - pay no extra latency), then falls back to sleeping upfront and checking + * infrequently instead of polling the DOM in a tight loop. A tight driver.wait() loop re-runs + * an XPath query on the webview's single JS thread every ~200ms, which can keep grabbing the + * thread the page's own async rendering (e.g. Material-UI) needs to finish - starving it + * instead of waiting for it. Sparse checks with real idle gaps let rendering actually complete. + */ + protected async findElementSparse(webView: WebView, xpath: string, timeout = 15_000, pollInterval = 1_500, initialDelay = 2_000): Promise { + try { + const immediate = await webView.findWebElement(By.xpath(xpath)); + if (immediate) { + return immediate; + } + } catch { + // not there yet, fall through to the sparse retry loop + } + + await new Promise((resolve) => setTimeout(resolve, initialDelay)); + + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + try { + const element = await webView.findWebElement(By.xpath(xpath)); + if (element) { + return element; + } + } catch { + // not rendered yet, fall through to retry after the poll interval + } + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + return null; + } } \ No newline at end of file diff --git a/test/ui/common/ui/webview/addClusterWebView.ts b/test/ui/common/ui/webview/addClusterWebView.ts index 3e005f22a..12d44caa2 100644 --- a/test/ui/common/ui/webview/addClusterWebView.ts +++ b/test/ui/common/ui/webview/addClusterWebView.ts @@ -2,6 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { expect } from 'chai'; import { By, WebElement, WebView } from 'vscode-extension-tester'; import { WebViewForm } from './WebViewForm'; @@ -21,6 +22,7 @@ export class AddClusterWebView extends WebViewForm { public async addLocalCluster(): Promise { await this.enterWebView(async (webView) => { const button = await this.getCreateRefreshClusterButton(webView); + expect(button, 'Create/Refresh cluster button should be found').to.exist; await button.click(); }); } @@ -28,36 +30,43 @@ export class AddClusterWebView extends WebViewForm { public async addDevSandbox(): Promise { await this.enterWebView(async (webView) => { const button = await this.getStartYourOpenshiftExperienceButton(webView); + expect(button, 'Start your OpenShift experience button should be found').to.exist; await button.click(); }); } public async checkLearningButton(): Promise { await this.enterWebView(async (webView) => { - await this.getStartLearningButton(webView); + const button = await this.getStartLearningButton(webView); + expect(button, 'Start Learning button should be found').to.exist; }); } public async checkRosaButton(): Promise { await this.enterWebView(async (webView) => { - await this.getRosaButton(webView); + const button = await this.getRosaButton(webView); + expect(button, 'Create a ROSA cluster button should be found').to.exist; }); } private async getCreateRefreshClusterButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//*[@role="button" and contains(normalize-space(),"Create/Refresh cluster")]')); + return this.findButton(webView, 'Create/Refresh cluster'); } private async getStartYourOpenshiftExperienceButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//*[@role="button" and contains(normalize-space(),"Start your OpenShift experience")]')); + return this.findButton(webView, 'Start your OpenShift experience'); } private async getStartLearningButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//*[@role="button" and contains(normalize-space(),"Start Learning")]')); + return this.findButton(webView, 'Start Learning'); } private async getRosaButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//*[@role="button" and contains(normalize-space(),"Create a ROSA cluster")]')); + return this.findButton(webView, 'Create a ROSA cluster'); + } + + private async findButton(webView: WebView, searchText: string, timeout = 35_000): Promise { + return this.findElementSparse(webView, `//*[@role="button" and contains(normalize-space(),"${searchText}")]`, timeout); } } diff --git a/test/ui/common/ui/webview/newComponentWebViewEditor.ts b/test/ui/common/ui/webview/newComponentWebViewEditor.ts index ec7458897..b40e95c98 100644 --- a/test/ui/common/ui/webview/newComponentWebViewEditor.ts +++ b/test/ui/common/ui/webview/newComponentWebViewEditor.ts @@ -2,6 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { expect } from 'chai'; import { By, EditorView, Key, VSBrowser, WebElement, WebView } from 'vscode-extension-tester'; import { debugClick } from '../../conditions'; import { WebViewForm } from './WebViewForm'; @@ -20,36 +21,23 @@ export class CreateComponentWebView extends WebViewForm { } private async getCreateFromTemplateButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//div[..//h6[contains(text(),"From Template Project")]]/button')); + return this.findElementSparse(webView, '//div[..//h6[contains(text(),"From Template Project")]]/button', 35_000); } private async getCreateFromGitButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//div[..//h6[contains(text(),"From Existing Remote Git Repository")]]/button')); + return this.findElementSparse(webView, '//div[..//h6[contains(text(),"From Existing Remote Git Repository")]]/button', 35_000); } private async getCreateFromLocalButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//div[..//h6[contains(text(),"From Existing Local Codebase")]]/button')); + return this.findElementSparse(webView, '//div[..//h6[contains(text(),"From Existing Local Codebase")]]/button', 35_000); } public async createComponentFromTemplate(): Promise { - await VSBrowser.instance.driver.wait(async () => { - try { - return await this.enterWebView(async (webView) => { - const button = await this.getCreateFromTemplateButton(webView); - if (!button) return false; - - const visible = await button.isDisplayed(); - const enabled = await button.isEnabled(); - - if (!visible || !enabled) return false; - - await button.click(); - return true; - }); - } catch { - return false; - } - }, 10000, 'Create Component from Template button not clickable'); + await this.enterWebView(async (webView) => { + const button = await this.getCreateFromTemplateButton(webView); + expect(button, 'Create Component from Template button should be found').to.exist; + await button.click(); + }); await VSBrowser.instance.driver.wait(async () => { const editors = await new EditorView().getOpenEditorTitles(); @@ -60,24 +48,11 @@ export class CreateComponentWebView extends WebViewForm { } public async createComponentFromGit(): Promise { - await VSBrowser.instance.driver.wait(async () => { - try { - return await this.enterWebView(async (webView) => { - const button = await this.getCreateFromGitButton(webView); - if (!button) return false; - - const visible = await button.isDisplayed(); - const enabled = await button.isEnabled(); - - if (!visible || !enabled) return false; - - await button.click(); - return true; - }); - } catch { - return false; - } - }, 10000, 'Create Component from Git button not clickable'); + await this.enterWebView(async (webView) => { + const button = await this.getCreateFromGitButton(webView); + expect(button, 'Create Component from Git button should be found').to.exist; + await button.click(); + }); await VSBrowser.instance.driver.wait(async () => { const editors = await new EditorView().getOpenEditorTitles(); @@ -86,24 +61,11 @@ export class CreateComponentWebView extends WebViewForm { } public async createComponentFromLocalCodebase(): Promise { - await VSBrowser.instance.driver.wait(async () => { - try { - return await this.enterWebView(async (webView) => { - const button = await this.getCreateFromLocalButton(webView); - if (!button) return false; - - const visible = await button.isDisplayed(); - const enabled = await button.isEnabled(); - - if (!visible || !enabled) return false; - - await button.click(); - return true; - }); - } catch { - return false; - } - }, 10000, 'Create Component from Local Folder button not clickable'); + await this.enterWebView(async (webView) => { + const button = await this.getCreateFromLocalButton(webView); + expect(button, 'Create Component from Local Folder button should be found').to.exist; + await button.click(); + }); await VSBrowser.instance.driver.wait(async () => { const editors = await new EditorView().getOpenEditorTitles(); @@ -219,26 +181,13 @@ export class GitProjectPage extends Page { */ public async clickContinueButton(): Promise { await this.enterWebView(async (webView) => { - const button = await this.continueButtonExists(webView); + const button = await this.getContinueButton(webView); await debugClick(button, 'Continue'); }); } - private async continueButtonExists(webView: WebView, timeout = 60_000): Promise { - return webView.getDriver().wait(async () => { - try { - const button = await this.getContinueButton(webView); - if (button) { - return button; - } - } catch { - return null; - } - }, timeout); - } - private async getContinueButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//button[contains(text(), "CONTINUE WITH THIS DEVFILE")]')); + return this.findElementSparse(webView, '//button[contains(text(), "CONTINUE WITH THIS DEVFILE")]', 60_000); } private async getGitRepositoryLinkField(webView: WebView): Promise { @@ -268,7 +217,7 @@ export class LocalCodeBasePage extends Page { public async clickCreateComponent(): Promise { await this.enterWebView(async (webView) => { - const button = await this.createButtonExists(webView); + const button = await this.getCreateComponentButton(webView); await debugClick(button, 'Create Component'); }); } @@ -281,20 +230,7 @@ export class LocalCodeBasePage extends Page { return await webView.findWebElement(By.xpath('//button[contains(text(), "Select Folder")]')); } - private async createButtonExists(webView: WebView, timeout = 60_000): Promise { - return webView.getDriver().wait(async () => { - try { - const button = await this.getCreateComponentButton(webView); - if (button) { - return button; - } - } catch { - return null; - } - }, timeout); - } - private async getCreateComponentButton(webView: WebView): Promise { - return await webView.findWebElement(By.xpath('//span[contains(text(), "Create Component")]')); + return this.findElementSparse(webView, '//span[contains(text(), "Create Component")]', 60_000); } } diff --git a/test/ui/public-ui-kind-test.ts b/test/ui/public-ui-kind-test.ts new file mode 100644 index 000000000..2db101195 --- /dev/null +++ b/test/ui/public-ui-kind-test.ts @@ -0,0 +1,119 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { ActivityBar, InputBox, SideBarView, VSBrowser, Workbench } from 'vscode-extension-tester'; +import { stabilizeComponentsView, waitForItemStable } from './common/conditions'; +import { VIEWS } from './common/constants'; +import { loadKubeConfigFromBackup } from './common/kubeConfigUtils'; +import { checkAboutCommand } from './suite/command-about'; +import { testComponentContextMenu } from './suite/componentContextMenu'; +import { testComponentCommands } from './suite/componentCommands'; +import { checkExtension } from './suite/extension'; +import { kubernetesContextTest } from './suite/kubernetesContext'; +import { projectTest } from './suite/project'; +import { operatorBackedServiceTest } from './suite/operatorBackedService'; + +import * as sourceMapSupport from 'source-map-support'; + +sourceMapSupport.install(); + +describe('Extension public-facing UI tests with Kind cluster', function() { + const contextFolder = path.join(__dirname, 'context'); + const clusterIsSet = true; + let view: SideBarView; + + before(async function() { + await loadKubeConfigFromBackup(); + }); + + // Runs first, and only after this describe's tests complete does the next + // sibling describe below start - safe to check the extension activated + // before we go anywhere near views the extension itself contributes. + checkExtension(); + + describe('Stabilize cluster explorer', function() { + before(async function() { + this.timeout(30_000); + + // This suite launches a fresh VS Code session that has never opened the + // OpenShift view before, so the Application Explorer needs to actually detect + // and settle on the (now restored) kubeconfig before any cluster-dependent + // test runs. Previously this settling happened implicitly, since the shared + // session had already been idle-open for the ~5 minutes the non-cluster suite + // took to run beforehand. This only runs after checkExtension()'s tests above + // have confirmed the extension is actually installed and activated. + view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); + await stabilizeComponentsView(() => view.getContent().getSection(VIEWS.appExplorer)); + }); + + it('Application Explorer reflects the connected cluster', function() { + // No-op: the before() hook above did the actual stabilization work. + }); + }); + + checkAboutCommand(clusterIsSet); + + describe('Open nodejs-starter component for cluster tests', function() { + before(async function() { + this.timeout(90_000); + + // This suite runs in its own VS Code session, separate from public-ui-test.ts's - + // extest wipes the whole user-data-dir between the two `extest run-tests` + // invocations, so nothing carries over from that other session. Component Context + // Menu/Commands below expect a "nodejs-starter" component to already exist, so copy + // the vendored fixture (a real, working devfile - not something the wizard needs to + // create fresh each time) into place and open it as the workspace folder. + // + // VSBrowser.openResources() (a `code -r ` CLI call) turned out not to + // actually open the folder mid-session here - the window title stayed a bare + // "Visual Studio Code" with no workspace, so the Components tree correctly saw + // nothing to show. Using the real "File: Open Folder..." command instead - the + // simple dialog it opens (files.simpleDialog.enable in test/ui/settings.json) is + // the same automatable InputBox that LocalCodeBasePage.clickSelectFolderButton() + // already drives successfully for component creation. + const componentFolder = path.join(contextFolder, 'nodejs-starter'); + fs.copySync(path.join(__dirname, '..', 'fixtures', 'components', 'nodejs-starter'), componentFolder); + + const prompt = await new Workbench().openCommandPrompt(); + await VSBrowser.instance.driver.wait(async () => prompt.isDisplayed(), 5_000); + await prompt.setText('>File: Open Folder...'); + await prompt.confirm(); + + const input = await InputBox.create(); + await input.setText(componentFolder); + await input.confirm(); + + // Opening a folder reloads the whole window - wait for the title to reflect the + // new workspace before touching any views, since elements from before the reload + // are no longer valid. + await VSBrowser.instance.driver.wait(async () => { + try { + const title = await VSBrowser.instance.driver.getTitle(); + return title.includes('nodejs-starter'); + } catch { + return false; + } + }, 30_000, 'Window did not reload with the nodejs-starter workspace folder'); + + view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); + const getComponentsSection = () => view.getContent().getSection(VIEWS.components); + await stabilizeComponentsView(getComponentsSection); + + await waitForItemStable(getComponentsSection, 'nodejs-starter', true, 30_000); + }); + + it('nodejs-starter component is open', function() { + // No-op: the before() hook above did the actual setup work. + }); + }); + + testComponentContextMenu(); + testComponentCommands(contextFolder); + projectTest(false) + kubernetesContextTest(false); + operatorBackedServiceTest(); +}); diff --git a/test/ui/public-ui-test.ts b/test/ui/public-ui-test.ts index 4dc05e90e..7c4cb8564 100644 --- a/test/ui/public-ui-test.ts +++ b/test/ui/public-ui-test.ts @@ -4,7 +4,7 @@ *-----------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { backupKubeConfig, loadKubeConfigFromBackup } from './common/kubeConfigUtils'; +import { backupKubeConfig } from './common/kubeConfigUtils'; import { testAddCluster } from './suite/addCluster'; import { checkAboutCommand } from './suite/command-about'; import { testCreateComponent } from './suite/createComponent'; @@ -15,45 +15,23 @@ import { checkOpenshiftView } from './suite/openshift'; import { testCreateServerlessFunction } from './suite/serverlessFunction'; import * as sourceMapSupport from 'source-map-support'; -import { testComponentContextMenu } from './suite/componentContextMenu'; -import { testComponentCommands } from './suite/componentCommands'; -import { kubernetesContextTest } from './suite/kubernetesContext'; -import { projectTest } from './suite/project'; -import { operatorBackedServiceTest } from './suite/operatorBackedService'; sourceMapSupport.install(); describe('Extension public-facing UI tests', function() { const contextFolder = path.join(__dirname, 'context'); - let clusterIsSet = false; + const clusterIsSet = false; - describe('Non-cluster tests', function() { - before(async function() { - await backupKubeConfig(); - }); - - checkExtension(); - checkOpenshiftView(); - testAddCluster(); - testDevfileRegistries(); - checkFocusOnCommands(); - testCreateComponent(contextFolder); - testCreateServerlessFunction(contextFolder); - checkAboutCommand(clusterIsSet); + before(async function() { + await backupKubeConfig(); }); - describe('Extension public-facing UI tests with Kind cluster', function() { - clusterIsSet = true; - - before(async function() { - await loadKubeConfigFromBackup(); - }); - - checkAboutCommand(clusterIsSet); - testComponentContextMenu(); - testComponentCommands(contextFolder); - projectTest(false) - kubernetesContextTest(false); - operatorBackedServiceTest(); - }); + checkExtension(); + checkOpenshiftView(); + testAddCluster(); + testDevfileRegistries(); + checkFocusOnCommands(); + testCreateComponent(contextFolder); + testCreateServerlessFunction(contextFolder); + checkAboutCommand(clusterIsSet); }); diff --git a/test/ui/settings.json b/test/ui/settings.json index 588111abd..761fb0388 100644 --- a/test/ui/settings.json +++ b/test/ui/settings.json @@ -3,5 +3,9 @@ "files.simpleDialog.enable": true, "workbench.welcomePage.experimentalOnboarding": false, "workbench.editor.tabActionLocation": "right", - "workbench.editor.showTabs": "multiple" + "workbench.editor.showTabs": "multiple", + "openshiftToolkit.showWelcomePage": false, + "security.workspace.trust.enabled": false, + "git.openRepositoryInParentFolders": "never", + "git.autoRepositoryDetection": false } \ No newline at end of file diff --git a/test/ui/suite/addCluster.ts b/test/ui/suite/addCluster.ts index 37bddfeaa..1dd4064bd 100644 --- a/test/ui/suite/addCluster.ts +++ b/test/ui/suite/addCluster.ts @@ -2,45 +2,78 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { ActivityBar, SideBarView, VSBrowser } from 'vscode-extension-tester'; +import { ActivityBar, EditorView, SideBarView, VSBrowser } from 'vscode-extension-tester'; import { VIEWS } from '../common/constants'; import { closeAllOpenEditors, collapse } from '../common/overdrives'; import { AddClusterWebView, DevSandboxWebViewPage, LocalClusterWebViewPage } from '../common/ui/webview/addClusterWebView'; import { webViewIsOpened, welcomeContentButtonsAreLoaded } from '../common/conditions'; +/** + * Polls getOpenEditorTitles() instead of trusting that the tab exists the instant + * webViewIsOpened()/the triggering click resolves - VS Code needs real time to finish + * opening and rendering the editor. + */ +async function waitForEditorTitle(title: string, timeout = 15_000, pollInterval = 1_000): Promise { + const editorView = new EditorView(); + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + const openEditorTitles = await editorView.getOpenEditorTitles(); + if (openEditorTitles.includes(title)) { + return; + } + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + throw new Error(`Timed out after ${timeout}ms waiting for editor tab "${title}" to open`); +} + export function testAddCluster() { describe('Add Cluster', function () { let view: SideBarView; - let addClusterView; - before(async function context() { - this.timeout(30_000) - //await new Promise((res) => {setTimeout(res, 20_000)}); + /** + * Opens the Add Cluster webview from a clean slate. VS Code webview panels use + * retainContextWhenHidden, so a stray Welcome tab's iframe stays in the DOM even + * after it's no longer the active tab - and WebView's frame-picking logic (matching + * bounding rects) can lock onto it instead of the Add Cluster iframe. Closing all + * editors first guarantees only one webview iframe exists when we switch into it. + */ + async function openAddClusterView(): Promise { await closeAllOpenEditors(); - view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); - for (const item of [VIEWS.components, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions]) { - await collapse(await view.getContent().getSection(item)) - } - }); - it('Page with options is shown', async function test() { - this.timeout(70_000); - //await new Promise((res) => {setTimeout(res, 15_000)}); const section = await view.getContent().getSection(VIEWS.appExplorer); const welcomeContent = await section.findWelcomeContent(); const buttons = await welcomeContentButtonsAreLoaded(welcomeContent); await buttons[2].click(); - addClusterView = new AddClusterWebView(); + const addClusterView = new AddClusterWebView(); await webViewIsOpened(addClusterView.editorName, VSBrowser.instance.driver); + await waitForEditorTitle(addClusterView.editorName); await addClusterView.initializeEditor(); + return addClusterView; + } + + before(async function context() { + this.timeout(30_000) + await closeAllOpenEditors(); + view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); + + for (const item of [VIEWS.components, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions]) { + await collapse(await view.getContent().getSection(item)) + } + }); + + it('Page with options is shown', async function test() { + this.timeout(30_000); + const addClusterView = await openAddClusterView(); await addClusterView.checkRosaButton(); await addClusterView.checkLearningButton(); }); it('Local Cluster Page shows appropriate content', async function test() { + this.timeout(40_000); + const addClusterView = await openAddClusterView(); await addClusterView.addLocalCluster(); const localClusterPage = new LocalClusterWebViewPage(); await localClusterPage.initializeEditor(); @@ -51,6 +84,8 @@ export function testAddCluster() { }); it('Developer Sandbox Page shows appropriate content', async function test() { + this.timeout(40_000); + const addClusterView = await openAddClusterView(); await addClusterView.addDevSandbox(); const devSandboxWebViewPage = new DevSandboxWebViewPage(); await devSandboxWebViewPage.initializeEditor(); diff --git a/test/ui/suite/command-about.ts b/test/ui/suite/command-about.ts index 544e8b06f..17557aa72 100644 --- a/test/ui/suite/command-about.ts +++ b/test/ui/suite/command-about.ts @@ -36,10 +36,16 @@ export function checkAboutCommand(clusterIsSet: boolean) { ); webviewView = new WebviewView(); await webviewView.switchToFrame(6_500); - await webviewView.findWebElement( - By.xpath('//textarea[@aria-label = "Terminal input"]'), - ); - await webviewView.switchBack(); + try { + await webviewView.findWebElement( + By.xpath('//textarea[@aria-label = "Terminal input"]'), + ); + } finally { + // Always switch back, even if the element isn't found - otherwise the + // driver stays parked inside this iframe and every subsequent test + // fails to find top-level elements like the workbench itself. + await webviewView.switchBack(); + } }); it('Terminal shows according information', async function() { diff --git a/test/ui/suite/componentCommands.ts b/test/ui/suite/componentCommands.ts index c5d89ecac..bbc990f97 100644 --- a/test/ui/suite/componentCommands.ts +++ b/test/ui/suite/componentCommands.ts @@ -13,13 +13,14 @@ import { SideBarView, TreeItem, ViewSection, + VSBrowser, Workbench, } from 'vscode-extension-tester'; import { parse } from 'yaml'; import { DevfileResolver } from '../../../src/devfile/devfileResolver'; import { waitForItemStable, warn } from '../common/conditions'; import { VIEWS } from '../common/constants'; -import { closeAllOpenEditors, reloadWindow } from '../common/overdrives'; +import { closeAllOpenEditors, collapseViews, reloadWindow } from '../common/overdrives'; import { OpenshiftTerminalWebviewView } from '../common/ui/webviewView/openshiftTerminalWebviewView'; export function testComponentCommands(path: string) { @@ -32,19 +33,17 @@ export function testComponentCommands(path: string) { const componentName = 'nodejs-starter'; before(async function context() { - this.timeout(30_000); + this.timeout(65_000); await closeAllOpenEditors(); view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); await (await new Workbench().openNotificationsCenter()).clearAllNotifications(); - for (const item of [ + await collapseViews(view, [ VIEWS.appExplorer, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions, - ]) { - await (await view.getContent().getSection(item)).collapse(); - } + ]); // expect component is running try { @@ -94,8 +93,14 @@ export function testComponentCommands(path: string) { expect(await component.hasChildren()).to.be.true; const commandsItem = await component.findChildItem('Commands'); - //check Commands has children - commands = await commandsItem.getChildren(); + //check Commands has children - expanding the item alone doesn't guarantee the + //child rows have rendered yet, so poll until getChildren() actually reports them. + await commandsItem.expand(); + await VSBrowser.instance.driver.wait(async () => { + commands = await commandsItem.getChildren(); + return commands.length > 0; + }, 15_000, 'Commands node children did not populate'); + const actualCommands = []; for (const command of commands) { actualCommands.push(await command.getLabel()); diff --git a/test/ui/suite/componentContextMenu.ts b/test/ui/suite/componentContextMenu.ts index 7a6e4aeb2..25fccb2ff 100644 --- a/test/ui/suite/componentContextMenu.ts +++ b/test/ui/suite/componentContextMenu.ts @@ -17,7 +17,7 @@ import { } from 'vscode-extension-tester'; import { findItemFuzzy, itemDoesNotExist, notificationDoesNotExist, stabilizeComponentsView, waitForItem, waitForItemStable, waitForItemToDisappear, warn } from '../common/conditions'; import { MENUS, VIEWS } from '../common/constants'; -import { closeAllOpenEditors, collapse } from '../common/overdrives'; +import { closeAllOpenEditors, collapse, collapseViews } from '../common/overdrives'; import { OpenshiftTerminalWebviewView } from '../common/ui/webviewView/openshiftTerminalWebviewView'; export function testComponentContextMenu() { @@ -34,14 +34,12 @@ export function testComponentContextMenu() { await closeAllOpenEditors(); view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); await (await new Workbench().openNotificationsCenter()).clearAllNotifications(); - for (const item of [ + await collapseViews(view, [ VIEWS.appExplorer, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions, - ]) { - await (await view.getContent().getSection(item)).collapse(); - } + ]); try { const item = await waitForItemStable(getSection, componentName, true, 40_000); diff --git a/test/ui/suite/createComponent.ts b/test/ui/suite/createComponent.ts index c1238c00b..83a1c0f1e 100644 --- a/test/ui/suite/createComponent.ts +++ b/test/ui/suite/createComponent.ts @@ -76,7 +76,7 @@ export function testCreateComponent(path: string) { }); it('Create component from git URL', async function test() { - this.timeout(25_000); + this.timeout(90_000); await clickCreateComponentStable(); @@ -103,7 +103,7 @@ export function testCreateComponent(path: string) { }); it('Create component from local folder', async function test() { - this.timeout(25_000); + this.timeout(90_000); await step(`Delete "${componentName}" component configuration` , async () => { await withStableItem(getSection, componentName, async (item) => { @@ -179,7 +179,7 @@ export function testCreateComponent(path: string) { }); it('Create component from template project', async function test() { - this.timeout(25_000); + this.timeout(90_000); //Click on create component await clickCreateComponentStable(); diff --git a/test/ui/suite/focusOn.ts b/test/ui/suite/focusOn.ts index a0dc63e36..975f9f1cf 100644 --- a/test/ui/suite/focusOn.ts +++ b/test/ui/suite/focusOn.ts @@ -4,9 +4,10 @@ *-----------------------------------------------------------------------------------------------*/ import { expect } from 'chai'; -import { ActivityBar, By, SideBarView, waitForAttributeValue } from 'vscode-extension-tester'; +import { ActivityBar, SideBarView } from 'vscode-extension-tester'; import { activateCommand } from '../common/command-activator'; import { VIEWS } from '../common/constants'; +import { collapseViews } from '../common/overdrives'; export function checkFocusOnCommands() { describe('Focus on Commands', () => { @@ -16,20 +17,7 @@ export function checkFocusOnCommands() { before('Open OpenShift View', async function(){ this.timeout(10_000); view = await (await new ActivityBar().getViewControl('OpenShift')).openView(); - for(const sectionName of sections){ - const section = await view.getContent().getSection(sectionName); - try{ - await section.collapse() - } catch { - if(await section.isExpanded()){ - const mainPanel = await section.findElement(By.className('pane-header')); - const arrowPanel = await section.findElement(By.className('codicon')); - await arrowPanel.click(); - await section.getDriver().wait(waitForAttributeValue(mainPanel, 'aria-expanded', 'false'), 2_000); - } - - } - } + await collapseViews(view, sections); }) sections.forEach(section => diff --git a/test/ui/suite/kubernetesContext.ts b/test/ui/suite/kubernetesContext.ts index f8381db12..6d805cedb 100644 --- a/test/ui/suite/kubernetesContext.ts +++ b/test/ui/suite/kubernetesContext.ts @@ -41,6 +41,7 @@ export function kubernetesContextTest(isOpenshiftCluster: boolean) { const kubeCopy = `${getKubeConfigPath()}-cp`; before(async function () { + this.timeout(30_000); view = await (await new ActivityBar().getViewControl(VIEWS.openshift)).openView(); explorer = await view.getContent().getSection(VIEWS.appExplorer); @@ -75,18 +76,33 @@ export function kubernetesContextTest(isOpenshiftCluster: boolean) { }); //put original kubeconfig back - after(async () => { - fs.moveSync(kubeCopy, getKubeConfigPath(), { overwrite: true }); + after(async function () { + this.timeout(15_000); + + // fs.moveSync() is a rename (delete+create) - the extension's kube-context cache + // is only refreshed by its file watcher's onDidChange handler, which explicitly + // ignores create/delete events, so a rename can be silently missed and leave the + // cached context stale for whatever test runs next. Overwrite the file content + // in place instead, which the watcher reliably picks up as a genuine change (same + // as addKubeContext() above does via writeFileSync), then give its ~500ms debounce + // time to actually process it before forcing a tree refresh. + const kubeContent = fs.readFileSync(kubeCopy, 'utf-8'); + fs.writeFileSync(getKubeConfigPath(), kubeContent); + fs.removeSync(kubeCopy); + await new Promise((res) => setTimeout(res, 1_500)); + const actions = await explorer.getActions(); await actions[3].click(); }); beforeEach(async function () { + // openNotificationsCenter() always opens the panel, so it must always be closed + // again here too - closing only "if notifications.length > 0" leaves it open + // (and able to interfere with later command palette/webview rendering) whenever + // there happen to be none. const notificationCenter = await new Workbench().openNotificationsCenter(); - const notifications = await notificationCenter.getNotifications(NotificationType.Any); - if (notifications.length > 0) { - await notificationCenter.close(); - } + await notificationCenter.getNotifications(NotificationType.Any); + await notificationCenter.close(); await closeAllOpenEditors(); }); diff --git a/test/ui/suite/openshift.ts b/test/ui/suite/openshift.ts index 3c85d2b8d..786a0ee47 100644 --- a/test/ui/suite/openshift.ts +++ b/test/ui/suite/openshift.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { ActivityBar, CustomTreeSection, SideBarView, ViewSection, WelcomeContentSection, Workbench } from 'vscode-extension-tester'; import { BUTTONS, VIEWS } from '../common/constants'; -import { collapse } from '../common/overdrives'; +import { collapse, collapseViews } from '../common/overdrives'; import { welcomeContentButtonsAreLoaded, welcomeContentIsLoaded } from '../common/conditions'; export function checkOpenshiftView() { @@ -39,9 +39,7 @@ export function checkOpenshiftView() { await explorer.expand(); welcome = await welcomeContentIsLoaded(explorer); - for (const item of [VIEWS.components, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions]) { - await (await view.getContent().getSection(item)).collapse(); - } + await collapseViews(view, [VIEWS.components, VIEWS.compRegistries, VIEWS.serverlessFunctions, VIEWS.debugSessions]); }); it('shows welcome content when not logged in', async function() { diff --git a/test/ui/suite/operatorBackedService.ts b/test/ui/suite/operatorBackedService.ts index 3df7c4a95..1618dc7c5 100644 --- a/test/ui/suite/operatorBackedService.ts +++ b/test/ui/suite/operatorBackedService.ts @@ -33,15 +33,27 @@ export function operatorBackedServiceTest() { section = await view.getContent().getSection(VIEWS.appExplorer); await closeAllOpenEditors(); - await (await new Workbench().openNotificationsCenter()).clearAllNotifications(); + // clearAllNotifications() only clicks "clear all" inside the panel - it doesn't + // close it, so a left-open Notifications Center can go on to interfere with + // context menus/quick-picks/webviews rendering for the rest of the suite. + const notificationsCenter = await new Workbench().openNotificationsCenter(); + await notificationsCenter.clearAllNotifications(); + await notificationsCenter.close(); }); after(async function () { + this.timeout(30_000); + try { + const notificationsCenter = await new Workbench().openNotificationsCenter(); + await notificationsCenter.clearAllNotifications(); + await notificationsCenter.close(); + } catch { /* Ignore */ } + await closeAllOpenEditors(); await reloadWindow(); }); it('Can create operator backed service', async function () { - this.timeout(80_000); + this.timeout(150_000); //get project, open context menu and select create new operator backed service const clusterItem = (await itemExists(clusterName, section)) as TreeItem; @@ -54,7 +66,14 @@ export function operatorBackedServiceTest() { //select service to be created const createServiceWebView = new CreateServiceWebView(); - await createServiceWebView.initializeEditor(); + await VSBrowser.instance.driver.wait(async () => { + try { + await createServiceWebView.initializeEditor(); + return true; + } catch { + return false; + } + }, 35_000, 'Create Service webview not initialized'); await createServiceWebView.clickComboBox(); await createServiceWebView.selectItemFromComboBox( 'Cluster', @@ -68,11 +87,13 @@ export function operatorBackedServiceTest() { serviceName = await serviceSetupPage.getName(); await serviceSetupPage.clickSubmit(); - //wait for notification about successful service creation + //wait for notification about successful service creation - bootstrapping a + //PostgreSQL cluster (image pull, initdb, pod startup) can genuinely take well + //over a minute on a loaded CI Kind cluster. await notificationExists( NOTIFICATIONS.serviceCreated(serviceName), VSBrowser.instance.driver, - 70_000 + 130_000 ); //check that deployment is shown diff --git a/test/ui/suite/project.ts b/test/ui/suite/project.ts index 16a018180..bf1ebe8bf 100644 --- a/test/ui/suite/project.ts +++ b/test/ui/suite/project.ts @@ -59,14 +59,42 @@ export function projectTest(isOpenshiftCluster: boolean) { }); //Switch back to existing project/namespace - after(async () => { + after(async function () { + this.timeout(30_000); + try { + const notifications = await new Workbench().getNotifications(); + for (const n of notifications) { + try { await n.dismiss(); } catch { /* Ignore */ } + } + } catch { /* Ignore */ } + try { + // "Delete a project" calls notificationExists() (common/conditions.ts), which + // opens the Notifications Center to poll for a message but never closes it - + // a left-open center can interfere with activateCommand()'s command palette + // rendering right below. + await (await new Workbench().openNotificationsCenter()).close(); + } catch { /* Ignore */ } + await closeAllOpenEditors(); + const option = isOpenshiftCluster ? 'Set Active Project' : 'Set Active Namespace'; const command = `>OpenShift: ${option}`; - await activateCommand(command); - const input = await InputBox.create(); - await input.setText(anotherProjectName); - await input.confirm(); + // The previous test just deleted the currently-active project, so the extension + // may still be mid-transition (updating its own state/notifications) when this + // runs - this exact "activate a command, then grab a fresh InputBox for its + // follow-up prompt" sequence isn't used successfully anywhere else in the suite, + // so retry the whole thing rather than trust a single attempt. + await VSBrowser.instance.driver.wait(async () => { + try { + await activateCommand(command); + const input = await InputBox.create(); + await input.setText(anotherProjectName); + await input.confirm(); + return true; + } catch { + return false; + } + }, 20_000, `Could not switch back to project "${anotherProjectName}"`); const explorer = await getExplorer(); (await itemExists(anotherProjectName, explorer)) as TreeItem; diff --git a/test/ui/suite/serverlessFunction.ts b/test/ui/suite/serverlessFunction.ts index ce198530c..aa82eb19a 100644 --- a/test/ui/suite/serverlessFunction.ts +++ b/test/ui/suite/serverlessFunction.ts @@ -47,7 +47,7 @@ export function testCreateServerlessFunction(path: string) { }); it('Create a new Serverless Function', async function test() { - this.timeout(20_000); + this.timeout(60_000); await collapse(section); await section.expand(); const createButton = await section.getAction('Create Function'); diff --git a/test/unit/index.ts b/test/unit/index.ts index 77936fb12..4e84a71b3 100644 --- a/test/unit/index.ts +++ b/test/unit/index.ts @@ -64,6 +64,7 @@ export async function run(): Promise { testFiles.push(...await testFinder('**/workspace.test.js')); testFiles.push(...await testFinder('openshift/component.test.js')); testFiles.push(...await testFinder('openshift/cluster.test.js')); + testFiles.push(...await testFinder('openshift/project.test.js')); testFiles.push(...await testFinder('devfile/*.test.js')); testFiles.push(...await testFinder('k8s/*.test.js')); testFiles.push(...await testFinder('util/*.test.js')); diff --git a/test/unit/openshift/project.test.ts b/test/unit/openshift/project.test.ts index f4a7a5d29..8bf5ab238 100644 --- a/test/unit/openshift/project.test.ts +++ b/test/unit/openshift/project.test.ts @@ -3,22 +3,23 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { KubernetesObject } from '@kubernetes/client-node'; import * as chai from 'chai'; import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; import * as vscode from 'vscode'; -import { CommandText } from '../../../src/base/command'; +import { CliChannel } from '../../../src/cli'; import { Oc } from '../../../src/oc/ocWrapper'; import { Project as OdoProject } from '../../../src/oc/project'; -import { Odo } from '../../../src/odo/odoWrapper'; import { Project } from '../../../src/openshift/project'; +import { ChildProcessUtil } from '../../../src/util/childProcessUtil'; +import * as inputValueUtil from '../../../src/util/inputValue'; const {expect} = chai; chai.use(sinonChai); suite('OpenShift/Project', () => { let sandbox: sinon.SinonSandbox; - let execStub: sinon.SinonStub; let createProjectStub: sinon.SinonStub; let deleteProjectStub: sinon.SinonStub; @@ -28,8 +29,10 @@ suite('OpenShift/Project', () => { setup(() => { projectItem = { name: 'project', active: true }; sandbox = sinon.createSandbox(); + // covers Oc's canCreateNamespace/canListNamespaces/getAllKubernetesObjects calls + sandbox.stub(ChildProcessUtil.prototype, 'execute').resolves({error: undefined, stdout: '', stderr: ''}); + sandbox.stub(CliChannel.getInstance(), 'executeSyncTool').resolves('apps.openshift.io/v1'); sandbox.stub(Oc.prototype, 'getProjects').resolves([projectItem]); - execStub = sandbox.stub(Odo.prototype, 'execute').resolves({error: undefined, stdout: '', stderr: ''}); createProjectStub = sandbox.stub(Oc.prototype, 'createProject').resolves(); deleteProjectStub = sandbox.stub(Oc.prototype, 'deleteProject').resolves(); }); @@ -42,7 +45,7 @@ suite('OpenShift/Project', () => { let inputStub: sinon.SinonStub; setup(() => { - inputStub = sandbox.stub(vscode.window, 'showInputBox').resolves(projectItem.name); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').resolves(projectItem.name); }); test('works with valid inputs', async () => { @@ -60,7 +63,7 @@ suite('OpenShift/Project', () => { }); test('wraps errors in additional info', async () => { - execStub.rejects(errorMessage); + createProjectStub.rejects(errorMessage); try { await Project.create(); expect.fail(); @@ -72,8 +75,8 @@ suite('OpenShift/Project', () => { test('validator returns undefined for valid project name', async () => { let result; inputStub.restore(); - inputStub = sandbox.stub(vscode.window, 'showInputBox').onFirstCall().callsFake(async (options?: vscode.InputBoxOptions): Promise => { - result = await options.validateInput('goodvalue'); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').onFirstCall().callsFake(async (_prompt, _initialValue, _password, validate): Promise => { + result = await validate('goodvalue'); return Promise.resolve('goodvalue'); }); await Project.create(); @@ -84,8 +87,8 @@ suite('OpenShift/Project', () => { test('validator returns error message for empty project name', async () => { let result; inputStub.restore(); - inputStub = sandbox.stub(vscode.window, 'showInputBox').onFirstCall().callsFake(async (options?: vscode.InputBoxOptions): Promise => { - result = await options.validateInput(''); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').onFirstCall().callsFake(async (_prompt, _initialValue, _password, validate): Promise => { + result = await validate(''); return Promise.resolve(''); }); await Project.create(); @@ -96,8 +99,8 @@ suite('OpenShift/Project', () => { test('validator returns error message for none alphanumeric project name', async () => { let result; inputStub.restore(); - inputStub = sandbox.stub(vscode.window, 'showInputBox').onFirstCall().callsFake(async (options?: vscode.InputBoxOptions): Promise => { - result = await options.validateInput('name&name'); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').onFirstCall().callsFake(async (_prompt, _initialValue, _password, validate): Promise => { + result = await validate('name&name'); return Promise.resolve('projectNameValidatorTest'); }); await Project.create(); @@ -108,8 +111,8 @@ suite('OpenShift/Project', () => { test('validator returns error message if same name of project found', async () => { let result; inputStub.restore(); - inputStub = sandbox.stub(vscode.window, 'showInputBox').onFirstCall().callsFake(async (options?: vscode.InputBoxOptions): Promise => { - result = await options.validateInput('project'); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').onFirstCall().callsFake(async (_prompt, _initialValue, _password, validate): Promise => { + result = await validate('project'); return Promise.resolve('project'); }); await Project.create(); @@ -120,8 +123,8 @@ suite('OpenShift/Project', () => { test('validator returns error message for project name longer than 63 characters', async () => { let result; inputStub.restore(); - inputStub = sandbox.stub(vscode.window, 'showInputBox').onFirstCall().callsFake(async (options?: vscode.InputBoxOptions): Promise => { - result = await options.validateInput('n123456789012345678901234567890123456789012345678901234567890123'); + inputStub = sandbox.stub(inputValueUtil, 'inputValue').onFirstCall().callsFake(async (_prompt, _initialValue, _password, validate): Promise => { + result = await validate('n123456789012345678901234567890123456789012345678901234567890123'); return Promise.resolve('projectLongNameValidatorTest'); }); await Project.create(); @@ -132,51 +135,82 @@ suite('OpenShift/Project', () => { suite('del', () => { let warnStub: sinon.SinonStub; + let projectObject: KubernetesObject; setup(() => { warnStub = sandbox.stub(vscode.window, 'showWarningMessage').resolves('Yes'); + sandbox.stub(Oc.prototype, 'getAllKubernetesObjects').resolves([]); + projectObject = { kind: 'Project', metadata: { name: projectItem.name } } as KubernetesObject; }); - // TODO: Fix me - // test('works with context', async () => { - // const result = await Project.del(projectItem); + test('works with context', async () => { + const mockOc = { + getAllKubernetesObjects: sandbox.stub().resolves([]), + getProjects: sandbox.stub().resolves([projectItem]), + deleteProject: sandbox.stub().resolves(), + } as unknown as Oc; - // expect(result).equals(`Project '${projectItem.getName()}' successfully deleted`); - // expect(`${execStub.getCall(0).args[0]}`).equals(`${Command.deleteProject(projectItem.getName())}`); - // }); + const result = await Project.del(projectObject, { oc: mockOc }); + + expect(result).equals(`Project '${projectItem.name}' successfully deleted`); + expect(mockOc.deleteProject).to.be.calledWith(projectItem.name); + expect(deleteProjectStub).not.called; + }); test('works without context', async () => { - const result = await Project.del(null); + const result = await Project.del(projectObject); expect(result).equals(`Project '${projectItem.name}' successfully deleted`); expect(deleteProjectStub).to.be.calledWith(projectItem.name); }); test('returns null when cancelled', async () => { warnStub.resolves('Cancel'); - const result = await Project.del(null); + const result = await Project.del(projectObject); expect(result).null; }); - // TODO: Fix me - // test('wraps errors in additional info', async () => { - // execStub.rejects(errorMessage); - // try { - // await Project.del(projectItem); - // expect.fail(); - // } catch (err) { - // expect(err.message).equals(`Failed to delete Project with error '${errorMessage}'`); - // } - // }); + test('throws when no project given', async () => { + try { + await Project.del(null); + expect.fail(); + } catch (err) { + expect(err.message).equals('Failed to delete Project: no project selected'); + } + }); + + test('throws when project is undefined', async () => { + try { + await Project.del(undefined); + expect.fail(); + } catch (err) { + expect(err.message).equals('Failed to delete Project: no project selected'); + } + }); + + test('wraps errors in additional info', async () => { + deleteProjectStub.rejects(errorMessage); + try { + await Project.del(projectObject); + expect.fail(); + } catch (err) { + expect(err.message).equals(`Failed to delete Project with error '${errorMessage}'`); + } + }); }); suite('set', () => { + let setProjectStub: sinon.SinonStub; + + setup(() => { + setProjectStub = sandbox.stub(Oc.prototype, 'setProject').resolves(); + }); test('makes selected project active', async () => { sandbox.stub(vscode.window, 'showQuickPick').resolves({ label: projectItem.name, }); const result = await Project.set(); - expect(execStub).calledWith(new CommandText('odo', `project set ${projectItem.name}`)); + expect(setProjectStub).calledWith(projectItem.name); expect(result).equals(`Project '${projectItem.name}' set as active.`); }); @@ -184,7 +218,7 @@ suite('OpenShift/Project', () => { sandbox.stub(vscode.window, 'showQuickPick').resolves(undefined); const result = await Project.set(); expect(result).null; - expect(execStub).not.called; + expect(setProjectStub).not.called; }); }); });