Compare commits

..

89 Commits

Author SHA1 Message Date
jamesgeorge007
73a5f4657c refactor: cleanup CollectionsProperties component 2024-05-28 19:50:13 +05:30
jamesgeorge007
4ba53a8bf3 refactor: remove redundancy from provider API method signatures 2024-05-28 19:31:41 +05:30
jamesgeorge007
2374ceb808 refactor: update exportRESTCollections method signature
- Drop the `collections` parameter since the it is already available in the `PersonalWorkspaceProviderService` context.
- Make the above method return a left error of `NO_COLLECTIONS_TO_EXPORT` when the collections list is empty.
- Error handling updates.
2024-05-27 11:34:50 +05:30
jamesgeorge007
17169e1c46 chore: bump @hoppscotch/ui 2024-05-23 22:30:11 +05:30
jamesgeorge007
4c74d0f865 chore: cleanup 2024-05-23 14:20:42 +05:30
jamesgeorge007
8b930a6d3d fix: resolve edge cases about moving collections under its sibling
- Increase test coverage.
- Move store mock data under `__tests__/__mocks__`.
- Rephrase test descriptions.
2024-05-22 21:00:49 +05:30
jamesgeorge007
f4a37f19c9 fix: empty state primary action for root collections
Ensure child collections are created instead of the action resulting in new root collections.
2024-05-22 21:00:49 +05:30
jamesgeorge007
86b17e2bd3 test: add test suite for personal workspace provider service 2024-05-22 21:00:49 +05:30
jamesgeorge007
e0083aa70d fix: handle based updates post collection move to a sibling level collection below it 2024-05-22 21:00:49 +05:30
jamesgeorge007
25b0818016 refactor: update inherited properties for affected requests flow updates 2024-05-22 21:00:46 +05:30
jamesgeorge007
648cc8f5bd refactor: handle based updates for affected requests post collection deletion 2024-05-22 20:59:59 +05:30
jamesgeorge007
6032cbb17b refactor: handle based updates for affected requests post request deletion 2024-05-22 20:59:59 +05:30
jamesgeorge007
52bff8ee68 refactor: handle based updates post collection reorder 2024-05-22 20:59:59 +05:30
jamesgeorge007
e342e53db1 refactor: handle based updates post collection move 2024-05-22 20:59:58 +05:30
jamesgeorge007
141652ffb6 fix: affected request indices computation post request reorder 2024-05-22 20:59:38 +05:30
jamesgeorge007
bb57b2248c fix: affected request indices computation post request move 2024-05-22 20:59:21 +05:30
jamesgeorge007
5d8da5fe49 chore: resolve type errors 2024-05-22 20:59:21 +05:30
Andrew Bastin
c8f0142b16 refactor: move more things to handles instead of handleref 2024-05-22 20:59:20 +05:30
jamesgeorge007
2f2273ee2c refactor: move to inert handles 2024-05-22 20:58:28 +05:30
jamesgeorge007
b239b6b4a6 refactor: handle updates post move request action
- Filter out duplicate issued handle entries.
- Move from `getAffectedIndexes` helper function to a custom implementation for updating affected request indices.
2024-05-22 20:57:48 +05:30
jamesgeorge007
bbac317b71 refactor: move to handle based updates with request move action 2024-05-22 20:57:48 +05:30
jamesgeorge007
412daa4d17 refactor: tab saveContext resolution post collection remove action 2024-05-22 20:57:48 +05:30
jamesgeorge007
0abdc63f0e refactor: better tab dirty check
Mark the tab (saved request under a collection) as not dirty if the request contents are reset to the value since previous save.
2024-05-22 20:57:47 +05:30
jamesgeorge007
9e8112a4e5 fix: make close all tabs action account for tabs with invalid request handles 2024-05-22 20:56:47 +05:30
jamesgeorge007
5aa57fce3f refactor: add save context resolution logic post request deletion 2024-05-22 20:56:47 +05:30
jamesgeorge007
8b65090dfb refactor: persist only request handles under tab saveContext at runtime
Remove provider, workspace and request IDs.
2024-05-22 20:56:47 +05:30
jamesgeorge007
7ca94a99b7 refactor: move tab saveContext resolution associated with actions on collections to be based on request handles 2024-05-22 20:56:46 +05:30
jamesgeorge007
fe3adeeb17 refactor: consider request handles with tab saveContext resolution for collection move/reorder actions 2024-05-22 20:56:17 +05:30
jamesgeorge007
3a195711a4 refactor: update data under request handles during tab save context resolution 2024-05-22 20:56:17 +05:30
jamesgeorge007
6cde6200ae refactor: signify updates via handle reference mutation post request move action 2024-05-22 20:56:17 +05:30
jamesgeorge007
e5e1260632 fix: ensure request name updates reflect immediately on the tabs 2024-05-22 20:56:17 +05:30
jamesgeorge007
90c9f2a9b1 fix: make writable handle operate on refs within the createRESTRequest method
Wrap the request handle data in a `ref` and make the writable handle operate over it ensuring reactive updates are received.
2024-05-22 20:56:17 +05:30
jamesgeorge007
db9ba17529 refactor: convey updates via handle mutation for update request action 2024-05-22 20:56:17 +05:30
jamesgeorge007
197d253e8b refactor: keep tab dirty status logic at the page level 2024-05-22 20:56:17 +05:30
jamesgeorge007
cd92dfec47 refactor: introduce writable handles to signify updates to handle references
A special list of writable handles is compiled in a list while issuing handles (request/collection creation, etc). Instead of manually computing the tab and toggling the dirty state, the writable handle is updated (changing the type to invalid on request deletion) and the tab with the request open can infer it via the update reflected in the request handle under the tab save context (reactive update trigger).
2024-05-22 20:53:49 +05:30
jamesgeorge007
8467417e7a refactor: persist request handles under tab saveContext
Only the IDs (workspace, provider & request IDs) to restore the handle are stored under `localStorage` and the handle is restored back at runtime.
2024-05-22 20:53:15 +05:30
jamesgeorge007
f6067f14aa fix: prevent infinite spinner state while expanding tree nodes 2024-05-22 20:52:57 +05:30
jamesgeorge007
3fd85df84b refactor: view implementation to retrieve collections for exporting 2024-05-22 20:52:57 +05:30
jamesgeorge007
01573cc51c chore: keep existing implementation for save context resolution 2024-05-22 20:52:57 +05:30
jamesgeorge007
b19486ea03 refactor: update provider method signatures + cleanup 2024-05-22 20:52:57 +05:30
jamesgeorge007
a729dfcacb fix: ensure tree nodes are not computed for requests 2024-05-22 20:52:57 +05:30
jamesgeorge007
62612e6c51 refactor: remove unnecessary safeguards + cleanup 2024-05-22 20:52:57 +05:30
jamesgeorge007
f87a4c81d8 refactor: leverage helpers 2024-05-22 20:52:57 +05:30
jamesgeorge007
116f2fd279 refactor: update provider method signatures 2024-05-22 20:52:57 +05:30
jamesgeorge007
7e2deaac5b fix: duplicate collection in search results
Ensure the entire collection tree is rendered if the search query matches a collection name.
2024-05-22 20:52:57 +05:30
jamesgeorge007
240b131e06 feat: support search at n level depth 2024-05-22 20:52:57 +05:30
jamesgeorge007
5b3986a53f fix: ensure the collection tree for search immediately reflects actions performed over it 2024-05-22 20:52:57 +05:30
jamesgeorge007
84fc31e8a9 refactor: port collection tree empty states 2024-05-22 20:52:56 +05:30
jamesgeorge007
c0978c3b20 refactor: eliminate parentCollectionID field from RESTCollectionViewRequest type
Collection ID serves the purpose.
2024-05-22 20:52:56 +05:30
jamesgeorge007
d70d5bdb16 refactor: eliminate collectionID from tab saveContext
Collection ID can be inferred from request ID by removing last index from the path.
2024-05-22 20:52:56 +05:30
jamesgeorge007
c30ee5becc refactor: session based search results view implementation 2024-05-22 20:50:32 +05:30
jamesgeorge007
5a64cdb7bc fix: update save context for affected requests with collection move/reorder 2024-05-22 20:50:32 +05:30
jamesgeorge007
89f2479845 refactor: integrate REST search collection adapter 2024-05-22 20:50:32 +05:30
jamesgeorge007
46654853f0 refactor: add new tree adapter corresponding to search 2024-05-22 20:50:32 +05:30
jamesgeorge007
af7e6b70cd refactor: view based implementation for search in personal workspace 2024-05-22 20:50:32 +05:30
jamesgeorge007
7c52c6b79d fix: associate requests under tabs while reordering collections 2024-05-22 20:50:32 +05:30
jamesgeorge007
fe01322bf7 refactor: integrate provider API methods for collection move/reorder 2024-05-22 20:50:32 +05:30
jamesgeorge007
0a0f441da1 refactor: unify markup 2024-05-22 20:50:32 +05:30
jamesgeorge007
d0c7c4a245 refactor: provider method definitions for collection reorder/move 2024-05-22 20:50:32 +05:30
jamesgeorge007
076006c4a6 refactor: port collection move/reorder 2024-05-22 20:50:32 +05:30
jamesgeorge007
6ed9c09f06 refactor: port import/export functionality 2024-05-22 20:50:31 +05:30
jamesgeorge007
8483339005 feat: add keypress actions 2024-05-22 20:49:49 +05:30
jamesgeorge007
cd23bb63c1 refactor: remove fields associated with pagination
Fix lint errors
2024-05-22 20:49:13 +05:30
James George
f4ea999d2d refactor: remove unnecessary imports and local state variable 2024-05-22 20:49:13 +05:30
jamesgeorge007
c43e4fcefd fix: open request straightaway after creating via save-as 2024-05-22 20:49:13 +05:30
jamesgeorge007
316dc8f759 refactor: persist IDs under tab save context 2024-05-22 20:49:12 +05:30
jamesgeorge007
89f7c2ce5e refactor: port share requests 2024-05-22 20:41:24 +05:30
jamesgeorge007
0d00826019 fix: ensure removing collection level headers persists 2024-05-22 20:41:24 +05:30
jamesgeorge007
00285df348 refactor: remove side effects from computed properties 2024-05-22 20:41:24 +05:30
jamesgeorge007
b821f452cf fix: ensure the reference is not kept while overwriting requests 2024-05-22 20:41:24 +05:30
jamesgeorge007
faa0bf7714 fix: updates to collection level authorization and headers reflect at the request level straightaway 2024-05-22 20:41:23 +05:30
jamesgeorge007
3a176f6620 fix: invalidate requests opened under tabs on deleting parent collection 2024-05-22 20:33:04 +05:30
jamesgeorge007
7549e456c8 fix: specify correct request index with update action 2024-05-22 20:33:04 +05:30
jamesgeorge007
68795a5017 refactor: port collection tree rendered in the save request modal to the new implementation 2024-05-22 20:33:03 +05:30
jamesgeorge007
b0c72fd295 feat: indicate request opened in the tab from the collection tree 2024-05-22 20:32:18 +05:30
jamesgeorge007
63eca80ff6 fix: prevent the need for an explicit save while editing request name 2024-05-22 20:32:18 +05:30
jamesgeorge007
30b6a67505 fix: prevent duplicate request creation and invalidate tabs with request deletion 2024-05-22 20:32:18 +05:30
jamesgeorge007
97899ec023 chore: cleanup 2024-05-22 20:32:18 +05:30
jamesgeorge007
2c47a63ca0 refactor: update provider method signature 2024-05-22 20:32:18 +05:30
jamesgeorge007
a0e373a4f3 refactor: persist request handle under tab saveContext
Bump vue version.
2024-05-22 20:32:16 +05:30
jamesgeorge007
392b2fc48d refactor: updates based on the provider methods signature changes 2024-05-22 20:30:55 +05:30
jamesgeorge007
c1a8a871d2 refactor: save request handle in tabs and remove tabs related logic from personal provider definition 2024-05-22 20:28:56 +05:30
jamesgeorge007
1abbdb0fe0 refactor: consistent return formats 2024-05-22 20:21:43 +05:30
jamesgeorge007
f0f504d10e refactor: unify edit collection API methods and ensure consistent naming convention 2024-05-22 20:21:42 +05:30
jamesgeorge007
f0dab55c99 refactor: prevent storing entire collection data in the respective handle 2024-05-22 20:21:15 +05:30
jamesgeorge007
d6a8e60239 refactor: finalize API methods 2024-05-22 20:21:14 +05:30
jamesgeorge007
89bcc58de6 refactor: compile data in handles
Introduce a handle for requests.
2024-05-22 20:16:54 +05:30
jamesgeorge007
ab7df212c2 refactor: iterations 2024-05-22 20:11:39 +05:30
jamesgeorge007
29e25b0ead refactor: initial iterations
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2024-05-22 20:10:50 +05:30
356 changed files with 20305 additions and 24081 deletions

View File

@@ -9,9 +9,6 @@ MAGIC_LINK_TOKEN_VALIDITY= 3
REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days (604800000 ms) in ms
ACCESS_TOKEN_VALIDITY="86400000" # Default validity is 1 day (86400000 ms) in ms
SESSION_SECRET='add some secret here'
# Reccomended to be true, set to false if you are using http
# Note: Some auth providers may not support http requests
ALLOW_SECURE_COOKIES=true
# Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000"
@@ -38,20 +35,9 @@ MICROSOFT_SCOPE="user.read"
MICROSOFT_TENANT="common"
# Mailer config
MAILER_SMTP_ENABLE="true"
MAILER_USE_CUSTOM_CONFIGS="false"
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" # used if custom mailer configs is false
# The following are used if custom mailer configs is true
MAILER_SMTP_HOST="smtp.domain.com"
MAILER_SMTP_PORT="587"
MAILER_SMTP_SECURE="true"
MAILER_SMTP_USER="user@domain.com"
MAILER_SMTP_PASSWORD="pass"
MAILER_TLS_REJECT_UNAUTHORIZED="true"
# Rate Limit Config
RATE_LIMIT_TTL=60 # In seconds
RATE_LIMIT_MAX=100 # Max requests per IP
@@ -61,7 +47,6 @@ RATE_LIMIT_MAX=100 # Max requests per IP
# Base URLs
VITE_BACKEND_LOGIN_API_URL=http://localhost:5444
VITE_BASE_URL=http://localhost:3000
VITE_SHORTCODE_BASE_URL=http://localhost:3000
VITE_ADMIN_URL=http://localhost:3100

View File

