Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"glob@<11.1.0": "11.1.0",
"hono@4.10.6": "4.11.4",
"jws@<3.2.3": "3.2.3",
"nodemailer@<7.0.12": "7.0.12",
"nodemailer@<7.0.12": "8.0.0",
"qs@6.14.0": "6.14.1",
"subscriptions-transport-ws>ws": "7.5.10",
"vue": "3.5.27",
Expand Down
4 changes: 2 additions & 2 deletions packages/hoppscotch-backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2026.1.0",
"version": "2026.1.1",
"description": "",
"author": "",
"private": true,
Expand Down Expand Up @@ -65,7 +65,7 @@
"handlebars": "4.7.8",
"io-ts": "2.2.22",
"morgan": "1.10.1",
"nodemailer": "7.0.12",
"nodemailer": "8.0.0",
"passport": "0.7.0",
"passport-github2": "0.1.12",
"passport-google-oauth20": "2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/hoppscotch-backend/src/auth/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ export const authCookieHandler = (
httpOnly: true,
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
sameSite: 'lax',
maxAge: Date.now() + accessTokenValidityInMs,
maxAge: accessTokenValidityInMs,
});
res.cookie(AuthTokenType.REFRESH_TOKEN, authTokens.refresh_token, {
httpOnly: true,
secure: configService.get('INFRA.ALLOW_SECURE_COOKIES') === 'true',
sameSite: 'lax',
maxAge: Date.now() + refreshTokenValidityInMs,
maxAge: refreshTokenValidityInMs,
});

