diff --git a/.github/workflows/build-hoppscotch-desktop.yml b/.github/workflows/build-hoppscotch-desktop.yml index 606e34cee3d..d4af8df7a60 100644 --- a/.github/workflows/build-hoppscotch-desktop.yml +++ b/.github/workflows/build-hoppscotch-desktop.yml @@ -8,11 +8,11 @@ on: repository: description: Repository to checkout required: false - default: "hoppscotch/hoppscotch" + default: "Ford/hoppscotch" branch: description: Branch to checkout required: false - default: "main" + default: "FS-Changes" tag: description: Tag to checkout (takes precedence over branch if provided) required: false @@ -33,106 +33,6 @@ env: DESKTOP_PATH: ${{ github.workspace }}/packages/hoppscotch-desktop BUNDLER_PATH: ${{ github.workspace }}/packages/hoppscotch-desktop/crates/webapp-bundler jobs: - build-linux: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v3 - with: - repository: ${{ inputs.repository }} - ref: ${{ inputs.tag != '' && inputs.tag || inputs.branch }} - token: ${{ secrets.CHECKOUT_GITHUB_TOKEN }} - - uses: actions/setup-node@v3 - with: - node-version: 20 - - uses: pnpm/action-setup@v4 - with: - version: 10.2.1 - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - - name: Install additional tools - run: | - curl -LO "https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.2.0/cargo-tauri-x86_64-unknown-linux-gnu.tgz" - tar -xzf cargo-tauri-x86_64-unknown-linux-gnu.tgz - chmod +x cargo-tauri - sudo mv cargo-tauri /usr/local/bin/tauri - - name: Install system dependencies - run: | - sudo apt update; - sudo apt install -y \ - build-essential \ - curl \ - wget \ - file \ - libssl-dev \ - libgtk-3-dev \ - libappindicator3-dev \ - librsvg2-dev; - - sudo apt install -y \ - libwebkit2gtk-4.1-0=2.44.0-2 \ - libwebkit2gtk-4.1-dev=2.44.0-2 \ - libjavascriptcoregtk-4.1-0=2.44.0-2 \ - libjavascriptcoregtk-4.1-dev=2.44.0-2 \ - gir1.2-javascriptcoregtk-4.1=2.44.0-2 \ - gir1.2-webkit2-4.1=2.44.0-2; - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Setup environment - run: | - if [ ! -z "${{ secrets.ENV_FILE_CONTENT }}" ]; then - echo "${{ secrets.ENV_FILE_CONTENT }}" > ${{ env.WORKSPACE_PATH }}/.env - echo "Created .env file from repository secret" - elif [ -f "${{ env.WORKSPACE_PATH }}/.env" ]; then - echo "Using existing .env file found in repository" - else - cp ${{ env.WORKSPACE_PATH }}/.env.example ${{ env.WORKSPACE_PATH }}/.env - echo "No .env found, copied from .env.example template" - fi - pnpm install --dir ${{ env.DESKTOP_PATH }} - - name: Build web app - run: | - pnpm install --dir ${{ env.WEB_PATH }} - pnpm --dir ${{ env.WEB_PATH }} generate - - name: Build and run webapp-bundler - run: | - cargo build --release --manifest-path ${{ env.BUNDLER_PATH }}/Cargo.toml - ${{ env.BUNDLER_PATH }}/target/release/webapp-bundler \ - --input ${{ env.WEB_PATH }}/dist \ - --output ${{ env.DESKTOP_PATH }}/bundle.zip \ - --manifest ${{ env.DESKTOP_PATH }}/manifest.json - - name: Build AppImage - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - RUST_LOG: debug - run: pnpm --dir ${{ env.DESKTOP_PATH }} tauri build --verbose -b appimage -b updater - - name: Build DEB - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} - RUST_LOG: debug - run: pnpm --dir ${{ env.DESKTOP_PATH }} tauri build --verbose -b deb -b updater - - name: Prepare artifacts - run: | - ls -lahR ${{ env.DESKTOP_PATH }}/src-tauri/target/release/bundle/ - mkdir -p dist - cp ${{ env.DESKTOP_PATH }}/src-tauri/target/release/bundle/appimage/*.AppImage dist/Hoppscotch_SelfHost_linux_x64.AppImage - cp ${{ env.DESKTOP_PATH }}/src-tauri/target/release/bundle/appimage/*.AppImage.sig dist/Hoppscotch_SelfHost_linux_x64.AppImage.sig - cp ${{ env.DESKTOP_PATH }}/src-tauri/target/release/bundle/deb/*.deb dist/Hoppscotch_SelfHost_linux_x64.deb - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: linux - path: dist/* build-windows: runs-on: windows-latest steps: @@ -151,7 +51,7 @@ jobs: node-version: 20 - uses: pnpm/action-setup@v4 with: - version: 10.2.1 + version: 10.15.0 - uses: actions-rs/toolchain@v1 with: toolchain: nightly @@ -253,7 +153,7 @@ jobs: node-version: 20 - uses: pnpm/action-setup@v4 with: - version: 10.2.1 + version: 10.15.0 - uses: actions-rs/toolchain@v1 with: toolchain: nightly @@ -349,7 +249,7 @@ jobs: node-version: 20 - uses: pnpm/action-setup@v4 with: - version: 10.2.1 + version: 10.15.0 - uses: actions-rs/toolchain@v1 with: toolchain: nightly @@ -433,7 +333,7 @@ jobs: name: macos-aarch64 path: dist/* create-update-manifest: - needs: [build-linux, build-windows, build-macos-x64, build-macos-arm64] + needs: [build-windows, build-macos-x64, build-macos-arm64] runs-on: ubuntu-latest steps: - name: Download all artifacts diff --git a/.gitignore b/.gitignore index 68c412619ab..d904cecfd30 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,9 @@ web_modules/ .env .env.* +# npm configuration files (contains sensitive registry credentials) +.npmrc + # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json old mode 100644 new mode 100755 index 9e308f231d1..01dd5c3261b --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -1,6 +1,6 @@ { - "name": "@hoppscotch/cli", - "version": "0.24.0", + "name": "@devenap/hoppscotch-cli", + "version": "1.2.5", "description": "A CLI to run Hoppscotch test scripts in CI environments.", "homepage": "https://hoppscotch.io", "type": "module", @@ -52,7 +52,9 @@ "qs": "6.14.0", "verzod": "0.4.0", "xmlbuilder2": "3.1.1", - "zod": "3.25.32" + "zod": "3.25.32", + "form-data": "4.0.0", + "proxy-agent": "6.4.0" }, "devDependencies": { "@hoppscotch/data": "workspace:^", diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/digest-auth-success-coll.json b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/digest-auth-success-coll.json index 16f0b6fca4c..0ae506443b4 100644 --- a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/digest-auth-success-coll.json +++ b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/digest-auth-success-coll.json @@ -29,7 +29,7 @@ "params": [], "headers": [], "endpoint": "<>", - "testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);}); \n pw.test(\"Receives the www-authenticate header\", ()=> { pw.expect(pw.response.headers['www-authenticate']).toBeType('string');});", + "testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);});", "preRequestScript": "", "responses": {}, "requestVariables": [] diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/sample-coll.json b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/sample-coll.json index b0ca8cecf2f..facdb85c474 100644 --- a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/sample-coll.json +++ b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/sample-coll.json @@ -15,7 +15,7 @@ "authActive": true }, "preRequestScript": "", - "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});", + "testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n }); \n\n// Check Content-Type\n pw.test(\"Check Content-Type\", () => {\n let contentType = null;\n for (const header of pw.response.headers) {\n if (header.key.toLowerCase() === 'content-type') {\n contentType = header.value;\n break;\n }\n}\npw.expect(contentType).toBe('application/json');\n });", "body": { "contentType": null, "body": null @@ -23,4 +23,4 @@ "requestVariables": [] } ] -} \ No newline at end of file +} diff --git a/packages/hoppscotch-cli/src/utils/collections.ts b/packages/hoppscotch-cli/src/utils/collections.ts index fa635783886..860b5c45b4f 100644 --- a/packages/hoppscotch-cli/src/utils/collections.ts +++ b/packages/hoppscotch-cli/src/utils/collections.ts @@ -34,6 +34,8 @@ import { processRequest, } from "./request"; import { getTestMetrics } from "./test"; +import {ProxyAgent} from "proxy-agent"; +import axios from "axios"; const { WARN, FAIL, INFO } = exceptionColors; @@ -132,8 +134,15 @@ const processCollection = async ( // Request processing initiated message. log(WARN(`\nRunning: ${chalk.bold(requestPath)}`)); + const agent = new ProxyAgent(); + const axiosInstance = axios.create({ + httpsAgent : agent, + httpAgent : agent, + proxy: false, // Disable axios's default proxy handling + }); + // Processing current request. - const result = await processRequest(processRequestParams)(); + const result = await processRequest(processRequestParams,axiosInstance)(); // Updating global & selected envs with new envs from processed-request output. const { global, selected } = result.envs; diff --git a/packages/hoppscotch-cli/src/utils/mutators.ts b/packages/hoppscotch-cli/src/utils/mutators.ts index 153e66b24a9..3fe2ba3eaf7 100644 --- a/packages/hoppscotch-cli/src/utils/mutators.ts +++ b/packages/hoppscotch-cli/src/utils/mutators.ts @@ -8,6 +8,8 @@ import { error } from "../types/errors"; import { FormDataEntry } from "../types/request"; import { isHoppErrnoException } from "./checks"; import { getResourceContents } from "./getters"; +import FormData from "form-data"; +import fsSync from "fs"; const getValidRequests = ( collections: HoppCollection[], @@ -52,20 +54,9 @@ const getValidRequests = ( export const toFormData = (values: FormDataEntry[]) => { const formData = new FormData(); - values.forEach(({ key, value, contentType }) => { - if (contentType) { - formData.append( - key, - new Blob([value], { - type: contentType, - }), - key - ); - - return; - } - - formData.append(key, value); + values.forEach(({ key, value }) => { + const isFilePath = typeof value === "string" && value.startsWith("/"); + formData.append(key, isFilePath ? fsSync.createReadStream(value) : value); }); return formData; diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts index 4837ff6fd63..a12a944d867 100644 --- a/packages/hoppscotch-cli/src/utils/pre-request.ts +++ b/packages/hoppscotch-cli/src/utils/pre-request.ts @@ -33,6 +33,7 @@ import { fetchInitialDigestAuthInfo, generateDigestAuthHeader, } from "./auth/digest"; +import FormData from "form-data"; import { calculateHawkHeader } from "@hoppscotch/data"; @@ -388,7 +389,14 @@ export async function getEffectiveRESTRequest( const effectiveFinalBody = _effectiveFinalBody.right; - if ( + if (effectiveFinalBody instanceof FormData) { + effectiveFinalHeaders.push({ + active: true, + key: "Content-Type", + value: `multipart/form-data; boundary=${effectiveFinalBody.getBoundary()}`, + description: "", + }); + } else if ( request.body.contentType && !effectiveFinalHeaders.some( ({ key }) => key.toLowerCase() === "content-type" diff --git a/packages/hoppscotch-cli/src/utils/request.ts b/packages/hoppscotch-cli/src/utils/request.ts index 4f1611f2103..9c0a33260c5 100644 --- a/packages/hoppscotch-cli/src/utils/request.ts +++ b/packages/hoppscotch-cli/src/utils/request.ts @@ -111,25 +111,38 @@ export const requestRunner = // NOTE: Temporary parsing check for request endpoint. requestConfig.url = new URL(requestConfig.url ?? "").toString(); - let status: number; const baseResponse = await axios(requestConfig); const { config } = baseResponse; - // PR-COMMENT: type error + + const end = hrtime(start); + const duration = getDurationInSeconds(end); + const responseTime = duration * 1000; // Convert seconds to milliseconds + + // Transform axios headers to required format + const transformedHeaders: { key: string; value: string }[] = []; + if (baseResponse.headers) { + for (const [key, value] of Object.entries(baseResponse.headers)) { + if (value !== undefined) { + transformedHeaders.push({ + key, + value: Array.isArray(value) ? value.join(", ") : String(value), + }); + } + } + } + const runnerResponse: RequestRunnerResponse = { - ...baseResponse, endpoint: getRequest.endpoint(config.url), method: getRequest.method(config.method), body: baseResponse.data, - duration: 0, + duration: responseTime, // Use responseTime (in milliseconds) for duration to match GUI behavior + status: baseResponse.status, + statusText: baseResponse.statusText, + headers: transformedHeaders, }; - const end = hrtime(start); - const duration = getDurationInSeconds(end); - runnerResponse.duration = duration; - return E.right(runnerResponse); } catch (e) { - let status: number; const runnerResponse: RequestRunnerResponse = { endpoint: "", method: "GET", @@ -148,14 +161,30 @@ export const requestRunner = runnerResponse.body = data; runnerResponse.statusText = statusText; runnerResponse.status = status; - runnerResponse.headers = headers; + + // Transform axios headers to required format + const transformedHeaders: { key: string; value: string }[] = []; + if (headers) { + for (const [key, value] of Object.entries(headers)) { + if (value !== undefined) { + transformedHeaders.push({ + key, + value: Array.isArray(value) + ? value.join(", ") + : String(value), + }); + } + } + } + runnerResponse.headers = transformedHeaders; } else if (e.request) { return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) })); } const end = hrtime(start); const duration = getDurationInSeconds(end); - runnerResponse.duration = duration; + const responseTime = duration * 1000; // Convert to milliseconds + runnerResponse.duration = responseTime; return E.right(runnerResponse); } @@ -237,7 +266,7 @@ export const processRequest = const preRequestRes = await preRequestScriptRunner( request, processedEnvs, - legacySandbox, + legacySandbox ?? false, collectionVariables )(); if (E.isLeft(preRequestRes)) { @@ -289,9 +318,9 @@ export const processRequest = // Extracting test-script-runner parameters. const testScriptParams = getTestScriptParams( _requestRunnerRes, - request, + effectiveRequest, updatedEnvs, - legacySandbox + legacySandbox ?? false ); // Executing test-runner. diff --git a/packages/hoppscotch-cli/src/utils/test.ts b/packages/hoppscotch-cli/src/utils/test.ts index 69815952167..adb2f086ff2 100644 --- a/packages/hoppscotch-cli/src/utils/test.ts +++ b/packages/hoppscotch-cli/src/utils/test.ts @@ -152,6 +152,7 @@ export const getTestScriptParams = ( body: reqRunnerRes.body, status: reqRunnerRes.status, headers: reqRunnerRes.headers, + duration: reqRunnerRes.duration, }, envs, legacySandbox, diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 7c4c60cfc53..871c8dcf0c3 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -991,6 +991,7 @@ "extensions": "Browser extension", "extensions_use_toggle": "Use the browser extension to send requests (if present)", "follow": "Follow us", + "follow_redirects": "Automatically follow redirects", "general": "General", "general_description": " General settings used in the application", "interceptor": "Interceptor", diff --git a/packages/hoppscotch-common/src/components/app/Footer.vue b/packages/hoppscotch-common/src/components/app/Footer.vue index 6676e5d8a4e..6732f65cc6c 100644 --- a/packages/hoppscotch-common/src/components/app/Footer.vue +++ b/packages/hoppscotch-common/src/components/app/Footer.vue @@ -11,7 +11,7 @@ @click="EXPAND_NAVIGATION = !EXPAND_NAVIGATION" /> -