@@ -7,15 +7,20 @@ Please make sure that the pull request is limited to one type (docs, feature, et
<!-- If this pull request closes an issue, please mention the issue number below -->
Closes # <!-- Issue # here -->
<!-- Add an introduction into what this PR tries to solve in a couple of sentences -->
### What's changed
<!-- Describe point by point the different things you have changed in this PR -->
### Description
<!-- Add a brief description of the pull request -->
<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->
### Notes to reviewers
<!-- Any information you feel the reviewer should know about when reviewing your PR -->
### Checks
<!-- Make sure your pull request passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed
### Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behaviour, etc. -->

View File

@@ -1,21 +1,30 @@
# CODEOWNERS is prioritized from bottom to top
# If none of the below matched
* @AndrewBastin @liyasthomas
# Packages
/packages/codemirror-lang-graphql/ @AndrewBastin
/packages/hoppscotch-cli/ @jamesgeorge007
/packages/hoppscotch-cli/ @AndrewBastin
/packages/hoppscotch-common/ @amk-dev @AndrewBastin
/packages/hoppscotch-data/ @AndrewBastin
/packages/hoppscotch-js-sandbox/ @jamesgeorge007
/packages/hoppscotch-selfhost-web/ @jamesgeorge007
/packages/hoppscotch-selfhost-desktop/ @AndrewBastin
/packages/hoppscotch-js-sandbox/ @AndrewBastin
/packages/hoppscotch-ui/ @anwarulislam
/packages/hoppscotch-web/ @amk-dev
/packages/hoppscotch-selfhost-web/ @amk-dev
/packages/hoppscotch-sh-admin/ @JoelJacobStephen
/packages/hoppscotch-backend/ @balub
/packages/hoppscotch-backend/ @ankitsridhar16 @balub
# READMEs and other documentation files
*.md @liyasthomas
# Sections within Hoppscotch Common
/packages/hoppscotch-common/src/components @anwarulislam
/packages/hoppscotch-common/src/components/collections @nivedin @amk-dev
/packages/hoppscotch-common/src/components/environments @nivedin @amk-dev
/packages/hoppscotch-common/src/composables @amk-dev
/packages/hoppscotch-common/src/modules @AndrewBastin @amk-dev
/packages/hoppscotch-common/src/pages @AndrewBastin @amk-dev
/packages/hoppscotch-common/src/newstore @AndrewBastin @amk-dev
# Self Host deployment related files
*.Dockerfile @balub
docker-compose.yml @balub
docker-compose.deploy.yml @balub
*.Caddyfile @balub
.dockerignore @balub
README.md @liyasthomas
# The lockfile has no owner
pnpm-lock.yaml

View File

@@ -11,4 +11,7 @@ Please note we have a code of conduct, please follow it in all your interactions
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Make sure you do not expose environment variables or other sensitive information in your PR.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer merge it for you.

View File

@@ -4,36 +4,19 @@ This document outlines security procedures and general policies for the Hoppscot
- [Security Policy](#security-policy)
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
- [What is not a valid vulnerability](#what-is-not-a-valid-vulnerability)
- [Incident response process](#incident-response-process)
## Reporting a security vulnerability
We use [Github Security Advisories](https://github.com/hoppscotch/hoppscotch/security/advisories) to manage vulnerability reports and collaboration.
Someone from the Hoppscotch team shall report to you within 48 hours of the disclosure of the vulnerability in GHSA. If no response was received, please reach out to
Hoppscotch Support at support@hoppscotch.io along with the GHSA advisory link.
Report security vulnerabilities by emailing the Hoppscotch Support team at support@hoppscotch.io.
> NOTE: Since we have multiple open source components, Advisories may move into the relevant repo (for example, an XSS in a UI component might be part of [`@hoppscotch/ui`](https://github.com/hoppscotch/ui)).
> If in doubt, open your report in `hoppscotch/hoppscotch` GHSA.
The primary security point of contact from Hoppscotch Support team will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
**Do not create a GitHub issue ticket to report a security vulnerability!**
**Do not create a GitHub issue ticket to report a security vulnerability.**
The Hoppscotch team takes all security vulnerability reports in Hoppscotch seriously. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
The Hoppscotch team and community take all security vulnerability reports in Hoppscotch seriously. Thank you for improving the security of Hoppscotch. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
## What is not a valid vulnerability
We receive many reports about different sections of the Hoppscotch platform. Hence, we have a fine line we have drawn defining what is considered valid vulnerability.
Please refrain from opening an advisory if it describes the following:
- A vulnerability in a dependency of Hoppscotch (unless you have practical attack with it on the Hoppscotch codebase)
- Reports of vulnerabilities related to old runtimes (like NodeJS) or container images used by the codebase
- Vulnerabilities present when using Hoppscotch in anything other than the defined minimum requirements that Hoppscotch supports.
Hoppscotch Team ensures security support for:
- Modern Browsers (Chrome/Firefox/Safari/Edge) with versions up to 1 year old.
- Windows versions on or above Windows 10 on Intel and ARM.
- macOS versions dating back up to 2 years on Intel and Apple Silicon.
- Popular Linux distributions with up-to-date packages with preference to x86/64 CPUs.
- Docker/OCI Runtimes (preference to Docker and Podman) dating back up to 1 year.
Report security bugs in third-party modules to the person or team maintaining the module.
## Incident response process

View File

@@ -100,7 +100,7 @@ services:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s

View File

@@ -25,7 +25,7 @@
"devDependencies": {
"@commitlint/cli": "16.3.0",
"@commitlint/config-conventional": "16.2.4",
"@hoppscotch/ui": "0.2.0",
"@hoppscotch/ui": "0.1.0",
"@types/node": "17.0.27",
"cross-env": "7.0.3",
"http-server": "14.1.1",
@@ -34,10 +34,10 @@
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
"vue": "3.4.27"
},
"packageExtensions": {
"@hoppscotch/httpsnippet": {
"httpsnippet@3.0.1": {
"dependencies": {
"ajv": "6.12.3"
}

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2024.7.0",
"version": "2024.3.3",
"description": "",
"author": "",
"private": true,
@@ -35,14 +35,11 @@
"@nestjs/passport": "10.0.2",
"@nestjs/platform-express": "10.2.7",
"@nestjs/schedule": "4.0.1",
"@nestjs/swagger": "7.4.0",
"@nestjs/terminus": "10.2.3",
"@nestjs/throttler": "5.0.1",
"@prisma/client": "5.8.1",
"argon2": "0.30.3",
"bcrypt": "5.1.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"cookie": "0.5.0",
"cookie-parser": "1.4.6",
"cron": "3.1.6",

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "lastLoggedOn" TIMESTAMP(3);

View File

@@ -1,19 +0,0 @@
-- CreateTable
CREATE TABLE "PersonalAccessToken" (
"id" TEXT NOT NULL,
"userUid" TEXT NOT NULL,
"label" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresOn" TIMESTAMP(3),
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PersonalAccessToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PersonalAccessToken_token_key" ON "PersonalAccessToken"("token");
-- AddForeignKey
ALTER TABLE "PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "lastActiveOn" TIMESTAMP(3);

View File

@@ -1,15 +0,0 @@
-- CreateTable
CREATE TABLE "InfraToken" (
"id" TEXT NOT NULL,
"creatorUid" TEXT NOT NULL,
"label" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expiresOn" TIMESTAMP(3),
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InfraToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "InfraToken_token_key" ON "InfraToken"("token");

View File

@@ -41,31 +41,31 @@ model TeamInvitation {
}
model TeamCollection {
id String @id @default(cuid())
id String @id @default(cuid())
parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model TeamRequest {
id String @id @default(cuid())
id String @id @default(cuid())
collectionID String
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
request Json
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model Shortcode {
@@ -89,27 +89,24 @@ model TeamEnvironment {
}
model User {
uid String @id @default(cuid())
displayName String?
email String? @unique
photoURL String?
isAdmin Boolean @default(false)
refreshToken String?
providerAccounts Account[]
VerificationToken VerificationToken[]
settings UserSettings?
UserHistory UserHistory[]
UserEnvironments UserEnvironment[]
userCollections UserCollection[]
userRequests UserRequest[]
currentRESTSession Json?
currentGQLSession Json?
lastLoggedOn DateTime? @db.Timestamp(3)
lastActiveOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
personalAccessTokens PersonalAccessToken[]
uid String @id @default(cuid())
displayName String?
email String? @unique
photoURL String?
isAdmin Boolean @default(false)
refreshToken String?
providerAccounts Account[]
VerificationToken VerificationToken[]
settings UserSettings?
UserHistory UserHistory[]
UserEnvironments UserEnvironment[]
userCollections UserCollection[]
userRequests UserRequest[]
currentRESTSession Json?
currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
}
model Account {
@@ -221,24 +218,3 @@ model InfraConfig {
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model PersonalAccessToken {
id String @id @default(cuid())
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
label String
token String @unique @default(uuid())
expiresOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model InfraToken {
id String @id @default(cuid())
creatorUid String
label String
token String @unique @default(uuid())
expiresOn DateTime? @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @default(now()) @db.Timestamp(3)
}

View File

@@ -1,107 +0,0 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
ParseIntPipe,
Post,
Query,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AccessTokenService } from './access-token.service';
import { CreateAccessTokenDto } from './dto/create-access-token.dto';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import * as E from 'fp-ts/Either';
import { throwHTTPErr } from 'src/utils';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { AuthUser } from 'src/types/AuthUser';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { PATAuthGuard } from 'src/guards/rest-pat-auth.guard';
import { AccessTokenInterceptor } from 'src/interceptors/access-token.interceptor';
import { TeamEnvironmentsService } from 'src/team-environments/team-environments.service';
import { TeamCollectionService } from 'src/team-collection/team-collection.service';
import { ACCESS_TOKENS_INVALID_DATA_ID } from 'src/errors';
import { createCLIErrorResponse } from './helper';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'access-tokens', version: '1' })
export class AccessTokenController {
constructor(
private readonly accessTokenService: AccessTokenService,
private readonly teamCollectionService: TeamCollectionService,
private readonly teamEnvironmentsService: TeamEnvironmentsService,
) {}
@Post('create')
@UseGuards(JwtAuthGuard)
async createPAT(
@GqlUser() user: AuthUser,
@Body() createAccessTokenDto: CreateAccessTokenDto,
) {
const result = await this.accessTokenService.createPAT(
createAccessTokenDto,
user,
);
if (E.isLeft(result)) throwHTTPErr(result.left);
return result.right;
}
@Delete('revoke')
@UseGuards(JwtAuthGuard)
async deletePAT(@Query('id') id: string) {
const result = await this.accessTokenService.deletePAT(id);
if (E.isLeft(result)) throwHTTPErr(result.left);
return result.right;
}
@Get('list')
@UseGuards(JwtAuthGuard)
async listAllUserPAT(
@GqlUser() user: AuthUser,
@Query('offset', ParseIntPipe) offset: number,
@Query('limit', ParseIntPipe) limit: number,
) {
return await this.accessTokenService.listAllUserPAT(
user.uid,
offset,
limit,
);
}
@Get('collection/:id')
@UseGuards(PATAuthGuard)
@UseInterceptors(AccessTokenInterceptor)
async fetchCollection(@GqlUser() user: AuthUser, @Param('id') id: string) {
const res = await this.teamCollectionService.getCollectionForCLI(
id,
user.uid,
);
if (E.isLeft(res))
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKENS_INVALID_DATA_ID),
);
return res.right;
}
@Get('environment/:id')
@UseGuards(PATAuthGuard)
@UseInterceptors(AccessTokenInterceptor)
async fetchEnvironment(@GqlUser() user: AuthUser, @Param('id') id: string) {
const res = await this.teamEnvironmentsService.getTeamEnvironmentForCLI(
id,
user.uid,
);
if (E.isLeft(res))
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKENS_INVALID_DATA_ID),
);
return res.right;
}
}

View File

@@ -1,20 +0,0 @@
import { Module } from '@nestjs/common';
import { AccessTokenController } from './access-token.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
import { AccessTokenService } from './access-token.service';
import { TeamCollectionModule } from 'src/team-collection/team-collection.module';
import { TeamEnvironmentsModule } from 'src/team-environments/team-environments.module';
import { TeamModule } from 'src/team/team.module';
@Module({
imports: [
PrismaModule,
TeamCollectionModule,
TeamEnvironmentsModule,
TeamModule,
],
controllers: [AccessTokenController],
providers: [AccessTokenService],
exports: [AccessTokenService],
})
export class AccessTokenModule {}

View File

@@ -1,196 +0,0 @@
import { AccessTokenService } from './access-token.service';
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import {
ACCESS_TOKEN_EXPIRY_INVALID,
ACCESS_TOKEN_LABEL_SHORT,
ACCESS_TOKEN_NOT_FOUND,
} from 'src/errors';
import { AuthUser } from 'src/types/AuthUser';
import { PersonalAccessToken } from '@prisma/client';
import { AccessToken } from 'src/types/AccessToken';
import { HttpStatus } from '@nestjs/common';
const mockPrisma = mockDeep<PrismaService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const accessTokenService = new AccessTokenService(mockPrisma);
const currentTime = new Date();
const user: AuthUser = {
uid: '123344',
email: 'dwight@dundermifflin.com',
displayName: 'Dwight Schrute',
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
createdOn: currentTime,
currentGQLSession: {},
currentRESTSession: {},
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
};
const PATCreatedOn = new Date();
const expiryInDays = 7;
const PATExpiresOn = new Date(
PATCreatedOn.getTime() + expiryInDays * 24 * 60 * 60 * 1000,
);
const userAccessToken: PersonalAccessToken = {
id: 'skfvhj8uvdfivb',
userUid: user.uid,
label: 'test',
token: '0140e328-b187-4823-ae4b-ed4bec832ac2',
expiresOn: PATExpiresOn,
createdOn: PATCreatedOn,
updatedOn: new Date(),
};
const userAccessTokenCasted: AccessToken = {
id: userAccessToken.id,
label: userAccessToken.label,
createdOn: userAccessToken.createdOn,
lastUsedOn: userAccessToken.updatedOn,
expiresOn: userAccessToken.expiresOn,
};
beforeEach(() => {
mockReset(mockPrisma);
});
describe('AccessTokenService', () => {
describe('createPAT', () => {
test('should throw ACCESS_TOKEN_LABEL_SHORT if label is too short', async () => {
const result = await accessTokenService.createPAT(
{
label: 'a',
expiryInDays: 7,
},
user,
);
expect(result).toEqualLeft({
message: ACCESS_TOKEN_LABEL_SHORT,
statusCode: HttpStatus.BAD_REQUEST,
});
});
test('should throw ACCESS_TOKEN_EXPIRY_INVALID if expiry date is invalid', async () => {
const result = await accessTokenService.createPAT(
{
label: 'test',
expiryInDays: 9,
},
user,
);
expect(result).toEqualLeft({
message: ACCESS_TOKEN_EXPIRY_INVALID,
statusCode: HttpStatus.BAD_REQUEST,
});
});
test('should successfully create a new Access Token', async () => {
mockPrisma.personalAccessToken.create.mockResolvedValueOnce(
userAccessToken,
);
const result = await accessTokenService.createPAT(
{
label: userAccessToken.label,
expiryInDays,
},
user,
);
expect(result).toEqualRight({
token: `pat-${userAccessToken.token}`,
info: userAccessTokenCasted,
});
});
});
describe('deletePAT', () => {
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
mockPrisma.personalAccessToken.delete.mockRejectedValueOnce(
'RecordNotFound',
);
const result = await accessTokenService.deletePAT(userAccessToken.id);
expect(result).toEqualLeft({
message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
});
test('should successfully delete a new Access Token', async () => {
mockPrisma.personalAccessToken.delete.mockResolvedValueOnce(
userAccessToken,
);
const result = await accessTokenService.deletePAT(userAccessToken.id);
expect(result).toEqualRight(true);
});
});
describe('listAllUserPAT', () => {
test('should successfully return a list of user Access Tokens', async () => {
mockPrisma.personalAccessToken.findMany.mockResolvedValueOnce([
userAccessToken,
]);
const result = await accessTokenService.listAllUserPAT(user.uid, 0, 10);
expect(result).toEqual([userAccessTokenCasted]);
});
});
describe('getUserPAT', () => {
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
mockPrisma.personalAccessToken.findUniqueOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
const result = await accessTokenService.getUserPAT(userAccessToken.token);
expect(result).toEqualLeft(ACCESS_TOKEN_NOT_FOUND);
});
test('should successfully return a user Access Tokens', async () => {
mockPrisma.personalAccessToken.findUniqueOrThrow.mockResolvedValueOnce({
...userAccessToken,
user,
} as any);
const result = await accessTokenService.getUserPAT(
`pat-${userAccessToken.token}`,
);
expect(result).toEqualRight({
user,
...userAccessToken,
} as any);
});
});
describe('updateLastUsedforPAT', () => {
test('should throw ACCESS_TOKEN_NOT_FOUND if Access Token is not found', async () => {
mockPrisma.personalAccessToken.update.mockRejectedValueOnce(
'RecordNotFound',
);
const result = await accessTokenService.updateLastUsedForPAT(
userAccessToken.token,
);
expect(result).toEqualLeft(ACCESS_TOKEN_NOT_FOUND);
});
test('should successfully update lastUsedOn for a user Access Tokens', async () => {
mockPrisma.personalAccessToken.update.mockResolvedValueOnce(
userAccessToken,
);
const result = await accessTokenService.updateLastUsedForPAT(
`pat-${userAccessToken.token}`,
);
expect(result).toEqualRight(userAccessTokenCasted);
});
});
});

View File

@@ -1,190 +0,0 @@
import { HttpStatus, Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateAccessTokenDto } from './dto/create-access-token.dto';
import { AuthUser } from 'src/types/AuthUser';
import { calculateExpirationDate, isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either';
import {
ACCESS_TOKEN_EXPIRY_INVALID,
ACCESS_TOKEN_LABEL_SHORT,
ACCESS_TOKEN_NOT_FOUND,
} from 'src/errors';
import { CreateAccessTokenResponse } from './helper';
import { PersonalAccessToken } from '@prisma/client';
import { AccessToken } from 'src/types/AccessToken';
@Injectable()
export class AccessTokenService {
constructor(private readonly prisma: PrismaService) {}
TITLE_LENGTH = 3;
VALID_TOKEN_DURATIONS = [7, 30, 60, 90];
TOKEN_PREFIX = 'pat-';
/**
* Validate the expiration date of the token
*
* @param expiresOn Number of days the token is valid for
* @returns Boolean indicating if the expiration date is valid
*/
private validateExpirationDate(expiresOn: null | number) {
if (expiresOn === null || this.VALID_TOKEN_DURATIONS.includes(expiresOn))
return true;
return false;
}
/**
* Typecast a database PersonalAccessToken to a AccessToken model
* @param token database PersonalAccessToken
* @returns AccessToken model
*/
private cast(token: PersonalAccessToken): AccessToken {
return <AccessToken>{
id: token.id,
label: token.label,
createdOn: token.createdOn,
expiresOn: token.expiresOn,
lastUsedOn: token.updatedOn,
};
}
/**
* Extract UUID from the token
*
* @param token Personal Access Token
* @returns UUID of the token
*/
private extractUUID(token): string | null {
if (!token.startsWith(this.TOKEN_PREFIX)) return null;
return token.slice(this.TOKEN_PREFIX.length);
}
/**
* Create a Personal Access Token
*
* @param createAccessTokenDto DTO for creating a Personal Access Token
* @param user AuthUser object
* @returns Either of the created token or error message
*/
async createPAT(createAccessTokenDto: CreateAccessTokenDto, user: AuthUser) {
const isTitleValid = isValidLength(
createAccessTokenDto.label,
this.TITLE_LENGTH,
);
if (!isTitleValid)
return E.left({
message: ACCESS_TOKEN_LABEL_SHORT,
statusCode: HttpStatus.BAD_REQUEST,
});
if (!this.validateExpirationDate(createAccessTokenDto.expiryInDays))
return E.left({
message: ACCESS_TOKEN_EXPIRY_INVALID,
statusCode: HttpStatus.BAD_REQUEST,
});
const createdPAT = await this.prisma.personalAccessToken.create({
data: {
userUid: user.uid,
label: createAccessTokenDto.label,
expiresOn: calculateExpirationDate(createAccessTokenDto.expiryInDays),
},
});
const res: CreateAccessTokenResponse = {
token: `${this.TOKEN_PREFIX}${createdPAT.token}`,
info: this.cast(createdPAT),
};
return E.right(res);
}
/**
* Delete a Personal Access Token
*
* @param accessTokenID ID of the Personal Access Token
* @returns Either of true or error message
*/
async deletePAT(accessTokenID: string) {
try {
await this.prisma.personalAccessToken.delete({
where: { id: accessTokenID },
});
return E.right(true);
} catch {
return E.left({
message: ACCESS_TOKEN_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
}
}
/**
* List all Personal Access Tokens of a user
*
* @param userUid UID of the user
* @param offset Offset for pagination
* @param limit Limit for pagination
* @returns Either of the list of Personal Access Tokens or error message
*/
async listAllUserPAT(userUid: string, offset: number, limit: number) {
const userPATs = await this.prisma.personalAccessToken.findMany({
where: {
userUid: userUid,
},
skip: offset,
take: limit,
orderBy: {
createdOn: 'desc',
},
});
const userAccessTokenList = userPATs.map((pat) => this.cast(pat));
return userAccessTokenList;
}
/**
* Get a Personal Access Token
*
* @param accessToken Personal Access Token
* @returns Either of the Personal Access Token or error message
*/
async getUserPAT(accessToken: string) {
const extractedToken = this.extractUUID(accessToken);
if (!extractedToken) return E.left(ACCESS_TOKEN_NOT_FOUND);
try {
const userPAT = await this.prisma.personalAccessToken.findUniqueOrThrow({
where: { token: extractedToken },
include: { user: true },
});
return E.right(userPAT);
} catch {
return E.left(ACCESS_TOKEN_NOT_FOUND);
}
}
/**
* Update the last used date of a Personal Access Token
*
* @param token Personal Access Token
* @returns Either of the updated Personal Access Token or error message
*/
async updateLastUsedForPAT(token: string) {
const extractedToken = this.extractUUID(token);
if (!extractedToken) return E.left(ACCESS_TOKEN_NOT_FOUND);
try {
const updatedAccessToken = await this.prisma.personalAccessToken.update({
where: { token: extractedToken },
data: {
updatedOn: new Date(),
},
});
return E.right(this.cast(updatedAccessToken));
} catch {
return E.left(ACCESS_TOKEN_NOT_FOUND);
}
}
}

View File

@@ -1,5 +0,0 @@
// Inputs to create a new PAT
export class CreateAccessTokenDto {
label: string;
expiryInDays: number | null;
}

View File

@@ -1,17 +0,0 @@
import { AccessToken } from 'src/types/AccessToken';
// Response type of PAT creation method
export type CreateAccessTokenResponse = {
token: string;
info: AccessToken;
};
// Response type of any error in PAT module
export type CLIErrorResponse = {
reason: string;
};
// Return a CLIErrorResponse object
export function createCLIErrorResponse(reason: string): CLIErrorResponse {
return { reason };
}

View File

@@ -74,8 +74,6 @@ const dbAdminUsers: DbUser[] = [
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
lastLoggedOn: new Date(),
lastActiveOn: new Date(),
createdOn: new Date(),
},
{
@@ -87,11 +85,20 @@ const dbAdminUsers: DbUser[] = [
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
lastLoggedOn: new Date(),
lastActiveOn: new Date(),
createdOn: new Date(),
},
];
const dbNonAminUser: DbUser = {
uid: 'uid 3',
displayName: 'displayName',
email: 'email@email.com',
photoURL: 'photoURL',
isAdmin: false,
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
createdOn: new Date(),
};
describe('AdminService', () => {
describe('fetchInvitedUsers', () => {
@@ -114,7 +121,6 @@ describe('AdminService', () => {
NOT: {
inviteeEmail: {
in: [dbAdminUsers[0].email],
mode: 'insensitive',
},
},
},
@@ -223,10 +229,7 @@ describe('AdminService', () => {
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
where: {
inviteeEmail: {
in: [invitedUsers[0].inviteeEmail],
mode: 'insensitive',
},
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
},
});
expect(result).toEqualRight(true);

View File

@@ -89,17 +89,12 @@ export class AdminService {
adminEmail: string,
inviteeEmail: string,
) {
if (inviteeEmail.toLowerCase() == adminEmail.toLowerCase()) {
return E.left(DUPLICATE_EMAIL);
}
if (inviteeEmail == adminEmail) return E.left(DUPLICATE_EMAIL);
if (!validateEmail(inviteeEmail)) return E.left(INVALID_EMAIL);
const alreadyInvitedUser = await this.prisma.invitedUsers.findFirst({
where: {
inviteeEmail: {
equals: inviteeEmail,
mode: 'insensitive',
},
inviteeEmail: inviteeEmail,
},
});
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
@@ -161,17 +156,10 @@ export class AdminService {
* @returns an Either of boolean or error string
*/
async revokeUserInvitations(inviteeEmails: string[]) {
const areAllEmailsValid = inviteeEmails.every((email) =>
validateEmail(email),
);
if (!areAllEmailsValid) {
return E.left(INVALID_EMAIL);
}
try {
await this.prisma.invitedUsers.deleteMany({
where: {
inviteeEmail: { in: inviteeEmails, mode: 'insensitive' },
inviteeEmail: { in: inviteeEmails },
},
});
return E.right(true);
@@ -201,7 +189,6 @@ export class AdminService {
NOT: {
inviteeEmail: {
in: userEmailObjs.map((user) => user.email),
mode: 'insensitive',
},
},
},

View File

@@ -359,23 +359,4 @@ export class InfraResolver {
return true;
}
@Mutation(() => Boolean, {
description: 'Enable or Disable SMTP for sending emails',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async toggleSMTP(
@Args({
name: 'status',
type: () => ServiceStatus,
description: 'Toggle SMTP',
})
status: ServiceStatus,
) {
const isUpdated = await this.infraConfigService.enableAndDisableSMTP(
status,
);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;
}
}

View File

@@ -27,9 +27,6 @@ import { MailerModule } from './mailer/mailer.module';
import { PosthogModule } from './posthog/posthog.module';
import { ScheduleModule } from '@nestjs/schedule';
import { HealthModule } from './health/health.module';
import { AccessTokenModule } from './access-token/access-token.module';
import { UserLastActiveOnInterceptor } from './interceptors/user-last-active-on.interceptor';
import { InfraTokenModule } from './infra-token/infra-token.module';
@Module({
imports: [
@@ -105,13 +102,8 @@ import { InfraTokenModule } from './infra-token/infra-token.module';
PosthogModule,
ScheduleModule.forRoot(),
HealthModule,
AccessTokenModule,
InfraTokenModule,
],
providers: [
GQLComplexityPlugin,
{ provide: 'APP_INTERCEPTOR', useClass: UserLastActiveOnInterceptor },
],
providers: [GQLComplexityPlugin],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -7,7 +7,6 @@ import {
Request,
Res,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignInMagicDto } from './dto/signin-magic.dto';
@@ -28,7 +27,6 @@ import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
import { UserLastLoginInterceptor } from 'src/interceptors/user-last-login.interceptor';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })
@@ -112,7 +110,6 @@ export class AuthController {
@Get('google/callback')
@SkipThrottle()
@UseGuards(GoogleSSOGuard)
@UseInterceptors(UserLastLoginInterceptor)
async googleAuthRedirect(@Request() req, @Res() res) {
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
@@ -138,7 +135,6 @@ export class AuthController {
@Get('github/callback')
@SkipThrottle()
@UseGuards(GithubSSOGuard)
@UseInterceptors(UserLastLoginInterceptor)
async githubAuthRedirect(@Request() req, @Res() res) {
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
@@ -164,7 +160,6 @@ export class AuthController {
@Get('microsoft/callback')
@SkipThrottle()
@UseGuards(MicrosoftSSOGuard)
@UseInterceptors(UserLastLoginInterceptor)
async microsoftAuthRedirect(@Request() req, @Res() res) {
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);

View File

@@ -51,8 +51,6 @@ const user: AuthUser = {
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
currentGQLSession: {},
currentRESTSession: {},
@@ -174,11 +172,9 @@ describe('verifyMagicLinkTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
// deletePasswordlessVerificationToken
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
// usersService.updateUserLastLoggedOn
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualRight({
@@ -201,11 +197,9 @@ describe('verifyMagicLinkTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
// deletePasswordlessVerificationToken
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
// usersService.updateUserLastLoggedOn
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
expect(result).toEqualRight({
@@ -245,7 +239,7 @@ describe('verifyMagicLinkTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
E.left(USER_NOT_FOUND),
);
@@ -270,7 +264,7 @@ describe('verifyMagicLinkTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
// deletePasswordlessVerificationToken
mockPrisma.verificationToken.delete.mockRejectedValueOnce('RecordNotFound');
@@ -286,7 +280,7 @@ describe('generateAuthTokens', () => {
test('Should successfully generate tokens with valid inputs', async () => {
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
const result = await authService.generateAuthTokens(user.uid);
expect(result).toEqualRight({
@@ -298,7 +292,7 @@ describe('generateAuthTokens', () => {
test('Should throw USER_NOT_FOUND when updating refresh tokens fails', async () => {
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
E.left(USER_NOT_FOUND),
);
@@ -325,7 +319,7 @@ describe('refreshAuthTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue(user.refreshToken);
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
E.left(USER_NOT_FOUND),
);
@@ -354,7 +348,7 @@ describe('refreshAuthTokens', () => {
// generateAuthTokens
mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb');
// UpdateUserRefreshToken
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
E.right({
...user,
refreshToken: 'sdhjcbjsdhcbshjdcb',

View File

@@ -112,7 +112,7 @@ export class AuthService {
const refreshTokenHash = await argon2.hash(refreshToken);
const updatedUser = await this.usersService.updateUserRefreshToken(
const updatedUser = await this.usersService.UpdateUserRefreshToken(
refreshTokenHash,
userUid,
);
@@ -320,8 +320,6 @@ export class AuthService {
statusCode: HttpStatus.NOT_FOUND,
});
this.usersService.updateUserLastLoggedOn(passwordlessTokens.value.userUid);
return E.right(tokens.right);
}

View File

@@ -52,13 +52,13 @@ export const authCookieHandler = (
res.cookie(AuthTokenType.ACCESS_TOKEN, authTokens.access_token, {
httpOnly: true,
secure: configService.get('ALLOW_SECURE_COOKIES') === 'true',
secure: true,
sameSite: 'lax',
maxAge: accessTokenValidity,
});
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
httpOnly: true,
secure: configService.get('ALLOW_SECURE_COOKIES') === 'true',
secure: true,
sameSite: 'lax',
maxAge: refreshTokenValidity,
});

View File

@@ -1,15 +0,0 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
** Decorator to fetch refresh_token from cookie
*/
export const BearerToken = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest<Request>();
// authorization token will be "Bearer <token>"
const authorization = request.headers['authorization'];
// Remove "Bearer " and return the token only
return authorization.split(' ')[1];
},
);

View File

@@ -678,19 +678,6 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
export const MAILER_FROM_ADDRESS_UNDEFINED =
'mailer/from_address_undefined' as const;
/**
* MAILER_SMTP_USER environment variable is not defined
* (MailerModule)
*/
export const MAILER_SMTP_USER_UNDEFINED = 'mailer/smtp_user_undefined' as const;
/**
* MAILER_SMTP_PASSWORD environment variable is not defined
* (MailerModule)
*/
export const MAILER_SMTP_PASSWORD_UNDEFINED =
'mailer/smtp_password_undefined' as const;
/**
* SharedRequest invalid request JSON format
* (ShortcodeService)
@@ -774,82 +761,3 @@ export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
* Inputs supplied are invalid
*/
export const INVALID_PARAMS = 'invalid_parameters' as const;
/**
* The provided label for the access-token is short (less than 3 characters)
* (AccessTokenService)
*/
export const ACCESS_TOKEN_LABEL_SHORT = 'access_token/label_too_short';
/**
* The provided expiryInDays value is not valid
* (AccessTokenService)
*/
export const ACCESS_TOKEN_EXPIRY_INVALID = 'access_token/expiry_days_invalid';
/**
* The provided PAT ID is invalid
* (AccessTokenService)
*/
export const ACCESS_TOKEN_NOT_FOUND = 'access_token/access_token_not_found';
/**
* AccessTokens is expired
* (AccessTokenService)
*/
export const ACCESS_TOKEN_EXPIRED = 'TOKEN_EXPIRED';
/**
* AccessTokens is invalid
* (AccessTokenService)
*/
export const ACCESS_TOKEN_INVALID = 'TOKEN_INVALID';
/**
* AccessTokens is invalid
* (AccessTokenService)
*/
export const ACCESS_TOKENS_INVALID_DATA_ID = 'INVALID_ID';
/**
* The provided label for the infra-token is short (less than 3 characters)
* (InfraTokenService)
*/
export const INFRA_TOKEN_LABEL_SHORT = 'infra_token/label_too_short';
/**
* The provided expiryInDays value is not valid
* (InfraTokenService)
*/
export const INFRA_TOKEN_EXPIRY_INVALID = 'infra_token/expiry_days_invalid';
/**
* The provided Infra Token ID is invalid
* (InfraTokenService)
*/
export const INFRA_TOKEN_NOT_FOUND = 'infra_token/infra_token_not_found';
/**
* Authorization missing in header (Check 'Authorization' Header)
* (InfraTokenGuard)
*/
export const INFRA_TOKEN_HEADER_MISSING =
'infra_token/authorization_token_missing';
/**
* Infra Token is invalid
* (InfraTokenGuard)
*/
export const INFRA_TOKEN_INVALID_TOKEN = 'infra_token/invalid_token';
/**
* Infra Token is expired
* (InfraTokenGuard)
*/
export const INFRA_TOKEN_EXPIRED = 'infra_token/expired';
/**
* Token creator not found
* (InfraTokenService)
*/
export const INFRA_TOKEN_CREATOR_NOT_FOUND = 'infra_token/creator_not_found';

View File

@@ -28,8 +28,6 @@ import { UserEnvsUserResolver } from './user-environment/user.resolver';
import { UserHistoryUserResolver } from './user-history/user.resolver';
import { UserSettingsUserResolver } from './user-settings/user.resolver';
import { InfraResolver } from './admin/infra.resolver';
import { InfraConfigResolver } from './infra-config/infra-config.resolver';
import { InfraTokenResolver } from './infra-token/infra-token.resolver';
/**
* All the resolvers present in the application.
@@ -60,8 +58,6 @@ const RESOLVERS = [
UserRequestUserCollectionResolver,
UserSettingsResolver,
UserSettingsUserResolver,
InfraConfigResolver,
InfraTokenResolver,
];
/**

View File

@@ -1,47 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { DateTime } from 'luxon';
import {
INFRA_TOKEN_EXPIRED,
INFRA_TOKEN_HEADER_MISSING,
INFRA_TOKEN_INVALID_TOKEN,
} from 'src/errors';
@Injectable()
export class InfraTokenGuard implements CanActivate {
constructor(private readonly prisma: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authorization = request.headers['authorization'];
if (!authorization)
throw new UnauthorizedException(INFRA_TOKEN_HEADER_MISSING);
if (!authorization.startsWith('Bearer '))
throw new UnauthorizedException(INFRA_TOKEN_INVALID_TOKEN);
const token = authorization.split(' ')[1];
if (!token) throw new UnauthorizedException(INFRA_TOKEN_INVALID_TOKEN);
const infraToken = await this.prisma.infraToken.findUnique({
where: { token },
});
if (infraToken === null)
throw new UnauthorizedException(INFRA_TOKEN_INVALID_TOKEN);
const currentTime = DateTime.now().toISO();
if (currentTime > infraToken.expiresOn?.toISOString()) {
throw new UnauthorizedException(INFRA_TOKEN_EXPIRED);
}
return true;
}
}

View File

@@ -1,48 +0,0 @@
import {
BadRequestException,
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
import { AccessTokenService } from 'src/access-token/access-token.service';
import * as E from 'fp-ts/Either';
import { DateTime } from 'luxon';
import { ACCESS_TOKEN_EXPIRED, ACCESS_TOKEN_INVALID } from 'src/errors';
import { createCLIErrorResponse } from 'src/access-token/helper';
@Injectable()
export class PATAuthGuard implements CanActivate {
constructor(private accessTokenService: AccessTokenService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKEN_INVALID),
);
}
const userAccessToken = await this.accessTokenService.getUserPAT(token);
if (E.isLeft(userAccessToken))
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKEN_INVALID),
);
request.user = userAccessToken.right.user;
const accessToken = userAccessToken.right;
if (accessToken.expiresOn === null) return true;
const today = DateTime.now().toISO();
if (accessToken.expiresOn.toISOString() > today) return true;
throw new BadRequestException(
createCLIErrorResponse(ACCESS_TOKEN_EXPIRED),
);
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@@ -33,17 +33,10 @@ const AuthProviderConfigurations = {
InfraConfigEnum.MICROSOFT_SCOPE,
InfraConfigEnum.MICROSOFT_TENANT,
],
[AuthProvider.EMAIL]: !!process.env.MAILER_USE_CUSTOM_CONFIGS
? [
InfraConfigEnum.MAILER_SMTP_HOST,
InfraConfigEnum.MAILER_SMTP_PORT,
InfraConfigEnum.MAILER_SMTP_SECURE,
InfraConfigEnum.MAILER_SMTP_USER,
InfraConfigEnum.MAILER_SMTP_PASSWORD,
InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
InfraConfigEnum.MAILER_ADDRESS_FROM,
]
: [InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.MAILER_ADDRESS_FROM],
[AuthProvider.EMAIL]: [
InfraConfigEnum.MAILER_SMTP_URL,
InfraConfigEnum.MAILER_ADDRESS_FROM,
],
};
/**
@@ -82,14 +75,6 @@ export async function getDefaultInfraConfigs(): Promise<
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_ENABLE,
value: process.env.MAILER_SMTP_ENABLE ?? 'true',
},
{
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
},
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
@@ -98,30 +83,6 @@ export async function getDefaultInfraConfigs(): Promise<
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.MAILER_SMTP_HOST,
value: process.env.MAILER_SMTP_HOST,
},
{
name: InfraConfigEnum.MAILER_SMTP_PORT,
value: process.env.MAILER_SMTP_PORT,
},
{
name: InfraConfigEnum.MAILER_SMTP_SECURE,
value: process.env.MAILER_SMTP_SECURE,
},
{
name: InfraConfigEnum.MAILER_SMTP_USER,
value: process.env.MAILER_SMTP_USER,
},
{
name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
value: process.env.MAILER_SMTP_PASSWORD,
},
{
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,

View File

@@ -2,11 +2,10 @@ import { Module } from '@nestjs/common';
import { InfraConfigService } from './infra-config.service';
import { PrismaModule } from 'src/prisma/prisma.module';
import { SiteController } from './infra-config.controller';
import { InfraConfigResolver } from './infra-config.resolver';
@Module({
imports: [PrismaModule],
providers: [InfraConfigResolver, InfraConfigService],
providers: [InfraConfigService],
exports: [InfraConfigService],
controllers: [SiteController],
})

View File

@@ -1,20 +0,0 @@
import { UseGuards } from '@nestjs/common';
import { Query, Resolver } from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { InfraConfig } from './infra-config.model';
import { InfraConfigService } from './infra-config.service';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => InfraConfig)
export class InfraConfigResolver {
constructor(private infraConfigService: InfraConfigService) {}
@Query(() => Boolean, {
description: 'Check if the SMTP is enabled or not',
})
@UseGuards(GqlAuthGuard)
isSMTPEnabled() {
return this.infraConfigService.isSMTPEnabled();
}
}

View File

@@ -43,7 +43,6 @@ export class InfraConfigService implements OnModuleInit {
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.MAILER_SMTP_ENABLE,
];
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
EXCLUDE_FROM_FETCH_CONFIGS = [
@@ -197,20 +196,7 @@ export class InfraConfigService implements OnModuleInit {
configMap.MICROSOFT_TENANT
);
case AuthProvider.EMAIL:
if (configMap.MAILER_SMTP_ENABLE !== 'true') return false;
if (configMap.MAILER_USE_CUSTOM_CONFIGS === 'true') {
return (
configMap.MAILER_SMTP_HOST &&
configMap.MAILER_SMTP_PORT &&
configMap.MAILER_SMTP_SECURE &&
configMap.MAILER_SMTP_USER &&
configMap.MAILER_SMTP_PASSWORD &&
configMap.MAILER_TLS_REJECT_UNAUTHORIZED &&
configMap.MAILER_ADDRESS_FROM
);
} else {
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
}
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
default:
return false;
}
@@ -232,47 +218,6 @@ export class InfraConfigService implements OnModuleInit {
return E.right(isUpdated.right.value === 'true');
}
/**
* Enable or Disable SMTP
* @param status Status to enable or disable
* @returns Either true or an error
*/
async enableAndDisableSMTP(status: ServiceStatus) {
const isUpdated = await this.toggleServiceStatus(
InfraConfigEnum.MAILER_SMTP_ENABLE,
status,
true,
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
if (status === ServiceStatus.DISABLE) {
this.enableAndDisableSSO([{ provider: AuthProvider.EMAIL, status }]);
}
return E.right(true);
}
/**
* Enable or Disable Service (i.e. ALLOW_AUDIT_LOGS, ALLOW_ANALYTICS_COLLECTION, ALLOW_DOMAIN_WHITELISTING, SITE_PROTECTION)
* @param configName Name of the InfraConfigEnum
* @param status Status to enable or disable
* @param restartEnabled If true, restart the app after updating the InfraConfig
* @returns Either true or an error
*/
async toggleServiceStatus(
configName: InfraConfigEnum,
status: ServiceStatus,
restartEnabled = false,
) {
const isUpdated = await this.update(
configName,
status === ServiceStatus.ENABLE ? 'true' : 'false',
restartEnabled,
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(true);
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
@@ -371,16 +316,6 @@ export class InfraConfigService implements OnModuleInit {
.split(',');
}
/**
* Check if SMTP is enabled or not
* @returns boolean
*/
isSMTPEnabled() {
return (
this.configService.get<string>('INFRA.MAILER_SMTP_ENABLE') === 'true'
);
}
/**
* Reset all the InfraConfigs to their default values (from .env)
*/
@@ -428,20 +363,6 @@ export class InfraConfigService implements OnModuleInit {
) {
for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) {
case InfraConfigEnum.MAILER_SMTP_ENABLE:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
@@ -450,32 +371,6 @@ export class InfraConfigService implements OnModuleInit {
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_HOST:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_PORT:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_SECURE:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_USER:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_PASSWORD:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.GOOGLE_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;

View File

@@ -1,248 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
HttpStatus,
Param,
Patch,
Post,
Query,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { AdminService } from 'src/admin/admin.service';
import { InfraTokenGuard } from 'src/guards/infra-token.guard';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import {
DeleteUserInvitationRequest,
DeleteUserInvitationResponse,
ExceptionResponse,
GetUserInvitationResponse,
GetUsersRequestQuery,
GetUserResponse,
UpdateUserRequest,
UpdateUserAdminStatusRequest,
UpdateUserAdminStatusResponse,
CreateUserInvitationRequest,
CreateUserInvitationResponse,
} from './request-response.dto';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import {
ApiBadRequestResponse,
ApiCreatedResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { throwHTTPErr } from 'src/utils';
import { UserService } from 'src/user/user.service';
import {
INFRA_TOKEN_CREATOR_NOT_FOUND,
USER_NOT_FOUND,
USERS_NOT_FOUND,
} from 'src/errors';
import { InfraTokenService } from './infra-token.service';
import { InfraTokenInterceptor } from 'src/interceptors/infra-token.interceptor';
import { BearerToken } from 'src/decorators/bearer-token.decorator';
@ApiTags('User Management API')
@ApiSecurity('infra-token')
@UseGuards(ThrottlerBehindProxyGuard, InfraTokenGuard)
@UseInterceptors(InfraTokenInterceptor)
@Controller({ path: 'infra', version: '1' })
export class InfraTokensController {
constructor(
private readonly infraTokenService: InfraTokenService,
private readonly adminService: AdminService,
private readonly userService: UserService,
) {}
@Post('user-invitations')
@ApiCreatedResponse({
description: 'Create a user invitation',
type: CreateUserInvitationResponse,
})
@ApiBadRequestResponse({ type: ExceptionResponse })
@ApiNotFoundResponse({ type: ExceptionResponse })
async createUserInvitation(
@BearerToken() token: string,
@Body() dto: CreateUserInvitationRequest,
) {
const createdInvitations =
await this.infraTokenService.createUserInvitation(token, dto);
if (E.isLeft(createdInvitations)) {
const statusCode =
(createdInvitations.left as string) === INFRA_TOKEN_CREATOR_NOT_FOUND
? HttpStatus.NOT_FOUND
: HttpStatus.BAD_REQUEST;
throwHTTPErr({ message: createdInvitations.left, statusCode });
}
return plainToInstance(
CreateUserInvitationResponse,
{ invitationLink: process.env.VITE_BASE_URL },
{
excludeExtraneousValues: true,
enableImplicitConversion: true,
},
);
}
@Get('user-invitations')
@ApiOkResponse({
description: 'Get pending user invitations',
type: [GetUserInvitationResponse],
})
async getPendingUserInvitation(
@Query() paginationQuery: OffsetPaginationArgs,
) {
const pendingInvitedUsers = await this.adminService.fetchInvitedUsers(
paginationQuery,
);
return plainToInstance(GetUserInvitationResponse, pendingInvitedUsers, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
}
@Delete('user-invitations')
@ApiOkResponse({
description: 'Delete a pending user invitation',
type: DeleteUserInvitationResponse,
})
@ApiBadRequestResponse({ type: ExceptionResponse })
async deleteUserInvitation(@Body() dto: DeleteUserInvitationRequest) {
const isDeleted = await this.adminService.revokeUserInvitations(
dto.inviteeEmails,
);
if (E.isLeft(isDeleted)) {
throwHTTPErr({
message: isDeleted.left,
statusCode: HttpStatus.BAD_REQUEST,
});
}
return plainToInstance(
DeleteUserInvitationResponse,
{ message: isDeleted.right },
{
excludeExtraneousValues: true,
enableImplicitConversion: true,
},
);
}
@Get('users')
@ApiOkResponse({
description: 'Get users list',
type: [GetUserResponse],
})
async getUsers(@Query() query: GetUsersRequestQuery) {
const users = await this.userService.fetchAllUsersV2(query.searchString, {
take: query.take,
skip: query.skip,
});
return plainToInstance(GetUserResponse, users, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
}
@Get('users/:uid')
@ApiOkResponse({
description: 'Get user details',
type: GetUserResponse,
})
@ApiNotFoundResponse({ type: ExceptionResponse })
async getUser(@Param('uid') uid: string) {
const user = await this.userService.findUserById(uid);
if (O.isNone(user)) {
throwHTTPErr({
message: USER_NOT_FOUND,
statusCode: HttpStatus.NOT_FOUND,
});
}
return plainToInstance(GetUserResponse, user.value, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
}
@Patch('users/:uid')
@ApiOkResponse({
description: 'Update user display name',
type: GetUserResponse,
})
@ApiBadRequestResponse({ type: ExceptionResponse })
@ApiNotFoundResponse({ type: ExceptionResponse })
async updateUser(@Param('uid') uid: string, @Body() body: UpdateUserRequest) {
const updatedUser = await this.userService.updateUserDisplayName(
uid,
body.displayName,
);
if (E.isLeft(updatedUser)) {
const statusCode =
(updatedUser.left as string) === USER_NOT_FOUND
? HttpStatus.NOT_FOUND
: HttpStatus.BAD_REQUEST;
throwHTTPErr({ message: updatedUser.left, statusCode });
}
return plainToInstance(GetUserResponse, updatedUser.right, {
excludeExtraneousValues: true,
enableImplicitConversion: true,
});
}
@Patch('users/:uid/admin-status')
@ApiOkResponse({
description: 'Update user admin status',
type: UpdateUserAdminStatusResponse,
})
@ApiBadRequestResponse({ type: ExceptionResponse })
@ApiNotFoundResponse({ type: ExceptionResponse })
async updateUserAdminStatus(
@Param('uid') uid: string,
@Body() body: UpdateUserAdminStatusRequest,
) {
let updatedUser;
if (body.isAdmin) {
updatedUser = await this.adminService.makeUsersAdmin([uid]);
} else {
updatedUser = await this.adminService.demoteUsersByAdmin([uid]);
}
if (E.isLeft(updatedUser)) {
const statusCode =
(updatedUser.left as string) === USERS_NOT_FOUND
? HttpStatus.NOT_FOUND
: HttpStatus.BAD_REQUEST;
throwHTTPErr({ message: updatedUser.left as string, statusCode });
}
return plainToInstance(
UpdateUserAdminStatusResponse,
{ message: updatedUser.right },
{
excludeExtraneousValues: true,
enableImplicitConversion: true,
},
);
}
}

View File

@@ -1,43 +0,0 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class InfraToken {
@Field(() => ID, {
description: 'ID of the infra token',
})
id: string;
@Field(() => String, {
description: 'Label of the infra token',
})
label: string;
@Field(() => Date, {
description: 'Date when the infra token was created',
})
createdOn: Date;
@Field(() => Date, {
description: 'Date when the infra token expires',
nullable: true,
})
expiresOn: Date;
@Field(() => Date, {
description: 'Date when the infra token was last used',
})
lastUsedOn: Date;
}
@ObjectType()
export class CreateInfraTokenResponse {
@Field(() => String, {
description: 'The infra token',
})
token: string;
@Field(() => InfraToken, {
description: 'Infra token info',
})
info: InfraToken;
}

View File

@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/prisma/prisma.module';
import { InfraTokenResolver } from './infra-token.resolver';
import { InfraTokenService } from './infra-token.service';
import { InfraTokensController } from './infra-token.controller';
import { AdminModule } from 'src/admin/admin.module';
import { UserModule } from 'src/user/user.module';
@Module({
imports: [PrismaModule, AdminModule, UserModule],
controllers: [InfraTokensController],
providers: [InfraTokenResolver, InfraTokenService],
})
export class InfraTokenModule {}

View File

@@ -1,68 +0,0 @@
import { Args, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateInfraTokenResponse, InfraToken } from './infra-token.model';
import { UseGuards } from '@nestjs/common';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { InfraTokenService } from './infra-token.service';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import { GqlAdmin } from 'src/admin/decorators/gql-admin.decorator';
import { Admin } from 'src/admin/admin.model';
import * as E from 'fp-ts/Either';
import { throwErr } from 'src/utils';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => InfraToken)
export class InfraTokenResolver {
constructor(private readonly infraTokenService: InfraTokenService) {}
/* Query */
@Query(() => [InfraToken], {
description: 'Get list of infra tokens',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
infraTokens(@Args() args: OffsetPaginationArgs) {
return this.infraTokenService.getAll(args.take, args.skip);
}
/* Mutations */
@Mutation(() => CreateInfraTokenResponse, {
description: 'Create a new infra token',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async createInfraToken(
@GqlAdmin() admin: Admin,
@Args({ name: 'label', description: 'Label of the token' }) label: string,
@Args({
name: 'expiryInDays',
description: 'Number of days the token is valid for',
nullable: true,
})
expiryInDays: number,
) {
const infraToken = await this.infraTokenService.create(
label,
expiryInDays,
admin,
);
if (E.isLeft(infraToken)) throwErr(infraToken.left);
return infraToken.right;
}
@Mutation(() => Boolean, {
description: 'Revoke an infra token',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeInfraToken(
@Args({ name: 'id', type: () => ID, description: 'ID of the infra token' })
id: string,
) {
const res = await this.infraTokenService.revoke(id);
if (E.isLeft(res)) throwErr(res.left);
return res.right;
}
}

View File

@@ -1,160 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InfraToken as dbInfraToken } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { CreateInfraTokenResponse, InfraToken } from './infra-token.model';
import { calculateExpirationDate, isValidLength } from 'src/utils';
import { Admin } from 'src/admin/admin.model';
import {
INFRA_TOKEN_CREATOR_NOT_FOUND,
INFRA_TOKEN_EXPIRY_INVALID,
INFRA_TOKEN_LABEL_SHORT,
INFRA_TOKEN_NOT_FOUND,
} from 'src/errors';
import * as E from 'fp-ts/Either';
import { CreateUserInvitationRequest } from './request-response.dto';
import { AdminService } from 'src/admin/admin.service';
@Injectable()
export class InfraTokenService {
constructor(
private readonly prisma: PrismaService,
private readonly adminService: AdminService,
) {}
TITLE_LENGTH = 3;
VALID_TOKEN_DURATIONS = [7, 30, 60, 90];
/**
* Validate the expiration date of the token
*
* @param expiresOn Number of days the token is valid for
* @returns Boolean indicating if the expiration date is valid
*/
private validateExpirationDate(expiresOn: null | number) {
if (expiresOn === null || this.VALID_TOKEN_DURATIONS.includes(expiresOn))
return true;
return false;
}
/**
* Typecast a database InfraToken to a InfraToken model
* @param dbInfraToken database InfraToken
* @returns InfraToken model
*/
private cast(dbInfraToken: dbInfraToken): InfraToken {
return {
id: dbInfraToken.id,
label: dbInfraToken.label,
createdOn: dbInfraToken.createdOn,
expiresOn: dbInfraToken.expiresOn,
lastUsedOn: dbInfraToken.updatedOn,
};
}
/**
* Fetch all infra tokens with pagination
* @param take take for pagination
* @param skip skip for pagination
* @returns List of InfraToken models
*/
async getAll(take = 10, skip = 0) {
const infraTokens = await this.prisma.infraToken.findMany({
take,
skip,
orderBy: { createdOn: 'desc' },
});
return infraTokens.map((token) => this.cast(token));
}
/**
* Create a new infra token
* @param label label of the token
* @param expiryInDays expiry duration of the token
* @param admin admin who created the token
* @returns Either of error message or CreateInfraTokenResponse
*/
async create(label: string, expiryInDays: number, admin: Admin) {
if (!isValidLength(label, this.TITLE_LENGTH)) {
return E.left(INFRA_TOKEN_LABEL_SHORT);
}
if (!this.validateExpirationDate(expiryInDays ?? null)) {
return E.left(INFRA_TOKEN_EXPIRY_INVALID);
}
const createdInfraToken = await this.prisma.infraToken.create({
data: {
creatorUid: admin.uid,
label,
expiresOn: calculateExpirationDate(expiryInDays ?? null) ?? undefined,
},
});
const res: CreateInfraTokenResponse = {
token: createdInfraToken.token,
info: this.cast(createdInfraToken),
};
return E.right(res);
}
/**
* Revoke an infra token
* @param id ID of the infra token
* @returns Either of error or true
*/
async revoke(id: string) {
try {
await this.prisma.infraToken.delete({
where: { id },
});
} catch (error) {
return E.left(INFRA_TOKEN_NOT_FOUND);
}
return E.right(true);
}
/**
* Update the last used on of an infra token
* @param token token to update
* @returns Either of error or InfraToken
*/
async updateLastUsedOn(token: string) {
try {
const infraToken = await this.prisma.infraToken.update({
where: { token },
data: { updatedOn: new Date() },
});
return E.right(this.cast(infraToken));
} catch (error) {
return E.left(INFRA_TOKEN_NOT_FOUND);
}
}
/**
* Create a user invitation using an infra token
* @param token token used to create the invitation
* @param dto CreateUserInvitationRequest
* @returns Either of error or InvitedUser
*/
async createUserInvitation(token: string, dto: CreateUserInvitationRequest) {
const infraToken = await this.prisma.infraToken.findUnique({
where: { token },
});
const tokenCreator = await this.prisma.user.findUnique({
where: { uid: infraToken.creatorUid },
});
if (!tokenCreator) return E.left(INFRA_TOKEN_CREATOR_NOT_FOUND);
const invitedUser = await this.adminService.inviteUserToSignInViaEmail(
tokenCreator.uid,
tokenCreator.email,
dto.inviteeEmail,
);
if (E.isLeft(invitedUser)) return E.left(invitedUser.left);
return E.right(invitedUser);
}
}

View File

@@ -1,115 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose, Transform, Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
MinLength,
} from 'class-validator';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
// POST v1/infra/user-invitations
export class CreateUserInvitationRequest {
@Type(() => String)
@IsNotEmpty()
@ApiProperty()
inviteeEmail: string;
}
export class CreateUserInvitationResponse {
@ApiProperty()
@Expose()
invitationLink: string;
}
// GET v1/infra/user-invitations
export class GetUserInvitationResponse {
@ApiProperty()
@Expose()
inviteeEmail: string;
@ApiProperty()
@Expose()
invitedOn: Date;
}
// DELETE v1/infra/user-invitations
export class DeleteUserInvitationRequest {
@IsArray()
@ArrayMinSize(1)
@Type(() => String)
@IsNotEmpty()
@ApiProperty()
inviteeEmails: string[];
}
export class DeleteUserInvitationResponse {
@ApiProperty()
@Expose()
message: string;
}
// POST v1/infra/users
export class GetUsersRequestQuery extends OffsetPaginationArgs {
@IsOptional()
@IsString()
@MinLength(1)
@ApiPropertyOptional()
searchString: string;
}
export class GetUserResponse {
@ApiProperty()
@Expose()
uid: string;
@ApiProperty()
@Expose()
displayName: string;
@ApiProperty()
@Expose()
email: string;
@ApiProperty()
@Expose()
photoURL: string;
@ApiProperty()
@Expose()
isAdmin: boolean;
}
// PATCH v1/infra/users/:uid
export class UpdateUserRequest {
@IsOptional()
@IsString()
@MinLength(1)
@ApiPropertyOptional()
displayName: string;
}
// PATCH v1/infra/users/:uid/admin-status
export class UpdateUserAdminStatusRequest {
@IsBoolean()
@IsNotEmpty()
@ApiProperty()
isAdmin: boolean;
}
export class UpdateUserAdminStatusResponse {
@ApiProperty()
@Expose()
message: string;
}
// Used for Swagger doc only, in codebase throwHTTPErr function is used to throw errors
export class ExceptionResponse {
@ApiProperty()
@Expose()
message: string;
@ApiProperty()
@Expose()
statusCode: number;
}

View File

@@ -1,36 +0,0 @@
import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { AccessTokenService } from 'src/access-token/access-token.service';
import * as E from 'fp-ts/Either';
import { ACCESS_TOKEN_NOT_FOUND } from 'src/errors';
@Injectable()
export class AccessTokenInterceptor implements NestInterceptor {
constructor(private readonly accessTokenService: AccessTokenService) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
throw new BadRequestException(ACCESS_TOKEN_NOT_FOUND);
}
return handler.handle().pipe(
map(async (data) => {
const userAccessToken =
await this.accessTokenService.updateLastUsedForPAT(token);
if (E.isLeft(userAccessToken))
throw new BadRequestException(userAccessToken.left);
return data;
}),
);
}
}

View File

@@ -1,30 +0,0 @@
import {
BadRequestException,
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { INFRA_TOKEN_NOT_FOUND } from 'src/errors';
import { InfraTokenService } from 'src/infra-token/infra-token.service';
@Injectable()
export class InfraTokenInterceptor implements NestInterceptor {
constructor(private readonly infraTokenService: InfraTokenService) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new BadRequestException(INFRA_TOKEN_NOT_FOUND);
}
const token = authHeader.split(' ')[1];
this.infraTokenService.updateLastUsedOn(token);
return handler.handle();
}
}

View File

@@ -1,65 +0,0 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { AuthUser } from 'src/types/AuthUser';
import { UserService } from 'src/user/user.service';
@Injectable()
export class UserLastActiveOnInterceptor implements NestInterceptor {
constructor(private userService: UserService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (context.getType() === 'http') {
return this.restHandler(context, next);
} else if (context.getType<GqlContextType>() === 'graphql') {
return this.graphqlHandler(context, next);
}
}
restHandler(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const user: AuthUser = request.user;
return next.handle().pipe(
tap(() => {
if (user && typeof user === 'object') {
this.userService.updateUserLastActiveOn(user.uid);
}
}),
catchError((error) => {
if (user && typeof user === 'object') {
this.userService.updateUserLastActiveOn(user.uid);
}
return throwError(() => error);
}),
);
}
graphqlHandler(
context: ExecutionContext,
next: CallHandler,
): Observable<any> {
const contextObject = GqlExecutionContext.create(context).getContext();
const user: AuthUser = contextObject?.req?.user;
return next.handle().pipe(
tap(() => {
if (user && typeof user === 'object') {
this.userService.updateUserLastActiveOn(user.uid);
}
}),
catchError((error) => {
if (user && typeof user === 'object') {
this.userService.updateUserLastActiveOn(user.uid);
}
return throwError(() => error);
}),
);
}
}

View File

@@ -1,25 +0,0 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AuthUser } from 'src/types/AuthUser';
import { UserService } from 'src/user/user.service';
@Injectable()
export class UserLastLoginInterceptor implements NestInterceptor {
constructor(private userService: UserService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const user: AuthUser = context.switchToHttp().getRequest().user;
return next.handle().pipe(
tap(() => {
this.userService.updateUserLastLoggedOn(user.uid);
}),
);
}
}

View File

@@ -1,59 +0,0 @@
import { TransportType } from '@nestjs-modules/mailer/dist/interfaces/mailer-options.interface';
import {
MAILER_SMTP_PASSWORD_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED,
MAILER_SMTP_USER_UNDEFINED,
} from 'src/errors';
import { throwErr } from 'src/utils';
function isSMTPCustomConfigsEnabled(value) {
return value === 'true';
}
export function getMailerAddressFrom(env, config): string {
return (
env.INFRA.MAILER_ADDRESS_FROM ??
config.get('MAILER_ADDRESS_FROM') ??
throwErr(MAILER_SMTP_URL_UNDEFINED)
);
}
export function getTransportOption(env, config): TransportType {
const useCustomConfigs = isSMTPCustomConfigsEnabled(
env.INFRA.MAILER_USE_CUSTOM_CONFIGS ??
config.get('MAILER_USE_CUSTOM_CONFIGS'),
);
if (!useCustomConfigs) {
console.log('Using simple mailer configuration');
return (
env.INFRA.MAILER_SMTP_URL ??
config.get('MAILER_SMTP_URL') ??
throwErr(MAILER_SMTP_URL_UNDEFINED)
);
} else {
console.log('Using advanced mailer configuration');
return {
host: env.INFRA.MAILER_SMTP_HOST ?? config.get('MAILER_SMTP_HOST'),
port: +env.INFRA.MAILER_SMTP_PORT ?? +config.get('MAILER_SMTP_PORT'),
secure:
(env.INFRA.MAILER_SMTP_SECURE ?? config.get('MAILER_SMTP_SECURE')) ===
'true',
auth: {
user:
env.INFRA.MAILER_SMTP_USER ??
config.get('MAILER_SMTP_USER') ??
throwErr(MAILER_SMTP_USER_UNDEFINED),
pass:
env.INFRA.MAILER_SMTP_PASSWORD ??
config.get('MAILER_SMTP_PASSWORD') ??
throwErr(MAILER_SMTP_PASSWORD_UNDEFINED),
},
tls: {
rejectUnauthorized:
(env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED ??
config.get('MAILER_TLS_REJECT_UNAUTHORIZED')) === 'true',
},
};
}
}

View File

@@ -2,9 +2,13 @@ import { Global, Module } from '@nestjs/common';
import { MailerModule as NestMailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { MailerService } from './mailer.service';
import { throwErr } from 'src/utils';
import {
MAILER_FROM_ADDRESS_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED,
} from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
import { getMailerAddressFrom, getTransportOption } from './helper';
@Global()
@Module({
@@ -14,31 +18,24 @@ import { getMailerAddressFrom, getTransportOption } from './helper';
})
export class MailerModule {
static async register() {
const config = new ConfigService();
const env = await loadInfraConfiguration();
// If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.)
if (env.INFRA.MAILER_SMTP_ENABLE !== 'true') {
console.log('Mailer module is disabled');
return {
module: MailerModule,
};
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
const config = new ConfigService();
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
}
// If mailer is ENABLED, return the module with configuration (service, etc.)
// Determine transport configuration based on custom config flag
let transportOption = getTransportOption(env, config);
// Get mailer address from environment or config
const mailerAddressFrom = getMailerAddressFrom(env, config);
return {
module: MailerModule,
imports: [
NestMailerModule.forRoot({
transport: transportOption,
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from: mailerAddressFrom,
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',

View File

@@ -1,4 +1,4 @@
import { Injectable, Optional } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
AdminUserInvitationMailDescription,
MailDescription,
@@ -7,14 +7,10 @@ import {
import { throwErr } from 'src/utils';
import { EMAIL_FAILED } from 'src/errors';
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MailerService {
constructor(
@Optional() private readonly nestMailerService: NestMailerService,
private readonly configService: ConfigService,
) {}
constructor(private readonly nestMailerService: NestMailerService) {}
/**
* Takes an input mail description and spits out the Email subject required for it
@@ -46,8 +42,6 @@ export class MailerService {
to: string,
mailDesc: MailDescription | UserMagicLinkMailDescription,
) {
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
try {
await this.nestMailerService.sendMail({
to,
@@ -56,7 +50,6 @@ export class MailerService {
context: mailDesc.variables,
});
} catch (error) {
console.log('Error from sendEmail:', error);
return throwErr(EMAIL_FAILED);
}
}
@@ -71,8 +64,6 @@ export class MailerService {
to: string,
mailDesc: AdminUserInvitationMailDescription,
) {
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
try {
const res = await this.nestMailerService.sendMail({
to,
@@ -82,7 +73,6 @@ export class MailerService {
});
return res;
} catch (error) {
console.log('Error from sendUserInvitationEmail:', error);
return throwErr(EMAIL_FAILED);
}
}

View File

@@ -2,40 +2,11 @@ import { NestFactory } from '@nestjs/core';
import { json } from 'express';
import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { VersioningType } from '@nestjs/common';
import * as session from 'express-session';
import { emitGQLSchemaFile } from './gql-schema';
import { checkEnvironmentAuthProvider } from './utils';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { InfraTokensController } from './infra-token/infra-token.controller';
import { InfraTokenModule } from './infra-token/infra-token.module';
function setupSwagger(app) {
const swaggerDocPath = '/api-docs';
const config = new DocumentBuilder()
.setTitle('Hoppscotch API Documentation')
.setDescription('APIs for external integration')
.addApiKey(
{
type: 'apiKey',
name: 'Authorization',
in: 'header',
scheme: 'bearer',
bearerFormat: 'Bearer',
},
'infra-token',
)
.build();
const document = SwaggerModule.createDocument(app, config, {
include: [InfraTokenModule],
});
SwaggerModule.setup(swaggerDocPath, app, document, {
swaggerOptions: { persistAuthorization: true, ignoreGlobalPrefix: true },
});
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
@@ -82,14 +53,6 @@ async function bootstrap() {
type: VersioningType.URI,
});
app.use(cookieParser());
app.useGlobalPipes(
new ValidationPipe({
transform: true,
}),
);
await setupSwagger(app);
await app.listen(configService.get('PORT') || 3170);
// Graceful shutdown

View File

@@ -48,8 +48,6 @@ const user: AuthUser = {
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: createdOn,
lastActiveOn: createdOn,
createdOn: createdOn,
currentGQLSession: {},
currentRESTSession: {},

View File

@@ -299,10 +299,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
where: userEmail
? {
User: {
email: {
equals: userEmail,
mode: 'insensitive',
},
email: userEmail,
},
}
: undefined,

View File

@@ -1,5 +1,3 @@
import { TeamRequest } from '@prisma/client';
// Type of data returned from the query to obtain all search results
export type SearchQueryReturnType = {
id: string;
@@ -14,12 +12,3 @@ export type ParentTreeQueryReturnType = {
parentID: string;
title: string;
};
// Type of data returned from the query to fetch collection details from CLI
export type GetCollectionResponse = {
id: string;
data: string | null;
title: string;
parentID: string | null;
folders: GetCollectionResponse[];
requests: TeamRequest[];
};

View File

@@ -331,26 +331,6 @@ export class TeamCollectionResolver {
return updatedTeamCollection.right;
}
@Mutation(() => Boolean, {
description: 'Duplicate a Team Collection',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async duplicateTeamCollection(
@Args({
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string,
) {
const duplicatedTeamCollection =
await this.teamCollectionService.duplicateTeamCollection(collectionID);
if (E.isLeft(duplicatedTeamCollection))
throwErr(duplicatedTeamCollection.left);
return duplicatedTeamCollection.right;
}
// Subscriptions
@Subscription(() => TeamCollection, {

View File

@@ -12,7 +12,6 @@ import {
TEAM_COL_REORDERING_FAILED,
TEAM_COL_SAME_NEXT_COLL,
TEAM_INVALID_COLL_ID,
TEAM_MEMBER_NOT_FOUND,
TEAM_NOT_OWNER,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -20,18 +19,15 @@ import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
import { TeamService } from 'src/team/team.service';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
const mockTeamService = mockDeep<TeamService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const teamCollectionService = new TeamCollectionService(
mockPrisma,
mockPubSub as any,
mockTeamService,
);
const currentTime = new Date();
@@ -43,8 +39,6 @@ const user: AuthUser = {
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
currentGQLSession: {},
currentRESTSession: {},
@@ -1744,63 +1738,3 @@ describe('updateTeamCollection', () => {
});
//ToDo: write test cases for exportCollectionsToJSON
describe('getCollectionForCLI', () => {
test('should throw TEAM_COLL_NOT_FOUND if collectionID is invalid', async () => {
mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
const result = await teamCollectionService.getCollectionForCLI(
'invalidID',
user.uid,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should throw TEAM_MEMBER_NOT_FOUND if user not in same team', async () => {
mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
mockTeamService.getTeamMember.mockResolvedValue(null);
const result = await teamCollectionService.getCollectionForCLI(
rootTeamCollection.id,
user.uid,
);
expect(result).toEqualLeft(TEAM_MEMBER_NOT_FOUND);
});
// test('should return the TeamCollection data for CLI', async () => {
// mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
// rootTeamCollection,
// );
// mockTeamService.getTeamMember.mockResolvedValue({
// membershipID: 'sdc3sfdv',
// userUid: user.uid,
// role: TeamMemberRole.OWNER,
// });
// const result = await teamCollectionService.getCollectionForCLI(
// rootTeamCollection.id,
// user.uid,
// );
// expect(result).toEqualRight({
// id: rootTeamCollection.id,
// data: JSON.stringify(rootTeamCollection.data),
// title: rootTeamCollection.title,
// parentID: rootTeamCollection.parentID,
// folders: [
// {
// id: childTeamCollection.id,
// data: JSON.stringify(childTeamCollection.data),
// title: childTeamCollection.title,
// parentID: childTeamCollection.parentID,
// folders: [],
// requests: [],
// },
// ],
// requests: [],
// });
// });
});

View File

@@ -18,38 +18,23 @@ import {
TEAM_COL_SEARCH_FAILED,
TEAM_REQ_PARENT_TREE_GEN_FAILED,
TEAM_COLL_PARENT_TREE_GEN_FAILED,
TEAM_MEMBER_NOT_FOUND,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import {
escapeSqlLikeString,
isValidLength,
transformCollectionData,
} from 'src/utils';
import { escapeSqlLikeString, isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import {
Prisma,
TeamCollection as DBTeamCollection,
TeamRequest,
} from '@prisma/client';
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
import { CollectionFolder } from 'src/types/CollectionFolder';
import { stringToJson } from 'src/utils';
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
import {
GetCollectionResponse,
ParentTreeQueryReturnType,
SearchQueryReturnType,
} from './helper';
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
import { RESTError } from 'src/types/RESTError';
import { TeamService } from 'src/team/team.service';
@Injectable()
export class TeamCollectionService {
constructor(
private readonly prisma: PrismaService,
private readonly pubsub: PubSubService,
private readonly teamService: TeamService,
) {}
TITLE_LENGTH = 3;
@@ -138,13 +123,11 @@ export class TeamCollectionService {
},
});
const data = transformCollectionData(collection.right.data);
const result: CollectionFolder = {
name: collection.right.title,
folders: childrenCollectionObjects,
requests: requests.map((x) => x.request),
data,
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -315,13 +298,11 @@ export class TeamCollectionService {
* @returns TeamCollection model
*/
private cast(teamCollection: DBTeamCollection): TeamCollection {
const data = transformCollectionData(teamCollection.data);
return <TeamCollection>{
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
}
@@ -1363,126 +1344,4 @@ export class TeamCollectionService {
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
}
}
/**
* Get all requests in a collection
*
* @param collectionID The Collection ID
* @returns A list of all requests in the collection
*/
private async getAllRequestsInCollection(collectionID: string) {
const dbTeamRequests = await this.prisma.teamRequest.findMany({
where: {
collectionID: collectionID,
},
orderBy: {
orderIndex: 'asc',
},
});
const teamRequests = dbTeamRequests.map((tr) => {
return <TeamRequest>{
id: tr.id,
collectionID: tr.collectionID,
teamID: tr.teamID,
title: tr.title,
request: JSON.stringify(tr.request),
};
});
return teamRequests;
}
/**
* Get Collection Tree for CLI
*
* @param parentID The parent Collection ID
* @returns Collection tree for CLI
*/
private async getCollectionTreeForCLI(parentID: string | null) {
const childCollections = await this.prisma.teamCollection.findMany({
where: { parentID },
orderBy: { orderIndex: 'asc' },
});
const response: GetCollectionResponse[] = [];
for (const collection of childCollections) {
const folder: GetCollectionResponse = {
id: collection.id,
data: collection.data === null ? null : JSON.stringify(collection.data),
title: collection.title,
parentID: collection.parentID,
folders: await this.getCollectionTreeForCLI(collection.id),
requests: await this.getAllRequestsInCollection(collection.id),
};
response.push(folder);
}
return response;
}
/**
* Get Collection for CLI
*
* @param collectionID The Collection ID
* @param userUid The User UID
* @returns An Either of the Collection details
*/
async getCollectionForCLI(collectionID: string, userUid: string) {
try {
const collection = await this.prisma.teamCollection.findUniqueOrThrow({
where: { id: collectionID },
});
const teamMember = await this.teamService.getTeamMember(
collection.teamID,
userUid,
);
if (!teamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
return E.right(<GetCollectionResponse>{
id: collection.id,
data: collection.data === null ? null : JSON.stringify(collection.data),
title: collection.title,
parentID: collection.parentID,
folders: await this.getCollectionTreeForCLI(collection.id),
requests: await this.getAllRequestsInCollection(collection.id),
});
} catch (error) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
/**
* Duplicate a Team Collection
*
* @param collectionID The Collection ID
* @returns Boolean of duplication status
*/
async duplicateTeamCollection(collectionID: string) {
const collection = await this.getCollection(collectionID);
if (E.isLeft(collection)) return E.left(TEAM_INVALID_COLL_ID);
const collectionJSONObject = await this.exportCollectionToJSONObject(
collection.right.teamID,
collectionID,
);
if (E.isLeft(collectionJSONObject)) return E.left(TEAM_INVALID_COLL_ID);
const result = await this.importCollectionsFromJSON(
JSON.stringify([
{
...collectionJSONObject.right,
name: `${collection.right.title} - Duplicate`,
},
]),
collection.right.teamID,
collection.right.parentID,
);
if (E.isLeft(result)) return E.left(result.left as string);
return E.right(true);
}
}

View File

@@ -6,24 +6,19 @@ import {
JSON_INVALID,
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors';
import { TeamService } from 'src/team/team.service';
import { TeamMemberRole } from 'src/team/team.model';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = {
publish: jest.fn().mockResolvedValue(null),
};
const mockTeamService = mockDeep<TeamService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const teamEnvironmentsService = new TeamEnvironmentsService(
mockPrisma,
mockPubSub as any,
mockTeamService,
);
const teamEnvironment = {
@@ -385,47 +380,4 @@ describe('TeamEnvironmentsService', () => {
expect(result).toEqual(0);
});
});
describe('getTeamEnvironmentForCLI', () => {
test('should successfully return a TeamEnvironment with valid ID', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
teamEnvironment,
);
mockTeamService.getTeamMember.mockResolvedValue({
membershipID: 'sdc3sfdv',
userUid: '123454',
role: TeamMemberRole.OWNER,
});
const result = await teamEnvironmentsService.getTeamEnvironmentForCLI(
teamEnvironment.id,
'123454',
);
expect(result).toEqualRight(teamEnvironment);
});
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
'RejectOnNotFound',
);
const result = await teamEnvironmentsService.getTeamEnvironment(
teamEnvironment.id,
);
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
});
test('should throw TEAM_MEMBER_NOT_FOUND if user not in same team', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
teamEnvironment,
);
mockTeamService.getTeamMember.mockResolvedValue(null);
const result = await teamEnvironmentsService.getTeamEnvironmentForCLI(
teamEnvironment.id,
'333',
);
expect(result).toEqualLeft(TEAM_MEMBER_NOT_FOUND);
});
});
});

View File

@@ -6,17 +6,14 @@ import { TeamEnvironment } from './team-environments.model';
import {
TEAM_ENVIRONMENT_NOT_FOUND,
TEAM_ENVIRONMENT_SHORT_NAME,
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors';
import * as E from 'fp-ts/Either';
import { isValidLength } from 'src/utils';
import { TeamService } from 'src/team/team.service';
@Injectable()
export class TeamEnvironmentsService {
constructor(
private readonly prisma: PrismaService,
private readonly pubsub: PubSubService,
private readonly teamService: TeamService,
) {}
TITLE_LENGTH = 3;
@@ -245,30 +242,4 @@ export class TeamEnvironmentsService {
});
return envCount;
}
/**
* Get details of a TeamEnvironment for CLI.
*
* @param id TeamEnvironment ID
* @param userUid User UID
* @returns Either of a TeamEnvironment or error message
*/
async getTeamEnvironmentForCLI(id: string, userUid: string) {
try {
const teamEnvironment =
await this.prisma.teamEnvironment.findFirstOrThrow({
where: { id },
});
const teamMember = await this.teamService.getTeamMember(
teamEnvironment.teamID,
userUid,
);
if (!teamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
return E.right(teamEnvironment);
} catch (error) {
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
}
}
}

View File

@@ -75,13 +75,12 @@ export class TeamInvitationService {
if (!isEmailValid) return E.left(INVALID_EMAIL);
try {
const teamInvite = await this.prisma.teamInvitation.findFirstOrThrow({
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
where: {
inviteeEmail: {
equals: inviteeEmail,
mode: 'insensitive',
teamID_inviteeEmail: {
inviteeEmail: inviteeEmail,
teamID: teamID,
},
teamID,
},
});

View File

@@ -1,7 +0,0 @@
export type AccessToken = {
id: string;
label: string;
createdOn: Date;
lastUsedOn: Date;
expiresOn: null | Date;
};

View File

@@ -1,16 +1,7 @@
export enum InfraConfigEnum {
MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE',
MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS',
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
MAILER_SMTP_HOST = 'MAILER_SMTP_HOST',
MAILER_SMTP_PORT = 'MAILER_SMTP_PORT',
MAILER_SMTP_SECURE = 'MAILER_SMTP_SECURE',
MAILER_SMTP_USER = 'MAILER_SMTP_USER',
MAILER_SMTP_PASSWORD = 'MAILER_SMTP_PASSWORD',
MAILER_TLS_REJECT_UNAUTHORIZED = 'MAILER_TLS_REJECT_UNAUTHORIZED',
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',

View File

@@ -5,6 +5,6 @@ import { HttpStatus } from '@nestjs/common';
** Since its REST we need to return the HTTP status code along with the error message
*/
export type RESTError = {
message: string | Record<string, string>;
message: string;
statusCode: HttpStatus;
};

View File

@@ -1,7 +1,4 @@
import { ArgsType, Field, ID, InputType } from '@nestjs/graphql';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsOptional } from 'class-validator';
@ArgsType()
@InputType()
@@ -24,10 +21,6 @@ export class PaginationArgs {
@ArgsType()
@InputType()
export class OffsetPaginationArgs {
@IsOptional()
@IsNotEmpty()
@Type(() => Number)
@ApiPropertyOptional()
@Field({
nullable: true,
defaultValue: 0,
@@ -35,10 +28,6 @@ export class OffsetPaginationArgs {
})
skip: number;
@IsOptional()
@IsNotEmpty()
@Type(() => Number)
@ApiPropertyOptional()
@Field({
nullable: true,
defaultValue: 10,

View File

@@ -390,36 +390,6 @@ export class UserCollectionResolver {
return updatedUserCollection.right;
}
@Mutation(() => Boolean, {
description: 'Duplicate a User Collection',
})
@UseGuards(GqlAuthGuard)
async duplicateUserCollection(
@GqlUser() user: AuthUser,
@Args({
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string,
@Args({
name: 'reqType',
description: 'Type of UserCollection',
type: () => ReqType,
})
reqType: ReqType,
) {
const duplicatedUserCollection =
await this.userCollectionService.duplicateUserCollection(
collectionID,
user.uid,
reqType,
);
if (E.isLeft(duplicatedUserCollection))
throwErr(duplicatedUserCollection.left);
return duplicatedUserCollection.right;
}
// Subscriptions
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',

View File

@@ -38,8 +38,6 @@ const user: AuthUser = {
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
currentGQLSession: {},
currentRESTSession: {},

View File

@@ -25,11 +25,7 @@ import {
UserCollectionExportJSONData,
} from './user-collections.model';
import { ReqType } from 'src/types/RequestTypes';
import {
isValidLength,
stringToJson,
transformCollectionData,
} from 'src/utils';
import { isValidLength, stringToJson } from 'src/utils';
import { CollectionFolder } from 'src/types/CollectionFolder';
@Injectable()
@@ -47,15 +43,13 @@ export class UserCollectionService {
* @returns UserCollection model
*/
private cast(collection: UserCollection) {
const data = transformCollectionData(collection.data);
return <UserCollectionModel>{
id: collection.id,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
userID: collection.userUid,
data,
data: !collection.data ? null : JSON.stringify(collection.data),
};
}
@@ -877,8 +871,6 @@ export class UserCollectionService {
},
});
const data = transformCollectionData(collection.right.data);
const result: CollectionFolder = {
id: collection.right.id,
name: collection.right.title,
@@ -890,7 +882,7 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
data,
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -1146,45 +1138,4 @@ export class UserCollectionService {
return E.left(USER_COLL_NOT_FOUND);
}
}
/**
* Duplicate a User Collection
*
* @param collectionID The Collection ID
* @returns Boolean of duplication status
*/
async duplicateUserCollection(
collectionID: string,
userID: string,
reqType: DBReqType,
) {
const collection = await this.getUserCollection(collectionID);
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
if (collection.right.type !== reqType)
return E.left(USER_COLL_NOT_SAME_TYPE);
const collectionJSONObject = await this.exportUserCollectionToJSONObject(
collection.right.userUid,
collectionID,
);
if (E.isLeft(collectionJSONObject))
return E.left(collectionJSONObject.left);
const result = await this.importCollectionsFromJSON(
JSON.stringify([
{
...collectionJSONObject.right,
name: `${collection.right.title} - Duplicate`,
},
]),
userID,
collection.right.parentID,
reqType,
);
if (E.isLeft(result)) return E.left(result.left as string);
return E.right(true);
}
}

View File

@@ -41,8 +41,6 @@ const user: AuthUser = {
photoURL: 'https://example.com/photo.png',
isAdmin: false,
refreshToken: null,
lastLoggedOn: new Date(),
lastActiveOn: new Date(),
createdOn: new Date(),
currentGQLSession: null,
currentRESTSession: null,

View File

@@ -27,8 +27,6 @@ const user: AuthUser = {
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
currentGQLSession: {},
currentRESTSession: {},
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
};

View File

@@ -30,18 +30,6 @@ export class User {
})
isAdmin: boolean;
@Field({
nullable: true,
description: 'Date when the user last logged in',
})
lastLoggedOn: Date;
@Field({
nullable: true,
description: 'Date when the user last interacted with the app',
})
lastActiveOn: Date;
@Field({
description: 'Date when the user account was created',
})

View File

@@ -42,8 +42,6 @@ const user: AuthUser = {
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
};
@@ -56,8 +54,6 @@ const adminUser: AuthUser = {
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
};
@@ -71,8 +67,6 @@ const users: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
{
@@ -84,8 +78,6 @@ const users: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
{
@@ -97,8 +89,6 @@ const users: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
];
@@ -113,8 +103,6 @@ const adminUsers: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
{
@@ -126,8 +114,6 @@ const adminUsers: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
{
@@ -139,8 +125,6 @@ const adminUsers: AuthUser[] = [
currentRESTSession: {},
currentGQLSession: {},
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
lastLoggedOn: currentTime,
lastActiveOn: currentTime,
createdOn: currentTime,
},
];
@@ -165,7 +149,7 @@ beforeEach(() => {
describe('UserService', () => {
describe('findUserByEmail', () => {
test('should successfully return a valid user given a valid email', async () => {
mockPrisma.user.findFirst.mockResolvedValueOnce(user);
mockPrisma.user.findUniqueOrThrow.mockResolvedValueOnce(user);
const result = await userService.findUserByEmail(
'dwight@dundermifflin.com',
@@ -174,7 +158,7 @@ describe('UserService', () => {
});
test('should return a null user given a invalid email', async () => {
mockPrisma.user.findFirst.mockResolvedValueOnce(null);
mockPrisma.user.findUniqueOrThrow.mockRejectedValueOnce('NotFoundError');
const result = await userService.findUserByEmail('jim@dundermifflin.com');
expect(result).resolves.toBeNone;
@@ -511,26 +495,6 @@ describe('UserService', () => {
});
});
describe('updateUserLastLoggedOn', () => {
test('should resolve right and update user last logged on', async () => {
const currentTime = new Date();
mockPrisma.user.update.mockResolvedValueOnce({
...user,
lastLoggedOn: currentTime,
});
const result = await userService.updateUserLastLoggedOn(user.uid);
expect(result).toEqualRight(true);
});
test('should resolve left and error when invalid user uid is passed', async () => {
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
const result = await userService.updateUserLastLoggedOn('invalidUserUid');
expect(result).toEqualLeft(USER_NOT_FOUND);
});
});
describe('fetchAllUsers', () => {
test('should resolve right and return 20 users when cursor is null', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(users);

View File

@@ -62,16 +62,16 @@ export class UserService {
* @returns Option of found User
*/
async findUserByEmail(email: string): Promise<O.None | O.Some<AuthUser>> {
const user = await this.prisma.user.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
try {
const user = await this.prisma.user.findUniqueOrThrow({
where: {
email: email,
},
},
});
if (!user) return O.none;
return O.some(user);
});
return O.some(user);
} catch (error) {
return O.none;
}
}
/**
@@ -114,7 +114,7 @@ export class UserService {
* @param userUid User uid
* @returns Either of User with updated refreshToken
*/
async updateUserRefreshToken(refreshTokenHash: string, userUid: string) {
async UpdateUserRefreshToken(refreshTokenHash: string, userUid: string) {
try {
const user = await this.prisma.user.update({
where: {
@@ -174,7 +174,6 @@ export class UserService {
displayName: userDisplayName,
email: profile.emails[0].value,
photoURL: userPhotoURL,
lastLoggedOn: new Date(),
providerAccounts: {
create: {
provider: profile.provider,
@@ -222,7 +221,7 @@ export class UserService {
}
/**
* Update User displayName and photoURL when logged in via a SSO provider
* Update User displayName and photoURL
*
* @param user User object
* @param profile Data received from SSO provider on the users account
@@ -237,7 +236,6 @@ export class UserService {
data: {
displayName: !profile.displayName ? null : profile.displayName,
photoURL: !profile.photos ? null : profile.photos[0].value,
lastLoggedOn: new Date(),
},
});
return E.right(updatedUser);
@@ -291,7 +289,7 @@ export class UserService {
}
/**
* Update a user's displayName
* Update a user's data
* @param userUID User UID
* @param displayName User's displayName
* @returns a Either of User or error
@@ -318,38 +316,6 @@ export class UserService {
}
}
/**
* Update user's lastLoggedOn timestamp
* @param userUID User UID
*/
async updateUserLastLoggedOn(userUid: string) {
try {
await this.prisma.user.update({
where: { uid: userUid },
data: { lastLoggedOn: new Date() },
});
return E.right(true);
} catch (e) {
return E.left(USER_NOT_FOUND);
}
}
/**
* Update user's lastActiveOn timestamp
* @param userUID User UID
*/
async updateUserLastActiveOn(userUid: string) {
try {
await this.prisma.user.update({
where: { uid: userUid },
data: { lastActiveOn: new Date() },
});
return E.right(true);
} catch (e) {
return E.left(USER_NOT_FOUND);
}
}
/**
* Validate and parse currentRESTSession and currentGQLSession
* @param sessionData string of the session

View File

@@ -1,21 +1,21 @@
import { ExecutionContext, HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Prisma } from '@prisma/client';
import * as A from 'fp-ts/Array';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/Option';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';
import { AuthProvider } from './auth/helper';
import * as T from 'fp-ts/Task';
import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array';
import { TeamMemberRole } from './team/team.model';
import { User } from './user/user.model';
import {
ENV_EMPTY_AUTH_PROVIDERS,
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
JSON_INVALID,
} from './errors';
import { TeamMemberRole } from './team/team.model';
import { AuthProvider } from './auth/helper';
import { RESTError } from './types/RESTError';
/**
@@ -286,33 +286,3 @@ export function escapeSqlLikeString(str: string) {
}
});
}
/**
* Calculate the expiration date of the token
*
* @param expiresOn Number of days the token is valid for
* @returns Date object of the expiration date
*/
export function calculateExpirationDate(expiresOn: null | number) {
if (expiresOn === null) return null;
return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
}
/*
* Transforms the collection level properties (authorization & headers) under the `data` field.
* Preserves `null` values and prevents duplicate stringification.
*
* @param {Prisma.JsonValue} collectionData - The team collection data to transform.
* @returns {string | null} The transformed team collection data as a string.
*/
export function transformCollectionData(
collectionData: Prisma.JsonValue,
): string | null {
if (!collectionData) {
return null;
}
return typeof collectionData === 'string'
? collectionData
: JSON.stringify(collectionData);
}

View File

@@ -0,0 +1,193 @@
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls, instances and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {
// 'ts-jest': {
// useESM: true,
// },
// },
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
moduleFileExtensions: ["js", "ts", "json"],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {
// '^(\\.{1,2}/.*)\\.js$': '$1',
// },
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: "ts-jest/presets/js-with-babel",
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ["./jest.setup.ts"],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/commands/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: {
"^.+\\.ts$": "ts-jest",
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
verbose: true,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@@ -0,0 +1 @@
import "@relmify/jest-fp-ts";

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.10.1",
"version": "0.8.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"type": "module",
@@ -20,9 +20,9 @@
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && vitest run",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"do-test": "pnpm run test"
"do-test": "pnpm test"
},
"keywords": [
"cli",
@@ -48,7 +48,6 @@
"lodash-es": "4.17.21",
"qs": "6.11.2",
"verzod": "0.2.2",
"xmlbuilder2": "3.1.1",
"zod": "3.22.4"
},
"devDependencies": {
@@ -56,13 +55,15 @@
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "2.1.1",
"@swc/core": "1.4.2",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/qs": "6.9.12",
"fp-ts": "2.16.2",
"jest": "29.7.0",
"prettier": "3.2.5",
"qs": "6.11.2",
"ts-jest": "29.1.2",
"tsup": "8.0.2",
"typescript": "5.3.3",
"vitest": "0.34.6"
"typescript": "5.3.3"
}
}

View File

@@ -1,15 +0,0 @@
// Vitest doesn't work without globals
// Ref: https://github.com/relmify/jest-fp-ts/issues/11
import decodeMatchers from "@relmify/jest-fp-ts/dist/decodeMatchers";
import eitherMatchers from "@relmify/jest-fp-ts/dist/eitherMatchers";
import optionMatchers from "@relmify/jest-fp-ts/dist/optionMatchers";
import theseMatchers from "@relmify/jest-fp-ts/dist/theseMatchers";
import eitherOrTheseMatchers from "@relmify/jest-fp-ts/dist/eitherOrTheseMatchers";
import { expect } from "vitest";
expect.extend(decodeMatchers.matchers);
expect.extend(eitherMatchers.matchers);
expect.extend(optionMatchers.matchers);
expect.extend(theseMatchers.matchers);
expect.extend(eitherOrTheseMatchers.matchers);

View File

@@ -0,0 +1,345 @@
import { ExecException } from "child_process";
import { HoppErrorCode } from "../../types/errors";
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
describe("Test `hopp test <file>` command:", () => {
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
const args = "test";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
const args = "invalid-arg";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
});
describe("Supplied collection export file validations", () => {
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
const args = "test notfound.json";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{
code: 1,
});
});
});
describe("Versioned entities", () => {
describe("Collections & Requests", () => {
const testFixtures = [
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
];
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Environments", () => {
const testFixtures = [
{ fileName: "env-v0.json", version: 0 },
{ fileName: "env-v1.json", version: 1 },
];
testFixtures.forEach(({ fileName, version }) => {
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
});
test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully inherits headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath(
"collection-level-headers-auth-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test `hopp test <file> --env <file>` command:", () => {
describe("Supplied environment export file validations", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt",
"collection"
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath(
"malformed-envs.json",
"environment"
);
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
});
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
});
});
test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with shorth `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
describe("Secret environment variables", () => {
jest.setTimeout(100000);
// Reads secret environment values from system environment
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
const env = {
...process.env,
secretBearerToken: "test-token",
secretBasicAuthUsername: "test-user",
secretBasicAuthPassword: "test-pass",
secretQueryParamValue: "secret-query-param-value",
secretBodyValue: "secret-body-value",
secretHeaderValue: "secret-header-value",
};
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args, { env });
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Successfully performs delayed request execution for a valid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with the short `-d` flag", async () => {
const args = `${VALID_TEST_ARGS} -d 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});

View File

@@ -1,529 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the default path 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<testsuites tests=\\"76\\" failures=\\"2\\" errors=\\"66\\" time=\\"time\\">
<testsuite name=\\"test-junit-report-export/request-level-errors/invalid-url\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL]]></system-err>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/request-level-errors/test-script-reference-error\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"0\\" failures=\\"0\\" errors=\\"0\\">
<system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: status is not defined]]></system-err>
</testsuite>
<testsuite name=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<system-err><![CDATA[
PARSING_ERROR - {
\\"key\\": \\"<<key>>\\"
} (ENV_EXPAND_LOOP)]]></system-err>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/error\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/success\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"5\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Status code is 200 - Expected '200' to be '200'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'undefined' to be 'undefined'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Status code is 2xx - Expected '200' to be 200-level status\\" classname=\\"test-junit-report-export/assertions/success\\"/>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/failure\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"5\\" failures=\\"2\\" errors=\\"0\\">
<testcase name=\\"Simulating failure - Status code is 200 - Expected '200' to not be '200'\\" classname=\\"test-junit-report-export/assertions/failure\\">
<failure type=\\"AssertionFailure\\" message=\\"Expected '200' to not be '200'\\"/>
</testcase>
<testcase name=\\"Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Check headers - Expected 'undefined' to not be 'value'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status\\" classname=\\"test-junit-report-export/assertions/failure\\">
<failure type=\\"AssertionFailure\\" message=\\"Expected '200' to not be 200-level status\\"/>
</testcase>
</testsuite>
</testsuites>"
`;
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report at the specified path 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<testsuites tests=\\"76\\" failures=\\"2\\" errors=\\"66\\" time=\\"time\\">
<testsuite name=\\"test-junit-report-export/request-level-errors/invalid-url\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL]]></system-err>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/request-level-errors/invalid-url\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/request-level-errors/test-script-reference-error\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"0\\" failures=\\"0\\" errors=\\"0\\">
<system-err><![CDATA[
REQUEST_ERROR - TypeError: Invalid URL
TEST_SCRIPT_ERROR - Script execution failed: ReferenceError: status is not defined]]></system-err>
</testsuite>
<testsuite name=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<system-err><![CDATA[
PARSING_ERROR - {
\\"key\\": \\"<<key>>\\"
} (ENV_EXPAND_LOOP)]]></system-err>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/request-level-errors/non-existent-env-var\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/error\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"22\\" failures=\\"0\\" errors=\\"22\\">
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeLevelxxx()\` error scenarios - Expected 200-level status but could not parse value 'foo'\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected 200-level status but could not parse value 'foo'\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toBeType()\` error scenarios - Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toBeType should be &quot;string&quot;, &quot;boolean&quot;, &quot;number&quot;, &quot;object&quot;, &quot;undefined&quot;, &quot;bigint&quot;, &quot;symbol&quot; or &quot;function&quot;\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Expected toHaveLength to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toHaveLength to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toHaveLength()\` error scenarios - Argument for toHaveLength should be a number\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toHaveLength should be a number\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Expected toInclude to be called for an array or string\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Expected toInclude to be called for an array or string\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be null\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toInclude should not be null\\"/>
</testcase>
<testcase name=\\"\`toInclude() error scenarios\` - Argument for toInclude should not be undefined\\" classname=\\"test-junit-report-export/assertions/error\\">
<error message=\\"Argument for toInclude should not be undefined\\"/>
</testcase>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/success\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"5\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Status code is 200 - Expected '200' to be '200'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'application/json, text/plain, */*,image/webp' to be 'application/json, text/plain, */*,image/webp'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'echo.hoppscotch.io' to be 'echo.hoppscotch.io'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Check headers - Expected 'undefined' to be 'undefined'\\" classname=\\"test-junit-report-export/assertions/success\\"/>
<testcase name=\\"Status code is 2xx - Expected '200' to be 200-level status\\" classname=\\"test-junit-report-export/assertions/success\\"/>
</testsuite>
<testsuite name=\\"test-junit-report-export/assertions/failure\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"5\\" failures=\\"2\\" errors=\\"0\\">
<testcase name=\\"Simulating failure - Status code is 200 - Expected '200' to not be '200'\\" classname=\\"test-junit-report-export/assertions/failure\\">
<failure type=\\"AssertionFailure\\" message=\\"Expected '200' to not be '200'\\"/>
</testcase>
<testcase name=\\"Simulating failure - Check headers - Expected 'application/json, text/plain, */*,image/webp' to not be 'application/json, text/plain, */*'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Check headers - Expected 'echo.hoppscotch.io' to not be 'httpbin.org'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Check headers - Expected 'undefined' to not be 'value'\\" classname=\\"test-junit-report-export/assertions/failure\\"/>
<testcase name=\\"Simulating failure - Status code is 2xx - Expected '200' to not be 200-level status\\" classname=\\"test-junit-report-export/assertions/failure\\">
<failure type=\\"AssertionFailure\\" message=\\"Expected '200' to not be 200-level status\\"/>
</testcase>
</testsuite>
</testsuites>"
`;
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report for a collection referring to environment variables 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<testsuites tests=\\"12\\" failures=\\"0\\" errors=\\"0\\" time=\\"time\\">
<testsuite name=\\"Test environment variables in request body/test-request\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"12\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Status code is 200 - Expected '200' to be '200'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments recursively - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments recursively - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments recursively - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'Hello' to be 'Hello'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected '7' to be '7'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'John' to be 'John'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'Doe' to be 'Doe'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'John Doe' to be 'John Doe'\\" classname=\\"Test environment variables in request body/test-request\\"/>
<testcase name=\\"Successfully resolves environments referenced in the request body - Expected 'Hello, John Doe' to be 'Hello, John Doe'\\" classname=\\"Test environment variables in request body/test-request\\"/>
</testsuite>
</testsuites>"
`;
exports[`hopp test [options] <file_path_or_id> > Test\`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path] > Generates a JUnit report for a collection with authorization/headers set at the collection level 1`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?>
<testsuites tests=\\"12\\" failures=\\"0\\" errors=\\"0\\" time=\\"time\\">
<testsuite name=\\"CollectionB/RequestA\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'\\" classname=\\"CollectionB/RequestA\\"/>
<testcase name=\\"Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'\\" classname=\\"CollectionB/RequestA\\"/>
</testsuite>
<testsuite name=\\"CollectionB/FolderA/RequestB\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'Set at root collection' to be 'Set at root collection'\\" classname=\\"CollectionB/FolderA/RequestB\\"/>
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'\\" classname=\\"CollectionB/FolderA/RequestB\\"/>
</testsuite>
<testsuite name=\\"CollectionA/RequestA\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Correctly inherits auth and headers from the root collection - Expected 'Set at root collection' to be 'Set at root collection'\\" classname=\\"CollectionA/RequestA\\"/>
<testcase name=\\"Correctly inherits auth and headers from the root collection - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'\\" classname=\\"CollectionA/RequestA\\"/>
</testsuite>
<testsuite name=\\"CollectionA/FolderA/RequestB\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'Set at root collection' to be 'Set at root collection'\\" classname=\\"CollectionA/FolderA/RequestB\\"/>
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'Bearer BearerToken' to be 'Bearer BearerToken'\\" classname=\\"CollectionA/FolderA/RequestB\\"/>
</testsuite>
<testsuite name=\\"CollectionA/FolderA/FolderB/RequestC\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'Overriden at FolderB' to be 'Overriden at FolderB'\\" classname=\\"CollectionA/FolderA/FolderB/RequestC\\"/>
<testcase name=\\"Correctly inherits auth and headers from the parent folder - Expected 'test-key' to be 'test-key'\\" classname=\\"CollectionA/FolderA/FolderB/RequestC\\"/>
</testsuite>
<testsuite name=\\"CollectionA/FolderA/FolderB/FolderC/RequestD\\" time=\\"time\\" timestamp=\\"timestamp\\" tests=\\"2\\" failures=\\"0\\" errors=\\"0\\">
<testcase name=\\"Overrides auth and headers set at the parent folder - Expected 'Overriden at RequestD' to be 'Overriden at RequestD'\\" classname=\\"CollectionA/FolderA/FolderB/FolderC/RequestD\\"/>
<testcase name=\\"Overrides auth and headers set at the parent folder - Expected 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=' to be 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='\\" classname=\\"CollectionA/FolderA/FolderB/FolderC/RequestD\\"/>
</testsuite>
</testsuites>"
`;

View File

@@ -1,670 +0,0 @@
import { ExecException } from "child_process";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import fs from "fs";
import path from "path";
import { HoppErrorCode } from "../../../types/errors";
import { getErrorCode, getTestJsonFilePath, runCLI } from "../../utils";
describe("hopp test [options] <file_path_or_id>", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
describe("Test `hopp test <file_path_or_id>` command:", () => {
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
const args = "test";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
const args = "invalid-arg";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
});
describe("Supplied collection export file validations", () => {
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
const args = "test notfound.json";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{
code: 1,
});
});
});
describe("Versioned entities", () => {
describe("Collections & Requests", () => {
const testFixtures = [
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
];
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Environments", () => {
const testFixtures = [
{ fileName: "env-v0.json", version: 0 },
{ fileName: "env-v1.json", version: 1 },
];
testFixtures.forEach(({ fileName, version }) => {
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
});
test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully inherits/overrides authorization and headers specified at the root collection at deeply nested collections", async () => {
const args = `test ${getTestJsonFilePath(
"collection-level-auth-headers-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test(
"Successfully inherits/overrides authorization and headers at each level with multiple child collections",
async () => {
const args = `test ${getTestJsonFilePath(
"multiple-child-collections-auth-headers-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
},
{ timeout: 50000 }
);
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {
describe("Supplied environment export file validations", () => {
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt",
"collection"
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath(
"malformed-envs.json",
"environment"
);
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
});
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
});
});
test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with short `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
describe(
"Secret environment variables",
() => {
// Reads secret environment values from system environment
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
const env = {
...process.env,
secretBearerToken: "test-token",
secretBasicAuthUsername: "test-user",
secretBasicAuthPassword: "test-pass",
secretQueryParamValue: "secret-query-param-value",
secretBodyValue: "secret-body-value",
secretHeaderValue: "secret-header-value",
};
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args, { env });
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
},
{ timeout: 20000 }
);
});
describe("Test `hopp test <file_path_or_id> --delay <delay_in_ms>` command:", () => {
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
});
test("Successfully performs delayed request execution for a valid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with the short `-d` flag", async () => {
const args = `${VALID_TEST_ARGS} -d 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
// Future TODO: Enable once a proper e2e test environment is set up locally
describe.skip("Test `hopp test <file_path_or_id> --env <file_path_or_id> --token <access_token> --server <server_url>` command:", () => {
const {
REQ_BODY_ENV_VARS_COLL_ID,
COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID,
REQ_BODY_ENV_VARS_ENVS_ID,
PERSONAL_ACCESS_TOKEN,
} = process.env;
if (
!REQ_BODY_ENV_VARS_COLL_ID ||
!COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID ||
!REQ_BODY_ENV_VARS_ENVS_ID ||
!PERSONAL_ACCESS_TOKEN
) {
return;
}
const SERVER_URL = "https://stage-shc.hoppscotch.io/backend";
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` on not supplying a value for the `--token` flag", async () => {
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --token`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_ARGUMENT` on not supplying a value for the `--server` flag", async () => {
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --server`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
});
describe("Workspace access validations", () => {
const INVALID_COLLECTION_ID = "invalid-coll-id";
const INVALID_ENVIRONMENT_ID = "invalid-env-id";
const INVALID_ACCESS_TOKEN = "invalid-token";
test("Errors with the code `TOKEN_INVALID` if the supplied access token is invalid", async () => {
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --token ${INVALID_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("TOKEN_INVALID");
});
test("Errors with the code `INVALID_ID` if the supplied collection ID is invalid", async () => {
const args = `test ${INVALID_COLLECTION_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ID");
});
test("Errors with the code `INVALID_ID` if the supplied environment ID is invalid", async () => {
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${INVALID_ENVIRONMENT_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ID");
});
test("Errors with the code `INVALID_SERVER_URL` if not supplying a valid SH instance server URL", async () => {
// FE URL of the staging SHC instance
const INVALID_SERVER_URL = "https://stage-shc.hoppscotch.io";
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${INVALID_SERVER_URL}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_SERVER_URL");
});
test("Errors with the code `SERVER_CONNECTION_REFUSED` if supplying an SH instance server URL that doesn't follow URL semantics", async () => {
const INVALID_URL = "invalid-url";
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${INVALID_URL}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("SERVER_CONNECTION_REFUSED");
});
});
test("Successfully retrieves a collection with the ID", async () => {
const args = `test ${COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully retrieves collections and environments from a workspace using their respective IDs", async () => {
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Supports specifying collection file path along with environment ID", async () => {
const TESTS_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const args = `test ${TESTS_PATH} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Supports specifying environment file path along with collection ID", async () => {
const ENV_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Supports specifying both collection and environment file paths", async () => {
const TESTS_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${TESTS_PATH} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test`hopp test <file_path_or_id> --env <file_path_or_id> --reporter-junit [path]", () => {
const genPath = path.resolve("hopp-cli-test");
// Helper function to replace dynamic values before generating test snapshots
// Currently scoped to JUnit report generation
const replaceDynamicValuesInStr = (input: string): string =>
input.replace(
/(time|timestamp)="[^"]+"/g,
(_, attr) => `${attr}="${attr}"`
);
beforeAll(() => {
fs.mkdirSync(genPath);
});
afterAll(() => {
fs.rmdirSync(genPath, { recursive: true });
});
test("Report export fails with the code `REPORT_EXPORT_FAILED` while encountering an error during path creation", async () => {
const exportPath = "hopp-junit-report.xml";
const COLL_PATH = getTestJsonFilePath("passes-coll.json", "collection");
const args = `test ${COLL_PATH} --reporter-junit /non-existent-path/report.xml`;
const { stdout, stderr } = await runCLI(args, {
cwd: path.resolve("hopp-cli-test"),
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("REPORT_EXPORT_FAILED");
expect(stdout).not.toContain(
`Successfully exported the JUnit report to: ${exportPath}`
);
});
test("Generates a JUnit report at the default path", async () => {
const exportPath = "hopp-junit-report.xml";
const COLL_PATH = getTestJsonFilePath(
"test-junit-report-export-coll.json",
"collection"
);
const args = `test ${COLL_PATH} --reporter-junit`;
const { stdout } = await runCLI(args, {
cwd: path.resolve("hopp-cli-test"),
});
expect(stdout).not.toContain(
`Overwriting the pre-existing path: ${exportPath}`
);
expect(stdout).toContain(
`Successfully exported the JUnit report to: ${exportPath}`
);
const fileContents = fs
.readFileSync(path.resolve(genPath, exportPath))
.toString();
expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot();
});
test("Generates a JUnit report at the specified path", async () => {
const exportPath = "outer-dir/inner-dir/report.xml";
const COLL_PATH = getTestJsonFilePath(
"test-junit-report-export-coll.json",
"collection"
);
const args = `test ${COLL_PATH} --reporter-junit ${exportPath}`;
const { stdout } = await runCLI(args, {
cwd: path.resolve("hopp-cli-test"),
});
expect(stdout).not.toContain(
`Overwriting the pre-existing path: ${exportPath}`
);
expect(stdout).toContain(
`Successfully exported the JUnit report to: ${exportPath}`
);
const fileContents = fs
.readFileSync(path.resolve(genPath, exportPath))
.toString();
expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot();
});
test("Generates a JUnit report for a collection with authorization/headers set at the collection level", async () => {
const exportPath = "hopp-junit-report.xml";
const COLL_PATH = getTestJsonFilePath(
"collection-level-auth-headers-coll.json",
"collection"
);
const args = `test ${COLL_PATH} --reporter-junit`;
const { stdout } = await runCLI(args, {
cwd: path.resolve("hopp-cli-test"),
});
expect(stdout).toContain(
`Overwriting the pre-existing path: ${exportPath}`
);
expect(stdout).toContain(
`Successfully exported the JUnit report to: ${exportPath}`
);
const fileContents = fs
.readFileSync(path.resolve(genPath, exportPath))
.toString();
expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot();
});
test("Generates a JUnit report for a collection referring to environment variables", async () => {
const exportPath = "hopp-junit-report.xml";
const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENV_PATH} --reporter-junit`;
const { stdout } = await runCLI(args, {
cwd: path.resolve("hopp-cli-test"),
});
expect(stdout).toContain(
`Overwriting the pre-existing path: ${exportPath}`
);
expect(stdout).toContain(
`Successfully exported the JUnit report to: ${exportPath}`
);
const fileContents = fs
.readFileSync(path.resolve(genPath, exportPath))
.toString();
expect(replaceDynamicValuesInStr(fileContents)).toMatchSnapshot();
});
});
});

View File

@@ -1,655 +0,0 @@
{
"v": 2,
"id": "clx1f86hv000010f8szcfya0t",
"name": "Multiple child collections with authorization & headers set at each level",
"folders": [
{
"v": 2,
"id": "clx1fjgah000110f8a5bs68gd",
"name": "folder-1",
"folders": [
{
"v": 2,
"id": "clx1fjwmm000410f8l1gkkr1a",
"name": "folder-11",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "inherit",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-11-request",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-1\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [
{
"key": "key",
"value": "Set at folder-11",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fjyxm000510f8pv90dt43",
"name": "folder-12",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-12-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-12-request",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-12-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(undefined)\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-12-request\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-12-request\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-12",
"active": true
},
{
"key": "key",
"value": "Set at folder-12",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fk1cv000610f88kc3aupy",
"name": "folder-13",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"key": "api-key",
"addTo": "HEADERS",
"value": "api-key-value",
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true,
"grantTypeInfo": {
"token": "",
"isPKCE": true,
"clientID": "sfasfa",
"password": "",
"username": "",
"grantType": "AUTHORIZATION_CODE",
"authEndpoint": "asfafs",
"clientSecret": "sfasfasf",
"tokenEndpoint": "asfa",
"codeVerifierMethod": "S256"
}
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-13-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header-Request-Level",
"value": "New custom header added at the folder-13-request level",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-13-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level with new header addition\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-13\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-13-request\")\n pw.expect(pw.response.body.headers[\"Custom-Header-Request-Level\"]).toBe(\"New custom header added at the folder-13-request level\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"token": "test-token",
"authType": "bearer",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-13",
"active": true
},
{
"key": "key",
"value": "Set at folder-13",
"active": true
}
]
}
],
"requests": [
{
"v": "4",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-1-request",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-1\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-1",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fjk9o000210f8j0573pls",
"name": "folder-2",
"folders": [
{
"v": 2,
"id": "clx1fk516000710f87sfpw6bo",
"name": "folder-21",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-21-request",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(undefined)\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-2\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [
{
"key": "key",
"value": "Set at folder-21",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fk72t000810f8gfwkpi5y",
"name": "folder-22",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-22-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-22-request",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-22-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(undefined)\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-22-request\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-22-request\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-22",
"active": true
},
{
"key": "key",
"value": "Set at folder-22",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fk95g000910f8bunhaoo8",
"name": "folder-23",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-23-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header-Request-Level",
"value": "New custom header added at the folder-23-request level",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-23-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level with new header addition\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-23\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-23-request\")\n pw.expect(pw.response.body.headers[\"Custom-Header-Request-Level\"]).toBe(\"New custom header added at the folder-23-request level\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"token": "test-token",
"authType": "bearer",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-23",
"active": true
},
{
"key": "key",
"value": "Set at folder-23",
"active": true
}
]
}
],
"requests": [
{
"v": "4",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-2-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-2-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(undefined)\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-2-request\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-2",
"active": true
}
]
},
{
"v": 2,
"id": "clx1fjmlq000310f86o4d3w2o",
"name": "folder-3",
"folders": [
{
"v": 2,
"id": "clx1iwq0p003e10f8u8zg0p85",
"name": "folder-31",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-31-request",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-3\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": [
{
"key": "key",
"value": "Set at folder-31",
"active": true
}
]
},
{
"v": 2,
"id": "clx1izut7003m10f894ip59zg",
"name": "folder-32",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-32-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-32-request",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-32-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(undefined)\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-32-request\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-32-request\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-32",
"active": true
},
{
"key": "key",
"value": "Set at folder-32",
"active": true
}
]
},
{
"v": 2,
"id": "clx1j2ka9003q10f8cdbzpgpg",
"name": "folder-33",
"folders": [],
"requests": [
{
"v": "4",
"auth": {
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-33-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header-Request-Level",
"value": "New custom header added at the folder-33-request level",
"active": true
},
{
"key": "key",
"value": "Overriden at folder-33-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level with new header addition\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-33\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Overriden at folder-33-request\")\n pw.expect(pw.response.body.headers[\"Custom-Header-Request-Level\"]).toBe(\"New custom header added at the folder-33-request level\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"token": "test-token",
"authType": "bearer",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-33",
"active": true
},
{
"key": "key",
"value": "Set at folder-33",
"active": true
}
]
}
],
"requests": [
{
"v": "4",
"auth": {
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "folder-3-request",
"method": "GET",
"params": [],
"headers": [
{
"key": "Custom-Header-Request-Level",
"value": "New custom header added at the folder-3-request level",
"active": true
},
{
"key": "key",
"value": "Set at folder-3-request",
"active": true
}
],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits/overrides authorization/header set at the parent collection level with new header addition\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value overriden at folder-3\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n pw.expect(pw.response.body.headers[\"Key\"]).toBe(\"Set at folder-3-request\")\n pw.expect(pw.response.body.headers[\"Custom-Header-Request-Level\"]).toBe(\"New custom header added at the folder-3-request level\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"key": "testuser",
"addTo": "HEADERS",
"value": "testpass",
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value overriden at folder-3",
"active": true
}
]
}
],
"requests": [
{
"v": "4",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "root-collection-request",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://httpbin.org/get",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully inherits authorization/header set at the parent collection level\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Basic dGVzdHVzZXI6dGVzdHBhc3M=\")\n \n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"Custom header value set at the root collection\")\n pw.expect(pw.response.body.headers[\"Inherited-Header\"]).toBe(\"Inherited header at all levels\")\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "basic",
"password": "testpass",
"username": "testuser",
"authActive": true
},
"headers": [
{
"key": "Custom-Header",
"value": "Custom header value set at the root collection",
"active": true
},
{
"key": "Inherited-Header",
"value": "Inherited header at all levels",
"active": true
}
]
}

View File

@@ -1,150 +0,0 @@
{
"v": 2,
"name": "test-junit-report-export",
"folders": [
{
"v": 2,
"name": "assertions",
"folders": [],
"requests": [
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "error",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.test(\"`toBeLevelxxx()` error scenarios\", ()=> {\n pw.expect(\"foo\").toBeLevel2xx();\n pw.expect(\"foo\").not.toBeLevel2xx();\n});\n\npw.test(\"`toBeType()` error scenarios\", () => {\n pw.expect(2).toBeType(\"foo\")\n pw.expect(\"2\").toBeType(\"bar\")\n pw.expect(true).toBeType(\"baz\")\n pw.expect({}).toBeType(\"qux\")\n pw.expect(undefined).toBeType(\"quux\")\n \n pw.expect(2).not.toBeType(\"foo\")\n pw.expect(\"2\").not.toBeType(\"bar\")\n pw.expect(true).not.toBeType(\"baz\")\n pw.expect({}).not.toBeType(\"qux\")\n pw.expect(undefined).not.toBeType(\"quux\")\n})\n\npw.test(\"`toHaveLength()` error scenarios\", () => {\n pw.expect(5).toHaveLength(0)\n pw.expect(true).toHaveLength(0)\n\n pw.expect(5).not.toHaveLength(0)\n pw.expect(true).not.toHaveLength(0)\n\n pw.expect([1, 2, 3, 4]).toHaveLength(\"a\")\n\n pw.expect([1, 2, 3, 4]).not.toHaveLength(\"a\")\n})\n\npw.test(\"`toInclude() error scenarios`\", () => {\n pw.expect(5).not.toInclude(0)\n pw.expect(true).not.toInclude(0)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(null)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(undefined)\n})",
"preRequestScript": "",
"requestVariables": []
},
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "success",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "\n\n// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check headers\npw.test(\"Check headers\", ()=> {\n pw.expect(pw.response.body.headers[\"accept\"]).toBe(\"application/json, text/plain, */*,image/webp\");\n pw.expect(pw.response.body.headers[\"host\"]).toBe(\"echo.hoppscotch.io\")\n pw.expect(pw.response.body.headers[\"custom-header\"]).toBe(undefined)\n});\n\n// Check status code is 2xx\npw.test(\"Status code is 2xx\", ()=> {\n pw.expect(pw.response.status).toBeLevel2xx();\n});",
"preRequestScript": "",
"requestVariables": []
},
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "failure",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "\n\n// Check status code is 200\npw.test(\"Simulating failure - Status code is 200\", ()=> {\n pw.expect(pw.response.status).not.toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Simulating failure - Check headers\", ()=> {\n pw.expect(pw.response.body.headers[\"accept\"]).not.toBe(\"application/json, text/plain, */*\");\n pw.expect(pw.response.body.headers[\"host\"]).not.toBe(\"httpbin.org\")\n pw.expect(pw.response.body.headers[\"custom-header\"]).not.toBe(\"value\")\n});\n\n// Check status code is 2xx\npw.test(\"Simulating failure - Status code is 2xx\", ()=> {\n pw.expect(pw.response.status).not.toBeLevel2xx();\n});",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
},
{
"v": 2,
"name": "request-level-errors",
"folders": [],
"requests": [
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "invalid-url",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "invalid-url",
"testScript": "pw.test(\"`toBeLevelxxx()` error scenarios\", ()=> {\n pw.expect(\"foo\").toBeLevel2xx();\n pw.expect(\"foo\").not.toBeLevel2xx();\n});\n\npw.test(\"`toBeType()` error scenarios\", () => {\n pw.expect(2).toBeType(\"foo\")\n pw.expect(\"2\").toBeType(\"bar\")\n pw.expect(true).toBeType(\"baz\")\n pw.expect({}).toBeType(\"qux\")\n pw.expect(undefined).toBeType(\"quux\")\n \n pw.expect(2).not.toBeType(\"foo\")\n pw.expect(\"2\").not.toBeType(\"bar\")\n pw.expect(true).not.toBeType(\"baz\")\n pw.expect({}).not.toBeType(\"qux\")\n pw.expect(undefined).not.toBeType(\"quux\")\n})\n\npw.test(\"`toHaveLength()` error scenarios\", () => {\n pw.expect(5).toHaveLength(0)\n pw.expect(true).toHaveLength(0)\n\n pw.expect(5).not.toHaveLength(0)\n pw.expect(true).not.toHaveLength(0)\n\n pw.expect([1, 2, 3, 4]).toHaveLength(\"a\")\n\n pw.expect([1, 2, 3, 4]).not.toHaveLength(\"a\")\n})\n\npw.test(\"`toInclude() error scenarios`\", () => {\n pw.expect(5).not.toInclude(0)\n pw.expect(true).not.toInclude(0)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(null)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(undefined)\n})",
"preRequestScript": "",
"requestVariables": []
},
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-script-reference-error",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "invalid-url",
"testScript": "pw.test(\"Reference error\", () => {\n pw.expect(status).toBe(200);\n})",
"preRequestScript": "",
"requestVariables": []
},
{
"v": "5",
"auth": {
"authType": "inherit",
"authActive": true
},
"body": {
"body": "{\n \"key\": \"<<key>>\"\n}",
"contentType": "application/json"
},
"name": "non-existent-env-var",
"method": "POST",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.test(\"`toBeLevelxxx()` error scenarios\", ()=> {\n pw.expect(\"foo\").toBeLevel2xx();\n pw.expect(\"foo\").not.toBeLevel2xx();\n});\n\npw.test(\"`toBeType()` error scenarios\", () => {\n pw.expect(2).toBeType(\"foo\")\n pw.expect(\"2\").toBeType(\"bar\")\n pw.expect(true).toBeType(\"baz\")\n pw.expect({}).toBeType(\"qux\")\n pw.expect(undefined).toBeType(\"quux\")\n \n pw.expect(2).not.toBeType(\"foo\")\n pw.expect(\"2\").not.toBeType(\"bar\")\n pw.expect(true).not.toBeType(\"baz\")\n pw.expect({}).not.toBeType(\"qux\")\n pw.expect(undefined).not.toBeType(\"quux\")\n})\n\npw.test(\"`toHaveLength()` error scenarios\", () => {\n pw.expect(5).toHaveLength(0)\n pw.expect(true).toHaveLength(0)\n\n pw.expect(5).not.toHaveLength(0)\n pw.expect(true).not.toHaveLength(0)\n\n pw.expect([1, 2, 3, 4]).toHaveLength(\"a\")\n\n pw.expect([1, 2, 3, 4]).not.toHaveLength(\"a\")\n})\n\npw.test(\"`toInclude() error scenarios`\", () => {\n pw.expect(5).not.toInclude(0)\n pw.expect(true).not.toInclude(0)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(null)\n\n pw.expect([1, 2, 3, 4]).not.toInclude(undefined)\n})",
"preRequestScript": "",
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
],
"requests": [],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

Some files were not shown because too many files have changed in this diff Show More