if (!redirect) {
Expand Down
35 changes: 29 additions & 6 deletions packages/hoppscotch-backend/src/infra-config/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ type DefaultInfraConfig = {
isEncrypted: boolean;
};

// Singleton PrismaService instance for infra config operations
let sharedPrismaInstance: PrismaService | null = null;

/**
* Get or create a shared PrismaService instance for infra config operations
*/
function getSharedPrismaInstance(): PrismaService {
if (!sharedPrismaInstance) {
sharedPrismaInstance = new PrismaService();
}
return sharedPrismaInstance;
}

/**
* Disconnect the shared Prisma instance during application shutdown
*/
export async function disconnectSharedPrismaInstance(): Promise<void> {
if (sharedPrismaInstance) {
await sharedPrismaInstance.onModuleDestroy();
sharedPrismaInstance = null;
}
}

/**
* Returns a mapping of authentication providers to their required configuration keys based on the current environment configuration.
*/
Expand Down Expand Up @@ -67,8 +90,8 @@ export function getAuthProviderRequiredKeys(
* (ConfigModule will set the environment variables in the process)
*/
export async function loadInfraConfiguration() {
const prisma = getSharedPrismaInstance();
try {
const prisma = new PrismaService();
const infraConfigs = await prisma.infraConfig.findMany();

const environmentObject: Record<string, string> = {};
Expand Down Expand Up @@ -97,7 +120,7 @@ export async function loadInfraConfiguration() {
* @returns Array of default infra configs
*/
export async function getDefaultInfraConfigs(): Promise<DefaultInfraConfig[]> {
const prisma = new PrismaService();
const prisma = getSharedPrismaInstance();

// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const onboardingCompleteStatus = await isOnboardingCompleted();
Expand Down Expand Up @@ -324,7 +347,7 @@ export async function getDefaultInfraConfigs(): Promise<DefaultInfraConfig[]> {
export async function getMissingInfraConfigEntries(
infraConfigDefaultObjs: DefaultInfraConfig[],
) {
const prisma = new PrismaService();
const prisma = getSharedPrismaInstance();
const dbInfraConfigs = await prisma.infraConfig.findMany();

const missingEntries = infraConfigDefaultObjs.filter(
Expand All @@ -342,7 +365,7 @@ export async function getMissingInfraConfigEntries(
export async function getEncryptionRequiredInfraConfigEntries(
infraConfigDefaultObjs: DefaultInfraConfig[],
) {
const prisma = new PrismaService();
const prisma = getSharedPrismaInstance();
const dbInfraConfigs = await prisma.infraConfig.findMany();

const requiredEncryption = dbInfraConfigs.filter((dbConfig) => {
Expand Down Expand Up @@ -400,7 +423,7 @@ export function stopApp() {
* @returns Array of configured SSO providers
*/
export async function getConfiguredSSOProvidersFromInfraConfig() {
const prisma = new PrismaService();
const prisma = getSharedPrismaInstance();
const env = await loadInfraConfiguration();
const providerConfigKeys = getAuthProviderRequiredKeys(env);

Expand Down Expand Up @@ -437,7 +460,7 @@ export async function getConfiguredSSOProvidersFromInfraConfig() {
* @returns boolean
*/
export async function isOnboardingCompleted(): Promise<boolean> {
const prisma = new PrismaService();
const prisma = getSharedPrismaInstance();
const allowedProviders = await prisma.infraConfig.findUnique({
where: { name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS },
select: { value: true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { InfraConfig } from './infra-config.model';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfig as DBInfraConfig } from 'src/generated/prisma/client';
Expand Down Expand Up @@ -26,6 +26,7 @@ import { ConfigService } from '@nestjs/config';
import {
ServiceStatus,
buildDerivedEnv,
disconnectSharedPrismaInstance,
getDefaultInfraConfigs,
getEncryptionRequiredInfraConfigEntries,
getMissingInfraConfigEntries,
Expand All @@ -45,7 +46,7 @@ import * as crypto from 'crypto';
import { PrismaError } from 'src/prisma/prisma-error-codes';

@Injectable()
export class InfraConfigService implements OnModuleInit {
export class InfraConfigService implements OnModuleInit, OnModuleDestroy {
constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
Expand All @@ -72,6 +73,9 @@ export class InfraConfigService implements OnModuleInit {
async onModuleInit() {
await this.initializeInfraConfigTable();
}
async onModuleDestroy() {
await disconnectSharedPrismaInstance();
}

/**
* Initialize the 'infra_config' table with values from .env
Expand Down
2 changes: 1 addition & 1 deletion packages/hoppscotch-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.30.1",
"version": "0.30.2",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"type": "module",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
}
],
"preRequestScript": "",
"testScript": "hopp.test(\"`hopp.response.body.asJSON()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(hopp.response.body.asJSON().data)\n\n hopp.expect(parsedData.name).toBe('John Doe')\n hopp.expect(parsedData.age).toBeType(\"number\")\n})\n\npm.test(\"`pm.response.json()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(pm.response.json().data)\n\n pm.expect(parsedData.name).toBe('John Doe')\n pm.expect(parsedData.age).toBeType(\"number\")\n})\n\nhopp.test(\"`hopp.response.body.asText()` parses response body as plain text\", () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\npm.test(\"`pm.response.text()` parses response body as plain text\", () => {\n const textResponse = pm.response.text()\n pm.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\nhopp.test(\"hopp.response.bytes()` parses response body as raw bytes\", () => {\n const rawResponse = hopp.response.body.bytes()\n\n hopp.expect(rawResponse[0]).toBe(123)\n})\n\npm.test(\"pm.response.stream` parses response body as raw bytes\", () => {\n const rawResponse = pm.response.stream\n\n pm.expect(rawResponse[0]).toBe(123)\n})\n",
"testScript": "export {};\nhopp.test(\"`hopp.response.body.asJSON()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(hopp.response.body.asJSON().data)\n\n hopp.expect(parsedData.name).toBe('John Doe')\n hopp.expect(parsedData.age).toBeType(\"number\")\n})\n\npm.test(\"`pm.response.json()` parses response body as JSON\", () => {\n const parsedData = JSON.parse(pm.response.json().data)\n\n pm.expect(parsedData.name).toBe('John Doe')\n pm.expect(parsedData.age).toBeType(\"number\")\n})\n\nhopp.test(\"`hopp.response.body.asText()` parses response body as plain text\", () => {\n const textResponse = hopp.response.body.asText()\n hopp.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\npm.test(\"`pm.response.text()` parses response body as plain text\", () => {\n const textResponse = pm.response.text()\n pm.expect(textResponse).toInclude('\\\"test-header\\\":\\\"test\\\"')\n})\n\nhopp.test(\"hopp.response.bytes()` parses response body as raw bytes\", () => {\n const rawResponse = hopp.response.body.bytes()\n\n hopp.expect(rawResponse[0]).toBe(123)\n})\n\npm.test(\"pm.response.stream` parses response body as raw bytes\", () => {\n const rawResponse = pm.response.stream\n\n pm.expect(rawResponse[0]).toBe(123)\n})\n",
"auth": {
"authType": "inherit",
"authActive": true
Expand Down Expand Up @@ -69,7 +69,7 @@
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "hopp.env.set('test_key', 'test_value')\nhopp.env.set('recursive_key', '<<test_key>>')\nhopp.env.global.set('global_key', 'global_value')\nhopp.env.active.set('active_key', 'active_value')\n\n// `pm` namespace equivalents\npm.variables.set('pm_test_key', 'pm_test_value')\npm.environment.set('pm_active_key', 'pm_active_value')\npm.globals.set('pm_global_key', 'pm_global_value')\n",
"preRequestScript": "export {};\nhopp.env.set('test_key', 'test_value')\nhopp.env.set('recursive_key', '<<test_key>>')\nhopp.env.global.set('global_key', 'global_value')\nhopp.env.active.set('active_key', 'active_value')\n\n// `pm` namespace equivalents\npm.variables.set('pm_test_key', 'pm_test_value')\npm.environment.set('pm_active_key', 'pm_active_value')\npm.globals.set('pm_global_key', 'pm_global_value')\n",
"testScript": "\nhopp.test('`hopp.env.get()` retrieves environment variables', () => {\n const value = hopp.env.get('test_key')\n hopp.expect(value).toBe('test_value')\n})\n\npm.test('`pm.variables.get()` retrieves environment variables', () => {\n const value = pm.variables.get('test_key')\n pm.expect(value).toBe('test_value')\n})\n\nhopp.test('`hopp.env.getRaw()` retrieves raw environment variables without resolution', () => {\n const rawValue = hopp.env.getRaw('recursive_key')\n hopp.expect(rawValue).toBe('<<test_key>>')\n})\n\nhopp.test('`hopp.env.get()` resolves recursive environment variables', () => {\n const resolvedValue = hopp.env.get('recursive_key')\n hopp.expect(resolvedValue).toBe('test_value')\n})\n\npm.test('`pm.variables.replaceIn()` resolves template variables', () => {\n const resolved = pm.variables.replaceIn('Value is {{test_key}}')\n pm.expect(resolved).toBe('Value is test_value')\n})\n\nhopp.test('`hopp.env.global.get()` retrieves global environment variables', () => {\n const globalValue = hopp.env.global.get('global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (globalValue) {\n hopp.expect(globalValue).toBe('global_value')\n }\n})\n\npm.test('`pm.globals.get()` retrieves global environment variables', () => {\n const globalValue = pm.globals.get('global_key')\n\n // `pm.globals` would be empty for the CLI\n if (globalValue) {\n pm.expect(globalValue).toBe('global_value')\n }\n})\n\nhopp.test('`hopp.env.active.get()` retrieves active environment variables', () => {\n const activeValue = hopp.env.active.get('active_key')\n hopp.expect(activeValue).toBe('active_value')\n})\n\npm.test('`pm.environment.get()` retrieves active environment variables', () => {\n const activeValue = pm.environment.get('active_key')\n pm.expect(activeValue).toBe('active_value')\n})\n\nhopp.test('Environment methods return null for non-existent keys', () => {\n hopp.expect(hopp.env.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.getRaw('non_existent')).toBe(null)\n hopp.expect(hopp.env.global.get('non_existent')).toBe(null)\n hopp.expect(hopp.env.active.get('non_existent')).toBe(null)\n})\n\npm.test('`pm` environment methods handle non-existent keys correctly', () => {\n pm.expect(pm.variables.get('non_existent')).toBe(undefined)\n pm.expect(pm.environment.get('non_existent')).toBe(undefined)\n pm.expect(pm.globals.get('non_existent')).toBe(undefined)\n pm.expect(pm.variables.has('non_existent')).toBe(false)\n pm.expect(pm.environment.has('non_existent')).toBe(false)\n pm.expect(pm.globals.has('non_existent')).toBe(false)\n})\n\npm.test('`pm` variables set in pre-request script are accessible', () => {\n pm.expect(pm.variables.get('pm_test_key')).toBe('pm_test_value')\n pm.expect(pm.environment.get('pm_active_key')).toBe('pm_active_value')\n\n const pmGlobalValue = hopp.env.global.get('pm_global_key')\n\n // `hopp.env.global` would be empty for the CLI\n if (pmGlobalValue) {\n hopp.expect(pmGlobalValue).toBe('pm_global_value')\n }\n})\n",
"auth": {
"authType": "inherit",
Expand Down Expand Up @@ -760,7 +760,7 @@
"endpoint": "https://echo.hoppscotch.io",
"params": [],
"headers": [],
"preRequestScript": "export {};",
"preRequestScript": "export {};\n",
"testScript": "\n// Map & Set Assertions\npm.test('Map assertions - size property', () => {\n const map = new Map([['key1', 'value1'], ['key2', 'value2']])\n pm.expect(map).to.have.property('size', 2)\n pm.expect(map.size).to.equal(2)\n})\n\npm.test('Set assertions - size property', () => {\n const set = new Set([1, 2, 3, 4])\n pm.expect(set).to.have.property('size', 4)\n pm.expect(set.size).to.equal(4)\n})\n\npm.test('Map instanceOf assertion', () => {\n const map = new Map()\n pm.expect(map).to.be.instanceOf(Map)\n pm.expect(map).to.be.an.instanceOf(Map)\n})\n\npm.test('Set instanceOf assertion', () => {\n const set = new Set()\n pm.expect(set).to.be.instanceOf(Set)\n pm.expect(set).to.be.an.instanceOf(Set)\n})\n\n// Advanced Chai - closeTo\npm.test('closeTo - validates numbers within delta', () => {\n pm.expect(3.14159).to.be.closeTo(3.14, 0.01)\n pm.expect(10.5).to.be.closeTo(11, 1)\n})\n\npm.test('closeTo - negation works', () => {\n pm.expect(100).to.not.be.closeTo(50, 10)\n pm.expect(3.14).to.not.be.closeTo(10, 0.1)\n})\n\npm.test('approximately - alias for closeTo', () => {\n pm.expect(2.5).to.approximately(2.4, 0.2)\n pm.expect(99.99).to.approximately(100, 0.1)\n})\n\n// Advanced Chai - finite\npm.test('finite - validates finite numbers', () => {\n pm.expect(123).to.be.finite\n pm.expect(0).to.be.finite\n pm.expect(-456).to.be.finite\n})\n\npm.test('finite - negation for Infinity', () => {\n pm.expect(Infinity).to.not.be.finite\n pm.expect(-Infinity).to.not.be.finite\n pm.expect(NaN).to.not.be.finite\n})\n\n// Advanced Chai - satisfy\npm.test('satisfy - custom predicate function', () => {\n pm.expect(10).to.satisfy((num) => num > 5)\n pm.expect('hello').to.satisfy((str) => str.length === 5)\n})\n\npm.test('satisfy - complex validation', () => {\n const obj = { name: 'test', value: 100 }\n pm.expect(obj).to.satisfy((o) => o.value > 50 && o.name.length > 0)\n})\n\npm.test('satisfy - negation works', () => {\n pm.expect(5).to.not.satisfy((num) => num > 10)\n pm.expect('abc').to.not.satisfy((str) => str.length > 5)\n})\n\n// Advanced Chai - respondTo\npm.test('respondTo - validates method existence', () => {\n class TestClass {\n testMethod() { return 'test' }\n anotherMethod() { return 'another' }\n }\n pm.expect(TestClass).to.respondTo('testMethod')\n pm.expect(TestClass).to.respondTo('anotherMethod')\n})\n\npm.test('respondTo - with itself for static methods', () => {\n class MyClass {\n static staticMethod() { return 'static' }\n instanceMethod() { return 'instance' }\n }\n pm.expect(MyClass).itself.to.respondTo('staticMethod')\n pm.expect(MyClass).to.not.itself.respondTo('instanceMethod')\n pm.expect(MyClass).to.respondTo('instanceMethod')\n})\n\n// Property Ownership - own.property\npm.test('own.property - distinguishes own vs inherited', () => {\n const parent = { inherited: true }\n const obj = Object.create(parent)\n obj.own = true\n pm.expect(obj).to.have.own.property('own')\n pm.expect(obj).to.not.have.own.property('inherited')\n pm.expect(obj).to.have.property('inherited')\n})\n\npm.test('deep.own.property - deep check with ownership', () => {\n const proto = { shared: 'inherited' }\n const obj = Object.create(proto)\n obj.data = { nested: 'value' }\n pm.expect(obj).to.have.deep.own.property('data', { nested: 'value' })\n pm.expect(obj).to.not.have.deep.own.property('shared')\n})\n\npm.test('ownProperty - alias for own.property', () => {\n const obj = { prop: 'value' }\n pm.expect(obj).to.have.ownProperty('prop')\n pm.expect(obj).to.have.ownProperty('prop', 'value')\n})\n\n// Hopp namespace parity tests\npm.test('hopp.expect Map/Set support', () => {\n const map = new Map([['x', 1]])\n const set = new Set([1, 2])\n hopp.expect(map.size).toBe(1)\n hopp.expect(set.size).toBe(2)\n})\n\npm.test('hopp.expect closeTo support', () => {\n hopp.expect(3.14).to.be.closeTo(3.1, 0.1)\n hopp.expect(10).to.be.closeTo(10.5, 1)\n})\n\npm.test('hopp.expect finite support', () => {\n hopp.expect(42).to.be.finite\n hopp.expect(Infinity).to.not.be.finite\n})\n\npm.test('hopp.expect satisfy support', () => {\n hopp.expect(100).to.satisfy((n) => n > 50)\n hopp.expect('test').to.satisfy((s) => s.length === 4)\n})\n\npm.test('hopp.expect respondTo support', () => {\n class TestClass { method() {} }\n hopp.expect(TestClass).to.respondTo('method')\n})\n\npm.test('hopp.expect own.property support', () => {\n const obj = Object.create({ inherited: 1 })\n obj.own = 2\n hopp.expect(obj).to.have.own.property('own')\n hopp.expect(obj).to.not.have.own.property('inherited')\n})\n\npm.test('hopp.expect ordered.members support', () => {\n const arr = ['a', 'b', 'c']\n hopp.expect(arr).to.have.ordered.members(['a', 'b', 'c'])\n})\n",
"auth": {
"authType": "inherit",
Expand Down Expand Up @@ -1683,4 +1683,4 @@
"headers": [],
"variables": [],
"description": null
}
}
16 changes: 16 additions & 0 deletions packages/hoppscotch-cli/src/utils/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,19 @@ export async function parseCollectionData(

return getValidRequests(collectionSchemaParsedResult.data, pathOrId);
}

/**
* Module prefix added by Monaco editor for TypeScript module mode.
*/
const MODULE_PREFIX = "export {};\n" as const;

/**
* Strips `export {};\n` prefix from scripts before sandbox execution.
* The prefix is added by the web app's Monaco editor for IntelliSense
* and must be removed before execution.
*/
export const stripModulePrefix = (script: string): string => {
return script.startsWith(MODULE_PREFIX)
? script.slice(MODULE_PREFIX.length)
: script;
};
Loading
Loading