mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: adding cookie apis (#5117)
This commit is contained in:
138
package-lock.json
generated
138
package-lock.json
generated
@@ -11521,6 +11521,33 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
||||
"integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-regexp": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp/node_modules/is-regexp": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
|
||||
"integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
@@ -11953,6 +11980,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-hrtime": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
|
||||
"integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -15557,6 +15596,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/function-timeout": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz",
|
||||
"integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
@@ -16902,6 +16953,14 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-regex": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
|
||||
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -17155,6 +17214,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-ip": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz",
|
||||
"integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==",
|
||||
"dependencies": {
|
||||
"ip-regex": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-lambda": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
|
||||
@@ -26277,6 +26347,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/super-regex": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
|
||||
"integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"clone-regexp": "^3.0.0",
|
||||
"function-timeout": "^0.1.0",
|
||||
"time-span": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -26809,6 +26896,21 @@
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/time-span": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
|
||||
"integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"convert-hrtime": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/timers-browserify": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
|
||||
@@ -26906,6 +27008,7 @@
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
@@ -26921,6 +27024,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
@@ -29943,7 +30047,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
@@ -31007,6 +31110,9 @@
|
||||
"name": "@usebruno/common",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-ip": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
@@ -31017,6 +31123,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
@@ -31537,6 +31644,34 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ip-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/is-ip": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz",
|
||||
"integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ip-regex": "^5.0.0",
|
||||
"super-regex": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -31723,7 +31858,6 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
|
||||
@@ -78,7 +78,17 @@ const STATIC_API_HINTS = {
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
'bru.interpolate(str)',
|
||||
'bru.cookies',
|
||||
'bru.cookies.jar()',
|
||||
'bru.cookies.jar().getCookie(url, name, callback)',
|
||||
'bru.cookies.jar().getCookies(url, callback)',
|
||||
'bru.cookies.jar().setCookie(url, name, value, callback)',
|
||||
'bru.cookies.jar().setCookie(url, cookieObject, callback)',
|
||||
'bru.cookies.jar().setCookies(url, cookiesArray, callback)',
|
||||
'bru.cookies.jar().clear(callback)',
|
||||
'bru.cookies.jar().deleteCookies(url, callback)',
|
||||
'bru.cookies.jar().deleteCookie(url, name, callback)',
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
"axios-ntlm": "^1.4.2",
|
||||
@@ -69,7 +69,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util');
|
||||
const path = require('path');
|
||||
const { parseDataFromResponse } = require('../utils/common');
|
||||
const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies');
|
||||
const { getCookieStringForUrl, saveCookies } = require('../utils/cookies');
|
||||
const { createFormData } = require('../utils/form-data');
|
||||
const { getOAuth2Token } = require('./oauth2');
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
@@ -1,103 +1 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true // silently ignore things like parse errors and invalid domains
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
|
||||
if (!Array.isArray(cookies) || !cookies.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap = {};
|
||||
|
||||
cookieJar.store.getAllCookies((err, cookies) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookies = domainCookieMap[domain];
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookies(domain, null, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookiesForDomain,
|
||||
saveCookies
|
||||
};
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = {
|
||||
'^.+\\.(ts|js)$': 'babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(lodash-es)/)',
|
||||
'/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)'
|
||||
],
|
||||
testEnvironment: 'node'
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
|
||||
500
packages/bruno-common/src/cookies/index.ts
Normal file
500
packages/bruno-common/src/cookies/index.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import { Cookie, CookieJar } from 'tough-cookie';
|
||||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import { isPotentiallyTrustworthyOrigin } from '../utils';
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader: string, requestUrl: string): void => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
if (!cookie) return;
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url: string) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url: string): string => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
if (!Array.isArray(cookies) || !cookies.length) return '';
|
||||
|
||||
const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = (): Promise<Array<{ domain: string; cookies: Cookie[]; cookieString: string }>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap: Record<string, Cookie[]> = {};
|
||||
|
||||
(cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
// Handle null domain by skipping the cookie
|
||||
if (!cookie.domain) return;
|
||||
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookiesForDomain = domainCookieMap[domain];
|
||||
const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookie = (domain: string, path: string, cookieKey: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(cookieJar as any).store.removeCookies(domain, null, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCookieObj = (cookieObj: any, oldCookie: Cookie) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: oldCookie.path,
|
||||
key: oldCookie.key,
|
||||
domain: oldCookie.domain,
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
|
||||
lastAccessed:
|
||||
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
|
||||
? new Date(oldCookie.lastAccessed)
|
||||
: new Date()
|
||||
} as any;
|
||||
};
|
||||
|
||||
const createCookieObj = (cookieObj: any) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: cookieObj.path,
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
|
||||
lastAccessed:
|
||||
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
|
||||
? new Date(cookieObj.lastAccessed)
|
||||
: new Date()
|
||||
} as any;
|
||||
};
|
||||
|
||||
const addCookieForDomain = (domain: string, cookieObj: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
(cookieJar as any).store.putCookie(cookie, (err: Error) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
|
||||
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
|
||||
(cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => {
|
||||
if (removeErr) return reject(removeErr);
|
||||
resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parseCookieString = (cookieStr: string): any | null => {
|
||||
try {
|
||||
const cookie = Cookie.parse(cookieStr);
|
||||
if (!cookie) return null;
|
||||
return {
|
||||
...cookie,
|
||||
expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires
|
||||
};
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const createCookieString = (cookieObj: any): string => {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
let cookieString = cookie.toString(); // tough-cookie omits domain
|
||||
|
||||
// Manually append domain if cookie is hostOnly but we still want Domain flag
|
||||
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
|
||||
cookieString += `; Domain=${cookieObj.domain}`;
|
||||
}
|
||||
return cookieString;
|
||||
}
|
||||
|
||||
const saveCookies = (url: string, headers: any) => {
|
||||
if (headers['set-cookie']) {
|
||||
let setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cookieJarWrapper = () => {
|
||||
return {
|
||||
|
||||
// Get the full cookie object for the given URL & name.
|
||||
getCookie: function (
|
||||
url: string,
|
||||
cookieName: string,
|
||||
callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void
|
||||
) {
|
||||
if (!url || !cookieName) {
|
||||
const error = new Error('URL and cookie name are required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
const cookie = cookies.find((c) => c.key === cookieName);
|
||||
callback(null, cookie || null);
|
||||
});
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<Cookie | null>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
const cookie = cookies.find((c) => c.key === cookieName);
|
||||
resolve(cookie || null);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Get all cookies that would be sent to the given URL.
|
||||
getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) {
|
||||
if (!url) {
|
||||
const error = new Error('URL is required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<Cookie[]>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
resolve(cookies);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
setCookie: function (
|
||||
url: string,
|
||||
nameOrCookieObj: string | Record<string, any>,
|
||||
valueOrCallback?: string | ((err?: Error | undefined) => void),
|
||||
maybeCallback?: (err?: Error | undefined) => void
|
||||
) {
|
||||
// Determine the callback
|
||||
let callback: ((err?: Error | undefined) => void) | undefined;
|
||||
if (typeof maybeCallback === 'function') {
|
||||
callback = maybeCallback;
|
||||
} else if (typeof valueOrCallback === 'function') {
|
||||
callback = valueOrCallback as (err?: Error | undefined) => void;
|
||||
}
|
||||
|
||||
const executeSetCookie = () => {
|
||||
if (!url) throw new Error('URL is required');
|
||||
|
||||
// CASE 1: name/value pair provided
|
||||
if (typeof nameOrCookieObj === 'string') {
|
||||
const cookieName = nameOrCookieObj;
|
||||
const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : '';
|
||||
|
||||
if (!cookieName) throw new Error('Cookie name is required');
|
||||
|
||||
const cookie = new Cookie({
|
||||
key: cookieName,
|
||||
value: cookieValue,
|
||||
domain: new URL(url).hostname,
|
||||
});
|
||||
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE 2: cookie object provided
|
||||
if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {
|
||||
const obj = { ...(nameOrCookieObj as any) } as any;
|
||||
|
||||
if (!obj.key && obj.name) obj.key = obj.name;
|
||||
if (!obj.key) throw new Error('cookieObject.key (name) is required');
|
||||
|
||||
const base = {
|
||||
domain: new URL(url).hostname,
|
||||
...obj,
|
||||
} as any;
|
||||
|
||||
const processedCookie = createCookieObj(base);
|
||||
const cookie = new Cookie(processedCookie);
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If we reach here, arguments were invalid
|
||||
throw new Error('Invalid arguments passed to setCookie');
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
try {
|
||||
executeSetCookie();
|
||||
callback(undefined);
|
||||
} catch (err) {
|
||||
callback(err as Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
executeSetCookie();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
setCookies: function (
|
||||
url: string,
|
||||
cookiesArray: any[],
|
||||
callback?: (err?: Error | undefined) => void
|
||||
) {
|
||||
const executeSetCookies = () => {
|
||||
if (!url) throw new Error('URL is required');
|
||||
if (!Array.isArray(cookiesArray)) {
|
||||
throw new Error('setCookies expects an array of cookie objects');
|
||||
}
|
||||
|
||||
for (const cookieObject of cookiesArray) {
|
||||
const obj = { ...(cookieObject as any) } as any;
|
||||
|
||||
if (!obj.key && obj.name) obj.key = obj.name;
|
||||
if (!obj.key) throw new Error('cookieObject.key (name) is required');
|
||||
|
||||
const base = {
|
||||
domain: new URL(url).hostname,
|
||||
...obj
|
||||
} as any;
|
||||
|
||||
const processedCookie = createCookieObj(base);
|
||||
const cookie = new Cookie(processedCookie);
|
||||
cookieJar.setCookieSync(cookie, url, { ignoreError: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
try {
|
||||
executeSetCookies();
|
||||
callback(undefined);
|
||||
} catch (err) {
|
||||
callback(err as Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
executeSetCookies();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
clear: function (callback?: (err?: Error | undefined) => void) {
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return (cookieJar as any).store.removeAllCookies(callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
(cookieJar as any).store.removeAllCookies((err?: Error) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) {
|
||||
if (!url) {
|
||||
const error = new Error('URL is required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
if (!cookies || !cookies.length) return callback(undefined);
|
||||
|
||||
let pending = cookies.length;
|
||||
const done = (removeErr?: Error) => {
|
||||
if (removeErr) return callback(removeErr);
|
||||
if (--pending === 0) {
|
||||
callback(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return reject(err);
|
||||
if (!cookies || !cookies.length) return resolve();
|
||||
|
||||
let pending = cookies.length;
|
||||
const done = (removeErr?: Error) => {
|
||||
if (removeErr) return reject(removeErr);
|
||||
if (--pending === 0) {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
(cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) {
|
||||
if (!url || !cookieName) {
|
||||
const error = new Error('URL and cookie name are required');
|
||||
if (callback) return callback(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const executeDelete = (callback: (err?: Error) => void) => {
|
||||
cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// Filter cookies matching key
|
||||
const matchingCookies = (cookies || []).filter((c) => c.key === cookieName);
|
||||
if (!matchingCookies.length) return callback(undefined);
|
||||
|
||||
const urlPath = new URL(url).pathname || '/';
|
||||
|
||||
// Prioritise a cookie whose path exactly matches the URL path
|
||||
let cookieToDelete = matchingCookies.find((c) => c.path === urlPath);
|
||||
|
||||
// If not found, fall back to the first matching cookie (most specific path first)
|
||||
if (!cookieToDelete) {
|
||||
// tough-cookie sorts cookies by path length desc, preserve that order
|
||||
cookieToDelete = matchingCookies[0];
|
||||
}
|
||||
|
||||
(cookieJar as any).store.removeCookie(
|
||||
cookieToDelete.domain,
|
||||
cookieToDelete.path,
|
||||
cookieToDelete.key,
|
||||
callback
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
// Callback mode
|
||||
return executeDelete(callback);
|
||||
}
|
||||
|
||||
// Promise mode
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
executeDelete((err?: Error) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} as const;
|
||||
};
|
||||
|
||||
|
||||
const cookiesModule = {
|
||||
cookieJar,
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookie,
|
||||
deleteCookiesForDomain,
|
||||
addCookieForDomain,
|
||||
modifyCookieForDomain,
|
||||
parseCookieString,
|
||||
createCookieString,
|
||||
updateCookieObj,
|
||||
createCookieObj,
|
||||
jar: cookieJarWrapper,
|
||||
saveCookies
|
||||
};
|
||||
|
||||
export default cookiesModule;
|
||||
@@ -1,5 +1,6 @@
|
||||
export { mockDataFunctions } from './utils/faker-functions';
|
||||
export { default as interpolate } from './interpolate';
|
||||
export { default as isRequestTagsIncluded } from './tags';
|
||||
export { default as cookies } from './cookies';
|
||||
|
||||
export * as utils from './utils';
|
||||
@@ -1,5 +1,9 @@
|
||||
export {
|
||||
encodeUrl,
|
||||
parseQueryParams,
|
||||
buildQueryString
|
||||
buildQueryString,
|
||||
} from './url';
|
||||
|
||||
export {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
} from './url/validation';
|
||||
@@ -1,4 +1,4 @@
|
||||
const { isPotentiallyTrustworthyOrigin } = require('./cookie-utils');
|
||||
import { isPotentiallyTrustworthyOrigin } from './validation';
|
||||
|
||||
describe('isPotentiallyTrustworthyOrigin', () => {
|
||||
describe('secure schemes', () => {
|
||||
@@ -130,4 +130,4 @@ describe('isPotentiallyTrustworthyOrigin', () => {
|
||||
expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/bruno-common/src/utils/url/validation.ts
Normal file
67
packages/bruno-common/src/utils/url/validation.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { isIPv4, isIPv6, isIP } from 'is-ip';
|
||||
|
||||
const hostNoBrackets = (host: string): string => {
|
||||
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
|
||||
return host.substring(1, host.length - 1);
|
||||
}
|
||||
return host;
|
||||
};
|
||||
|
||||
const isLoopbackV4 = (address: string): boolean => {
|
||||
const octets = address.split('.');
|
||||
if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) {
|
||||
return false;
|
||||
}
|
||||
return octets.every((octet) => {
|
||||
const n = parseInt(octet, 10);
|
||||
return !Number.isNaN(n) && n >= 0 && n <= 255;
|
||||
});
|
||||
};
|
||||
|
||||
const isLoopbackV6 = (address: string): boolean => address === '::1';
|
||||
|
||||
const isIpLoopback = (address: string): boolean => {
|
||||
if (isIPv4(address)) {
|
||||
return isLoopbackV4(address);
|
||||
}
|
||||
if (isIPv6(address)) {
|
||||
return isLoopbackV6(address);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost');
|
||||
|
||||
const isLocalHostname = (host: string): boolean => {
|
||||
return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mirrors Chrome / Secure Contexts spec for "potentially trustworthy origins".
|
||||
*/
|
||||
const isPotentiallyTrustworthyOrigin = (urlString: string): boolean => {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch {
|
||||
return false; // invalid URL or opaque origin
|
||||
}
|
||||
|
||||
const scheme = url.protocol.replace(':', '').toLowerCase();
|
||||
const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, '');
|
||||
|
||||
// Secure schemes
|
||||
if (scheme === 'https' || scheme === 'wss' || scheme === 'file') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IP literals
|
||||
if (isIP(hostname)) {
|
||||
return isIpLoopback(hostname);
|
||||
}
|
||||
|
||||
// localhost / *.localhost
|
||||
return isLocalHostname(hostname);
|
||||
};
|
||||
|
||||
export { isPotentiallyTrustworthyOrigin };
|
||||
228
packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js
Normal file
228
packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js
Normal file
@@ -0,0 +1,228 @@
|
||||
const cookiesModule = require('../../src/cookies/index.ts').default;
|
||||
|
||||
describe('Bruno Cookie Jar Wrapper - API Examples', () => {
|
||||
let jar;
|
||||
const testUrl = 'https://api.example.com';
|
||||
|
||||
beforeEach(() => {
|
||||
jar = cookiesModule.jar();
|
||||
// Clear all cookies before each test
|
||||
jar.clear();
|
||||
});
|
||||
|
||||
describe('Basic Cookie Operations', () => {
|
||||
test('setCookie and getCookie - name/value pair', async () => {
|
||||
const cookieName = 'authToken';
|
||||
const cookieValue = 'jwt123';
|
||||
|
||||
// Set a cookie
|
||||
await jar.setCookie(testUrl, cookieName, cookieValue);
|
||||
|
||||
// Get the cookie back
|
||||
const cookie = await jar.getCookie(testUrl, cookieName);
|
||||
expect(cookie.key).toBe(cookieName);
|
||||
expect(cookie.value).toBe(cookieValue);
|
||||
expect(cookie.domain).toBe('api.example.com');
|
||||
});
|
||||
|
||||
test('setCookie with cookie object', async () => {
|
||||
const cookieObj = {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
};
|
||||
|
||||
await jar.setCookie(testUrl, cookieObj);
|
||||
|
||||
const cookie = await jar.getCookie(testUrl + '/api', 'sessionId');
|
||||
expect(cookie.key).toBe('sessionId');
|
||||
expect(cookie.value).toBe('abc123');
|
||||
expect(cookie.path).toBe('/api');
|
||||
expect(cookie.httpOnly).toBe(true);
|
||||
expect(cookie.secure).toBe(true);
|
||||
});
|
||||
|
||||
test('getCookie returns null for non-existent cookie', async () => {
|
||||
const cookie = await jar.getCookie(testUrl, 'nonexistent');
|
||||
expect(cookie).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Cookie Operations', () => {
|
||||
test('setCookies with array of cookie objects', async () => {
|
||||
const cookies = [
|
||||
{ key: 'cookie1', value: 'value1' },
|
||||
{ key: 'cookie2', value: 'value2' },
|
||||
{ key: 'cookie3', value: 'value3', httpOnly: true }
|
||||
];
|
||||
|
||||
await jar.setCookies(testUrl, cookies);
|
||||
|
||||
// Verify all cookies were set
|
||||
const retrievedCookies = await jar.getCookies(testUrl);
|
||||
expect(retrievedCookies).toHaveLength(3);
|
||||
|
||||
const cookieNames = retrievedCookies.map(c => c.key);
|
||||
expect(cookieNames).toContain('cookie1');
|
||||
expect(cookieNames).toContain('cookie2');
|
||||
expect(cookieNames).toContain('cookie3');
|
||||
});
|
||||
|
||||
test('getCookies returns all cookies for URL', async () => {
|
||||
// Set multiple cookies
|
||||
await jar.setCookie(testUrl, 'auth', 'token123');
|
||||
await jar.setCookie(testUrl, 'session', 'sess456');
|
||||
await jar.setCookie(testUrl, 'prefs', 'theme=dark');
|
||||
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
const cookieMap = cookies.reduce((map, cookie) => {
|
||||
map[cookie.key] = cookie.value;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
expect(cookieMap.auth).toBe('token123');
|
||||
expect(cookieMap.session).toBe('sess456');
|
||||
expect(cookieMap.prefs).toBe('theme=dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cookie Deletion', () => {
|
||||
test('deleteCookie removes specific cookie', async () => {
|
||||
// Set two cookies
|
||||
await jar.setCookie(testUrl, 'keep', 'keepValue');
|
||||
await jar.setCookie(testUrl, 'remove', 'removeValue');
|
||||
|
||||
// Delete one cookie
|
||||
await jar.deleteCookie(testUrl, 'remove');
|
||||
|
||||
// Verify only one cookie remains
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(1);
|
||||
expect(cookies[0].key).toBe('keep');
|
||||
expect(cookies[0].value).toBe('keepValue');
|
||||
});
|
||||
|
||||
test('deleteCookies removes all cookies for URL', async () => {
|
||||
// Set multiple cookies
|
||||
await jar.setCookie(testUrl, 'cookie1', 'value1');
|
||||
await jar.setCookie(testUrl, 'cookie2', 'value2');
|
||||
|
||||
// Delete all cookies for the URL
|
||||
await jar.deleteCookies(testUrl);
|
||||
|
||||
// Verify no cookies remain
|
||||
const cookies = await jar.getCookies(testUrl);
|
||||
expect(cookies).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('clear removes all cookies from jar', async () => {
|
||||
// Set cookies for multiple URLs
|
||||
await jar.setCookie('https://site1.com', 'cookie1', 'value1');
|
||||
await jar.setCookie('https://site2.com', 'cookie2', 'value2');
|
||||
|
||||
// Clear entire jar
|
||||
await jar.clear();
|
||||
|
||||
// Verify no cookies remain for any URL
|
||||
const cookies1 = await jar.getCookies('https://site1.com');
|
||||
const cookies2 = await jar.getCookies('https://site2.com');
|
||||
|
||||
expect(cookies1).toHaveLength(0);
|
||||
expect(cookies2).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('setCookie handles missing URL', async () => {
|
||||
await expect(jar.setCookie('', 'name', 'value')).rejects.toThrow('URL is required');
|
||||
});
|
||||
|
||||
test('getCookie handles missing URL', async () => {
|
||||
await expect(jar.getCookie('', 'name')).rejects.toThrow('URL and cookie name are required');
|
||||
});
|
||||
|
||||
test('setCookies handles invalid input', async () => {
|
||||
await expect(jar.setCookies(testUrl, 'not-an-array')).rejects.toThrow('expects an array');
|
||||
});
|
||||
|
||||
test('setCookie handles missing cookie name in object', async () => {
|
||||
await expect(jar.setCookie(testUrl, { value: 'test' })).rejects.toThrow('key (name) is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Usage Examples', () => {
|
||||
test('Authentication workflow example', async () => {
|
||||
const apiUrl = 'https://api.example.com';
|
||||
const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
|
||||
|
||||
// Simulate login - set auth cookie
|
||||
await jar.setCookie(apiUrl, 'authToken', authToken);
|
||||
|
||||
// Later in the session - retrieve auth token
|
||||
const cookie = await jar.getCookie(apiUrl, 'authToken');
|
||||
expect(cookie.value).toBe(authToken);
|
||||
|
||||
// Simulate logout - remove auth cookie
|
||||
await jar.deleteCookie(apiUrl, 'authToken');
|
||||
|
||||
// Verify cookie is gone
|
||||
const deletedCookie = await jar.getCookie(apiUrl, 'authToken');
|
||||
expect(deletedCookie).toBeNull();
|
||||
});
|
||||
|
||||
test('Session management with multiple cookies', async () => {
|
||||
const sessionUrl = 'https://app.example.com';
|
||||
|
||||
// Set session cookies
|
||||
const sessionCookies = [
|
||||
{ key: 'sessionId', value: 'sess_123', httpOnly: true },
|
||||
{ key: 'csrfToken', value: 'csrf_456' },
|
||||
{ key: 'userPrefs', value: JSON.stringify({ theme: 'dark', lang: 'en' }) }
|
||||
];
|
||||
|
||||
await jar.setCookies(sessionUrl, sessionCookies);
|
||||
|
||||
// Retrieve all session cookies
|
||||
const cookies = await jar.getCookies(sessionUrl);
|
||||
expect(cookies).toHaveLength(3);
|
||||
|
||||
// Find specific cookies
|
||||
const sessionCookie = cookies.find(c => c.key === 'sessionId');
|
||||
const csrfCookie = cookies.find(c => c.key === 'csrfToken');
|
||||
const prefsCookie = cookies.find(c => c.key === 'userPrefs');
|
||||
|
||||
expect(sessionCookie.value).toBe('sess_123');
|
||||
expect(sessionCookie.httpOnly).toBe(true);
|
||||
expect(csrfCookie.value).toBe('csrf_456');
|
||||
|
||||
const prefs = JSON.parse(prefsCookie.value);
|
||||
expect(prefs.theme).toBe('dark');
|
||||
expect(prefs.lang).toBe('en');
|
||||
});
|
||||
|
||||
test('Cookie path handling', async () => {
|
||||
const baseUrl = 'https://example.com';
|
||||
|
||||
// Set cookies with different paths
|
||||
await jar.setCookie(baseUrl, { key: 'global', value: 'global_val', path: '/' });
|
||||
await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' });
|
||||
await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' });
|
||||
|
||||
const rootCookies = await jar.getCookies(baseUrl + '/');
|
||||
const globalCookie = rootCookies.find(c => c.key === 'global');
|
||||
expect(globalCookie).toBeTruthy();
|
||||
expect(globalCookie.value).toBe('global_val');
|
||||
|
||||
const apiCookies = await jar.getCookies(baseUrl + '/api/users');
|
||||
expect(apiCookies.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const apiCookieNames = apiCookies.map(c => c.key);
|
||||
expect(apiCookieNames).toContain('global');
|
||||
expect(apiCookieNames).toContain('api');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,6 +48,13 @@ const replacements = {
|
||||
'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest',
|
||||
'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()',
|
||||
'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()',
|
||||
// Cookie jar translations
|
||||
'pm\\.cookies\\.jar\\(\\)': 'bru.cookies.jar()',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.get\\(': 'bru.cookies.jar().getCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.set\\(': 'bru.cookies.jar().setCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.unset\\(': 'bru.cookies.jar().deleteCookie(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.clear\\(': 'bru.cookies.jar().deleteCookies(',
|
||||
'pm\\.cookies\\.jar\\(\\)\\.getAll\\(': 'bru.cookies.jar().getCookies(',
|
||||
};
|
||||
|
||||
const extendedReplacements = Object.keys(replacements).reduce((acc, key) => {
|
||||
|
||||
@@ -14,6 +14,11 @@ function getMemberExpressionString(node) {
|
||||
return node.name;
|
||||
}
|
||||
|
||||
if (node.type === 'CallExpression') {
|
||||
const calleeStr = getMemberExpressionString(node.callee);
|
||||
return `${calleeStr}()`;
|
||||
}
|
||||
|
||||
// Handle member expressions
|
||||
if (node.type === 'MemberExpression') {
|
||||
const objectStr = getMemberExpressionString(node.object);
|
||||
@@ -89,6 +94,13 @@ const simpleTranslations = {
|
||||
'pm.response.size().body': 'res.getSize().body',
|
||||
'pm.response.size().header': 'res.getSize().header',
|
||||
'pm.response.size().total': 'res.getSize().total',
|
||||
'pm.cookies.jar': 'bru.cookies.jar',
|
||||
|
||||
'pm.cookies.jar().get': 'bru.cookies.jar().getCookie',
|
||||
'pm.cookies.jar().getAll': 'bru.cookies.jar().getCookies',
|
||||
'pm.cookies.jar().set': 'bru.cookies.jar().setCookie',
|
||||
'pm.cookies.jar().unset': 'bru.cookies.jar().deleteCookie',
|
||||
'pm.cookies.jar().clear': 'bru.cookies.jar().deleteCookies',
|
||||
|
||||
// Execution control
|
||||
'pm.execution.skipRequest': 'bru.runner.skipRequest',
|
||||
@@ -332,6 +344,9 @@ function translateCode(code) {
|
||||
// Preprocess the code to resolve all aliases
|
||||
preprocessAliases(ast);
|
||||
|
||||
// Handle cookie jar variable assignments and method renaming
|
||||
processCookieJarVariables(ast);
|
||||
|
||||
// Process all transformations in a single pass
|
||||
processTransformations(ast, transformedNodes);
|
||||
|
||||
@@ -610,6 +625,59 @@ function removeResolvedDeclarations(ast, symbolTable) {
|
||||
return changesMade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process cookie jar variable assignments and rename methods on those variables
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
*/
|
||||
function processCookieJarVariables(ast) {
|
||||
// Map of Postman cookie jar method names to Bruno equivalents
|
||||
const cookieMethodMapping = {
|
||||
'get': 'getCookie',
|
||||
'getAll': 'getCookies',
|
||||
'set': 'setCookie',
|
||||
'unset': 'deleteCookie',
|
||||
'clear': 'deleteCookies'
|
||||
};
|
||||
|
||||
// Track variables that are assigned to cookie jar instances
|
||||
const cookieJarVariables = new Set();
|
||||
|
||||
// First pass: Find all variables assigned to cookie jar instances
|
||||
ast.find(j.VariableDeclarator).forEach(path => {
|
||||
if (path.value.init && path.value.init.type === 'CallExpression') {
|
||||
const initCall = path.value.init;
|
||||
|
||||
// Check if this is a cookie jar assignment
|
||||
if (initCall.callee.type === 'MemberExpression') {
|
||||
const calleeStr = getMemberExpressionString(initCall.callee);
|
||||
|
||||
if (calleeStr === 'pm.cookies.jar' || calleeStr === 'bru.cookies.jar') {
|
||||
if (path.value.id.type === 'Identifier') {
|
||||
cookieJarVariables.add(path.value.id.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: Rename method calls on cookie jar variables
|
||||
ast.find(j.CallExpression).forEach(path => {
|
||||
if (path.value.callee.type === 'MemberExpression' &&
|
||||
path.value.callee.object.type === 'Identifier' &&
|
||||
path.value.callee.property.type === 'Identifier') {
|
||||
|
||||
const varName = path.value.callee.object.name;
|
||||
const methodName = path.value.callee.property.name;
|
||||
|
||||
// If this is a method call on a cookie jar variable
|
||||
if (cookieJarVariables.has(varName) && cookieMethodMapping[methodName]) {
|
||||
const newMethodName = cookieMethodMapping[methodName];
|
||||
path.value.callee.property.name = newMethodName;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Postman's tests["..."] = ... syntax
|
||||
* @param {Object} ast - jscodeshift AST
|
||||
|
||||
@@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => {
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));";
|
||||
const inputScript = "pm.test('random test', () => pm.globals.clear());";
|
||||
const expectedOutput = "// test('random test', () => pm.globals.clear());";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
const { default: postmanTranslation } = require("../../../src/postman/postman-translations");
|
||||
|
||||
describe('postmanTranslations - cookie API conversions', () => {
|
||||
test('should convert pm.cookies.jar().get to bru.cookies.jar().getCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().get('https://example.com', 'sessionId', (err, cookie) => {
|
||||
console.log(cookie);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().getCookie('https://example.com', 'sessionId', (err, cookie) => {
|
||||
console.log(cookie);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().getAll to bru.cookies.jar().getCookies', () => {
|
||||
const inputScript = `pm.cookies.jar().getAll('https://example.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().getCookies('https://example.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().set to bru.cookies.jar().setCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().set('https://example.com', 'sessionId', 'abc123', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().setCookie('https://example.com', 'sessionId', 'abc123', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().unset to bru.cookies.jar().deleteCookie', () => {
|
||||
const inputScript = `pm.cookies.jar().unset('https://example.com', 'sessionId', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().deleteCookie('https://example.com', 'sessionId', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert pm.cookies.jar().clear to bru.cookies.jar().deleteCookies (behavior difference)', () => {
|
||||
const inputScript = `pm.cookies.jar().clear('https://example.com', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
const expectedOutput = `bru.cookies.jar().deleteCookies('https://example.com', (err) => {
|
||||
if (err) console.error(err);
|
||||
});`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle multiple cookie operations in one script', () => {
|
||||
const inputScript = `
|
||||
pm.cookies.jar().set('https://api.example.com', 'auth', 'token123');
|
||||
const cookie = pm.cookies.jar().get('https://api.example.com', 'auth');
|
||||
pm.cookies.jar().getAll('https://api.example.com', (err, cookies) => {
|
||||
console.log('All cookies:', cookies);
|
||||
});
|
||||
pm.cookies.jar().unset('https://api.example.com', 'temp');
|
||||
pm.cookies.jar().clear('https://api.example.com');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
bru.cookies.jar().setCookie('https://api.example.com', 'auth', 'token123');
|
||||
const cookie = bru.cookies.jar().getCookie('https://api.example.com', 'auth');
|
||||
bru.cookies.jar().getCookies('https://api.example.com', (err, cookies) => {
|
||||
console.log('All cookies:', cookies);
|
||||
});
|
||||
bru.cookies.jar().deleteCookie('https://api.example.com', 'temp');
|
||||
bru.cookies.jar().deleteCookies('https://api.example.com');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert variable assignment and method calls on cookie jar variables', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.set('https://example.com', 'user', 'john');
|
||||
const userCookie = jar.get('https://example.com', 'user');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.setCookie('https://example.com', 'user', 'john');
|
||||
const userCookie = jar.getCookie('https://example.com', 'user');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.get to jar.getCookie with callback', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.get('https://api.example.com', 'authToken', (error, cookie) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookie:', error);
|
||||
} else {
|
||||
console.log('Retrieved cookie:', cookie);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookie('https://api.example.com', 'authToken', (error, cookie) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookie:', error);
|
||||
} else {
|
||||
console.log('Retrieved cookie:', cookie);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.getAll to jar.getCookies with callback', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.getAll('https://api.example.com', (error, cookies) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies:', cookies);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookies('https://api.example.com', (error, cookies) => {
|
||||
if (error) {
|
||||
console.error('Error getting cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies:', cookies);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.set to jar.setCookie with cookie object', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.set('https://api.example.com', {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.setCookie('https://api.example.com', {
|
||||
key: 'sessionId',
|
||||
value: 'abc123',
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
}, (error) => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.unset to jar.deleteCookie', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.unset('https://api.example.com', 'tempCookie', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to delete cookie:', error);
|
||||
} else {
|
||||
console.log('Cookie deleted successfully');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.deleteCookie('https://api.example.com', 'tempCookie', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to delete cookie:', error);
|
||||
} else {
|
||||
console.log('Cookie deleted successfully');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should convert jar.clear to jar.deleteCookies', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.clear('https://api.example.com', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to clear cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies cleared for domain');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.deleteCookies('https://api.example.com', (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to clear cookies:', error);
|
||||
} else {
|
||||
console.log('All cookies cleared for domain');
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle complex cookie workflow with jar variable', () => {
|
||||
const inputScript = `
|
||||
const cookieJar = pm.cookies.jar();
|
||||
|
||||
// Set multiple cookies
|
||||
cookieJar.set('https://example.com', 'auth', 'token123');
|
||||
cookieJar.set('https://example.com', {
|
||||
key: 'preferences',
|
||||
value: JSON.stringify({theme: 'dark'}),
|
||||
path: '/'
|
||||
});
|
||||
|
||||
// Get specific cookie
|
||||
cookieJar.get('https://example.com', 'auth', (err, authCookie) => {
|
||||
console.log('Auth cookie:', authCookie);
|
||||
});
|
||||
|
||||
// Get all cookies
|
||||
cookieJar.getAll('https://example.com', (err, allCookies) => {
|
||||
console.log('Total cookies:', allCookies.length);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
cookieJar.unset('https://example.com', 'temp');
|
||||
cookieJar.clear('https://example.com');
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const cookieJar = bru.cookies.jar();
|
||||
|
||||
// Set multiple cookies
|
||||
cookieJar.setCookie('https://example.com', 'auth', 'token123');
|
||||
cookieJar.setCookie('https://example.com', {
|
||||
key: 'preferences',
|
||||
value: JSON.stringify({theme: 'dark'}),
|
||||
path: '/'
|
||||
});
|
||||
|
||||
// Get specific cookie
|
||||
cookieJar.getCookie('https://example.com', 'auth', (err, authCookie) => {
|
||||
console.log('Auth cookie:', authCookie);
|
||||
});
|
||||
|
||||
// Get all cookies
|
||||
cookieJar.getCookies('https://example.com', (err, allCookies) => {
|
||||
console.log('Total cookies:', allCookies.length);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
cookieJar.deleteCookie('https://example.com', 'temp');
|
||||
cookieJar.deleteCookies('https://example.com');
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle mixed jar variable and direct calls', () => {
|
||||
const inputScript = `
|
||||
const jar = pm.cookies.jar();
|
||||
jar.get('https://api.com', 'session');
|
||||
|
||||
pm.cookies.jar().set('https://other.com', 'temp', 'value');
|
||||
|
||||
jar.getAll('https://api.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
const jar = bru.cookies.jar();
|
||||
jar.getCookie('https://api.com', 'session');
|
||||
|
||||
bru.cookies.jar().setCookie('https://other.com', 'temp', 'value');
|
||||
|
||||
jar.getCookies('https://api.com', (err, cookies) => {
|
||||
console.log(cookies);
|
||||
});
|
||||
`;
|
||||
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -32,13 +32,13 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/node-machine-id": "^2.0.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"@usebruno/vm2": "^3.9.13",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.8.3",
|
||||
@@ -65,7 +65,6 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
|
||||
@@ -472,6 +472,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
@@ -584,6 +587,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
|
||||
collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;
|
||||
|
||||
const domainsWithCookiesPost = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost)));
|
||||
}
|
||||
return scriptResult;
|
||||
};
|
||||
@@ -891,6 +897,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1094,6 +1103,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
error: preRequestError
|
||||
});
|
||||
|
||||
const domainsWithCookiesPreRequest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest)));
|
||||
|
||||
if (preRequestError) {
|
||||
throw preRequestError;
|
||||
}
|
||||
@@ -1282,6 +1294,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
error: postResponseError
|
||||
});
|
||||
|
||||
const domainsWithCookiesPostResponse = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse)));
|
||||
|
||||
if (postResponseScriptResult?.nextRequestName !== undefined) {
|
||||
nextRequestName = postResponseScriptResult.nextRequestName;
|
||||
}
|
||||
@@ -1386,6 +1401,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
scriptType: 'test',
|
||||
error: testError
|
||||
});
|
||||
|
||||
const domainsWithCookiesTest = await getDomainsWithCookies();
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));
|
||||
}
|
||||
} catch (error) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
|
||||
@@ -1,197 +1 @@
|
||||
const { Cookie, CookieJar } = require('tough-cookie');
|
||||
const each = require('lodash/each');
|
||||
const moment = require('moment');
|
||||
const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils;
|
||||
|
||||
const cookieJar = new CookieJar();
|
||||
|
||||
const addCookieToJar = (setCookieHeader, requestUrl) => {
|
||||
const cookie = Cookie.parse(setCookieHeader, { loose: true });
|
||||
cookieJar.setCookieSync(cookie, requestUrl, {
|
||||
ignoreError: true // silently ignore things like parse errors and invalid domains
|
||||
});
|
||||
};
|
||||
|
||||
const getCookiesForUrl = (url) => {
|
||||
return cookieJar.getCookiesSync(url, {
|
||||
secure: isPotentiallyTrustworthyOrigin(url)
|
||||
});
|
||||
};
|
||||
|
||||
const getCookieStringForUrl = (url) => {
|
||||
const cookies = getCookiesForUrl(url);
|
||||
|
||||
if (!Array.isArray(cookies) || !cookies.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
return validCookies.map((cookie) => cookie.cookieString()).join('; ');
|
||||
};
|
||||
|
||||
const getDomainsWithCookies = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const domainCookieMap = {};
|
||||
|
||||
cookieJar.store.getAllCookies((err, cookies) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
cookies.forEach((cookie) => {
|
||||
if (!domainCookieMap[cookie.domain]) {
|
||||
domainCookieMap[cookie.domain] = [cookie];
|
||||
} else {
|
||||
domainCookieMap[cookie.domain].push(cookie);
|
||||
}
|
||||
});
|
||||
|
||||
const domains = Object.keys(domainCookieMap);
|
||||
const domainsWithCookies = [];
|
||||
|
||||
each(domains, (domain) => {
|
||||
const cookies = domainCookieMap[domain];
|
||||
const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now());
|
||||
|
||||
if (validCookies.length) {
|
||||
domainsWithCookies.push({
|
||||
domain,
|
||||
cookies: validCookies,
|
||||
cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve(domainsWithCookies);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookie = (domain, path, cookieKey) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookie(domain, path, cookieKey, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCookiesForDomain = (domain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cookieJar.store.removeCookies(domain, null, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateCookieObj = (cookieObj, oldCookie) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
// Preserve immutable properties from old cookie
|
||||
path: oldCookie.path,
|
||||
key: oldCookie.key,
|
||||
domain: oldCookie.domain,
|
||||
// Handle other mutable properties
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),
|
||||
lastAccessed:
|
||||
oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()
|
||||
? new Date(oldCookie.lastAccessed)
|
||||
: new Date()
|
||||
};
|
||||
};
|
||||
|
||||
const createCookieObj = (cookieObj) => {
|
||||
return {
|
||||
...cookieObj,
|
||||
path: cookieObj.path || '/',
|
||||
expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,
|
||||
creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),
|
||||
lastAccessed:
|
||||
cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()
|
||||
? new Date(cookieObj.lastAccessed)
|
||||
: new Date()
|
||||
};
|
||||
};
|
||||
|
||||
const addCookieForDomain = (domain, cookieObj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
cookieJar.store.putCookie(cookie, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const oldCookie = new Cookie(createCookieObj(oldCookieObj));
|
||||
const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));
|
||||
cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => {
|
||||
if (removeErr) {
|
||||
return reject(removeErr);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parseCookieString = (cookieStr) => {
|
||||
try {
|
||||
const cookie = Cookie.parse(cookieStr);
|
||||
if (!cookie) return null;
|
||||
|
||||
return {
|
||||
...cookie,
|
||||
expires: cookie.expires === Infinity ? null : cookie.expires
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const createCookieString = (cookieObj) => {
|
||||
const cookie = new Cookie(createCookieObj(cookieObj));
|
||||
|
||||
// cookie.toString() omits the domain
|
||||
let cookieString = cookie.toString();
|
||||
|
||||
// Manually append domain and hostOnly if they exist
|
||||
if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {
|
||||
cookieString += `; Domain=${cookieObj.domain}`;
|
||||
}
|
||||
|
||||
return cookieString;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addCookieToJar,
|
||||
getCookiesForUrl,
|
||||
getCookieStringForUrl,
|
||||
getDomainsWithCookies,
|
||||
deleteCookie,
|
||||
deleteCookiesForDomain,
|
||||
addCookieForDomain,
|
||||
modifyCookieForDomain,
|
||||
parseCookieString,
|
||||
createCookieString,
|
||||
updateCookieObj,
|
||||
createCookieObj
|
||||
};
|
||||
module.exports = require('@usebruno/common').cookies;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
const { sendRequest } = require('@usebruno/requests').scripting;
|
||||
const { jar: createCookieJar } = require('@usebruno/common').cookies;
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -17,6 +18,50 @@ class Bru {
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
this.sendRequest = sendRequest;
|
||||
|
||||
this.cookies = {
|
||||
jar: () => {
|
||||
const cookieJar = createCookieJar();
|
||||
|
||||
return {
|
||||
getCookie: (url, cookieName, callback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.getCookie(interpolatedUrl, cookieName, callback);
|
||||
},
|
||||
|
||||
getCookies: (url, callback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.getCookies(interpolatedUrl, callback);
|
||||
},
|
||||
|
||||
setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.setCookie(interpolatedUrl, nameOrCookieObj, valueOrCallback, maybeCallback);
|
||||
},
|
||||
|
||||
setCookies: (url, cookiesArray, callback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.setCookies(interpolatedUrl, cookiesArray, callback);
|
||||
},
|
||||
|
||||
// Clear entire cookie jar
|
||||
clear: (callback) => {
|
||||
return cookieJar.clear(callback);
|
||||
},
|
||||
|
||||
// Delete cookies for a specific URL/domain
|
||||
deleteCookies: (url, callback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.deleteCookies(interpolatedUrl, callback);
|
||||
},
|
||||
|
||||
deleteCookie: (url, cookieName, callback) => {
|
||||
const interpolatedUrl = this.interpolate(url);
|
||||
return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
|
||||
@@ -258,6 +258,136 @@ const addBruShimToContext = (vm, bru) => {
|
||||
});
|
||||
sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));
|
||||
|
||||
let bruCookiesObject = vm.newObject();
|
||||
|
||||
const _jarFn = vm.newFunction('_jar', () => {
|
||||
const nativeJar = bru.cookies.jar();
|
||||
const jarObj = vm.newObject();
|
||||
|
||||
const _getCookieFn = vm.newFunction('_getCookie', (url, cookieName) => {
|
||||
const promise = vm.newPromise();
|
||||
nativeJar.getCookie(vm.dump(url), vm.dump(cookieName), (err, cookie) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(marshallToVm(cleanCircularJson(cookie), vm));
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_getCookieFn.consume((handle) => vm.setProp(jarObj, '_getCookie', handle));
|
||||
|
||||
const _getCookiesFn = vm.newFunction('_getCookies', (url) => {
|
||||
const promise = vm.newPromise();
|
||||
nativeJar.getCookies(vm.dump(url), (err, cookies) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(marshallToVm(cleanCircularJson(cookies), vm));
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_getCookiesFn.consume((handle) => vm.setProp(jarObj, '_getCookies', handle));
|
||||
|
||||
const _setCookieFn = vm.newFunction('_setCookie', (url, nameOrCookieObj, value) => {
|
||||
const promise = vm.newPromise();
|
||||
const dumpedUrl = vm.dump(url);
|
||||
const dumpedNameOrObj = vm.dump(nameOrCookieObj);
|
||||
|
||||
// Check if the second argument is an object (cookie object case)
|
||||
if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) {
|
||||
// Cookie object case: setCookie(url, cookieObject, callback)
|
||||
nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, (err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Name/value case: setCookie(url, name, value, callback)
|
||||
const dumpedValue = value ? vm.dump(value) : '';
|
||||
nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, dumpedValue, (err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_setCookieFn.consume((handle) => vm.setProp(jarObj, '_setCookie', handle));
|
||||
|
||||
const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => {
|
||||
const promise = vm.newPromise();
|
||||
|
||||
nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_setCookiesFn.consume((handle) => vm.setProp(jarObj, '_setCookies', handle));
|
||||
|
||||
const _clearFn = vm.newFunction('_clear', () => {
|
||||
const promise = vm.newPromise();
|
||||
nativeJar.clear((err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_clearFn.consume((handle) => vm.setProp(jarObj, '_clear', handle));
|
||||
|
||||
const _deleteCookiesFn = vm.newFunction('_deleteCookies', (url) => {
|
||||
const promise = vm.newPromise();
|
||||
nativeJar.deleteCookies(vm.dump(url), (err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_deleteCookiesFn.consume((handle) => vm.setProp(jarObj, '_deleteCookies', handle));
|
||||
|
||||
const _deleteCookieFn = vm.newFunction('_deleteCookie', (url, cookieName) => {
|
||||
const promise = vm.newPromise();
|
||||
nativeJar.deleteCookie(vm.dump(url), vm.dump(cookieName), (err) => {
|
||||
if (err) {
|
||||
promise.reject(marshallToVm(cleanJson(err), vm));
|
||||
} else {
|
||||
promise.resolve(vm.undefined);
|
||||
}
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
_deleteCookieFn.consume((handle) => vm.setProp(jarObj, '_deleteCookie', handle));
|
||||
|
||||
return jarObj;
|
||||
});
|
||||
_jarFn.consume((handle) => vm.setProp(bruCookiesObject, '_jar', handle));
|
||||
|
||||
vm.setProp(bruObject, 'cookies', bruCookiesObject);
|
||||
bruCookiesObject.dispose();
|
||||
|
||||
vm.setProp(bruObject, 'runner', bruRunnerObject);
|
||||
vm.setProp(vm.global, 'bru', bruObject);
|
||||
bruObject.dispose();
|
||||
@@ -282,7 +412,41 @@ const addBruShimToContext = (vm, bru) => {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
globalThis.bru.cookies.jar = () => {
|
||||
const _jar = globalThis.bru.cookies._jar();
|
||||
|
||||
const callWithCallback = async (promiseFn, callback) => {
|
||||
if (!callback) return await promiseFn();
|
||||
try {
|
||||
const result = await promiseFn();
|
||||
try { await callback(null, result); } catch(cbErr) { return Promise.reject(cbErr); }
|
||||
} catch(err) {
|
||||
try { await callback(err, null); } catch(cbErr) { return Promise.reject(cbErr); }
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getCookie: (url, name, cb) => callWithCallback(() => _jar._getCookie(url, name), cb),
|
||||
getCookies: (url, cb) => callWithCallback(() => _jar._getCookies(url), cb),
|
||||
setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => {
|
||||
if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {
|
||||
const callback = typeof valueOrCallback === 'function' ? valueOrCallback : undefined;
|
||||
return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj), callback);
|
||||
} else {
|
||||
const value = typeof valueOrCallback === 'string' ? valueOrCallback : '';
|
||||
const callback = typeof maybeCallback === 'function' ? maybeCallback :
|
||||
(typeof valueOrCallback === 'function' ? valueOrCallback : undefined);
|
||||
return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj, value), callback);
|
||||
}
|
||||
},
|
||||
setCookies: (url, cookiesArray, cb) => callWithCallback(() => _jar._setCookies(url, cookiesArray), cb),
|
||||
clear: (cb) => callWithCallback(() => _jar._clear(), cb),
|
||||
deleteCookies: (url, cb) => callWithCallback(() => _jar._deleteCookies(url), cb),
|
||||
deleteCookie: (url, name, cb) => callWithCallback(() => _jar._deleteCookie(url, name), cb)
|
||||
};
|
||||
};
|
||||
`);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
export * as utils from './utils';
|
||||
|
||||
export * as network from './network';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
@@ -1,105 +0,0 @@
|
||||
const { URL } = require('node:url');
|
||||
const net = require('node:net');
|
||||
|
||||
const isLoopbackV4 = (address) => {
|
||||
// 127.0.0.0/8: first octet = 127
|
||||
const octets = address.split('.');
|
||||
return (
|
||||
octets.length === 4
|
||||
) && parseInt(octets[0], 10) === 127;
|
||||
}
|
||||
|
||||
const isLoopbackV6 = (address) => {
|
||||
// new URL(...) follows the WHATWG URL Standard
|
||||
// which compresses IPv6 addresses, therefore the IPv6
|
||||
// loopback address will always be compressed to '[::1]':
|
||||
// https://url.spec.whatwg.org/#concept-ipv6-serializer
|
||||
return (address === '::1');
|
||||
}
|
||||
|
||||
const isIpLoopback = (address) => {
|
||||
if (net.isIPv4(address)) {
|
||||
return isLoopbackV4(address);
|
||||
}
|
||||
|
||||
if (net.isIPv6(address)) {
|
||||
return isLoopbackV6(address);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const isNormalizedLocalhostTLD = (host) => {
|
||||
return host.toLowerCase().endsWith('.localhost');
|
||||
}
|
||||
|
||||
const isLocalHostname = (host) => {
|
||||
return host.toLowerCase() === 'localhost' ||
|
||||
isNormalizedLocalhostTLD(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes leading and trailing square brackets if present.
|
||||
* Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448
|
||||
*
|
||||
* @param {string} host
|
||||
* @returns {string}
|
||||
*/
|
||||
const hostNoBrackets = (host) => {
|
||||
if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {
|
||||
return host.substring(1, host.length - 1);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL string represents a potentially trustworthy origin.
|
||||
*
|
||||
* A URL is considered potentially trustworthy if it:
|
||||
* - Uses HTTPS, WSS or file schemes
|
||||
* - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1)
|
||||
* - Uses localhost or *.localhost hostnames
|
||||
*
|
||||
* @param {string} urlString - The URL to check
|
||||
* @returns {boolean}
|
||||
* @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec}
|
||||
*/
|
||||
const isPotentiallyTrustworthyOrigin = (urlString) => {
|
||||
let url;
|
||||
|
||||
// try ... catch doubles as an opaque origin check
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') {
|
||||
return false;
|
||||
} else throw e;
|
||||
}
|
||||
|
||||
const scheme = url.protocol.replace(':', '').toLowerCase();
|
||||
const hostname = hostNoBrackets(
|
||||
url.hostname
|
||||
).replace(/\.+$/, '');
|
||||
|
||||
if (
|
||||
scheme === 'https' ||
|
||||
scheme === 'wss' ||
|
||||
scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's already an IP literal, check if it's a loopback address
|
||||
if (net.isIP(hostname)) {
|
||||
return isIpLoopback(hostname);
|
||||
}
|
||||
|
||||
// RFC 6761 states that localhost names will always resolve
|
||||
// to the respective IP loopback address:
|
||||
// https://datatracker.ietf.org/doc/html/rfc6761#section-6.3
|
||||
return isLocalHostname(hostname);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isPotentiallyTrustworthyOrigin
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './cookie-utils';
|
||||
@@ -0,0 +1,67 @@
|
||||
meta {
|
||||
name: clear
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
await jar.setCookies('https://testbench-sanity.usebruno.com', [
|
||||
{
|
||||
key: 'test_cookie_1',
|
||||
value: 'value1',
|
||||
path: '/',
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
key: 'test_cookie_2',
|
||||
value: 'value2',
|
||||
path: '/',
|
||||
secure: true
|
||||
}
|
||||
]);
|
||||
|
||||
console.log("Test cookies set up for clear test");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
const cookiesBeforeClear = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
console.log(`Found ${cookiesBeforeClear.length} cookies before clearing`);
|
||||
|
||||
test("cookies should exist before clearing", function() {
|
||||
expect(cookiesBeforeClear).to.be.an('array');
|
||||
expect(cookiesBeforeClear.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
await jar.clear();
|
||||
console.log("Cookie jar cleared");
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
test("should have no cookies after clearing", async function() {
|
||||
const cookiesAfterClear = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
expect(cookiesAfterClear).to.be.an('array');
|
||||
expect(cookiesAfterClear.length).to.equal(0);
|
||||
});
|
||||
|
||||
jar.clear(function(error) {
|
||||
test("should successfully clear with callback", function() {
|
||||
expect(error).to.be.null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
meta {
|
||||
name: deleteCookie
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
await jar.setCookies('https://testbench-sanity.usebruno.com', [
|
||||
{
|
||||
key: 'cookie_to_delete',
|
||||
value: 'will_be_deleted',
|
||||
path: '/',
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
key: 'cookie_to_keep',
|
||||
value: 'should_remain',
|
||||
path: '/',
|
||||
secure: true
|
||||
}
|
||||
]);
|
||||
|
||||
console.log("Test cookies set up");
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
const cookiesBefore = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
console.log(`Found ${cookiesBefore.length} cookies before deletion`);
|
||||
|
||||
const targetCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');
|
||||
test("cookie should exist before deletion", function() {
|
||||
expect(targetCookie).to.not.be.null;
|
||||
expect(targetCookie.key).to.equal('cookie_to_delete');
|
||||
});
|
||||
|
||||
await jar.deleteCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');
|
||||
console.log("Cookie deleted");
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
test("should have deleted the target cookie", async function() {
|
||||
const deletedCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');
|
||||
expect(deletedCookie).to.be.null;
|
||||
});
|
||||
|
||||
test("should keep other cookies intact", async function() {
|
||||
const cookieToKeep = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_keep');
|
||||
expect(cookieToKeep).to.not.be.null;
|
||||
expect(cookieToKeep.key).to.equal('cookie_to_keep');
|
||||
});
|
||||
|
||||
jar.deleteCookie("https://testbench-sanity.usebruno.com", "cookie_to_keep", function(error) {
|
||||
test("should successfully delete with callback", function() {
|
||||
expect(error).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
meta {
|
||||
name: deleteCookies
|
||||
type: http
|
||||
seq: 7
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
// Set up test cookies before the request
|
||||
try {
|
||||
await jar.setCookies('https://testbench-sanity.usebruno.com', [
|
||||
{
|
||||
key: 'test_cookie_1',
|
||||
value: 'value1',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
key: 'test_cookie_2',
|
||||
value: 'value2',
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: true
|
||||
},
|
||||
{
|
||||
key: 'test_cookie_3',
|
||||
value: 'value3',
|
||||
path: '/api',
|
||||
httpOnly: false,
|
||||
secure: true
|
||||
}
|
||||
]);
|
||||
|
||||
console.log("Test cookies set up successfully in pre-request script");
|
||||
|
||||
// Verify cookies were set
|
||||
const cookies = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
console.log(`${cookies.length} cookies set for domain`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to set up test cookies:", error);
|
||||
throw new Error(`Pre-request cookie setup failed: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
// Verify cookies exist before deletion
|
||||
try {
|
||||
const cookiesBeforeDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
|
||||
test("cookies should exist before clearing", function() {
|
||||
expect(cookiesBeforeDeletion).to.be.an('array');
|
||||
expect(cookiesBeforeDeletion.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
if (cookiesBeforeDeletion.length === 0) {
|
||||
throw new Error("No cookies found to delete - setup may have failed");
|
||||
}
|
||||
|
||||
// Delete all cookies for the domain
|
||||
await jar.deleteCookies('https://testbench-sanity.usebruno.com');
|
||||
console.log("deleteCookies operation completed in post-response");
|
||||
|
||||
// Verify deletion worked
|
||||
const cookiesAfterDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com');
|
||||
console.log(`Found ${cookiesAfterDeletion.length} cookies after deletion`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Delete cookies error in post-response:", error);
|
||||
throw new Error(`Failed to delete cookies in post-response: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
jar.getCookies("https://testbench-sanity.usebruno.com", function(error, remainingCookies) {
|
||||
if(error) {
|
||||
console.error("Error checking remaining cookies:", error)
|
||||
throw new Error(`Failed to get remaining cookies: ${error.message || error}`)
|
||||
}
|
||||
|
||||
test("should have no cookies remaining after deletion", function() {
|
||||
expect(remainingCookies).to.be.an('array');
|
||||
expect(remainingCookies.length).to.equal(0);
|
||||
console.log("✓ Confirmed: no cookies remain for domain after deleteCookies");
|
||||
});
|
||||
});
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: cookies
|
||||
seq: 17
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
meta {
|
||||
name: getCookie
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
jar.getCookie("https://testbench-sanity.usebruno.com", "__cf_bm", function(error, data) {
|
||||
if(error) {
|
||||
console.error("Cookie retrieval error:", error)
|
||||
throw new Error(`Failed to get cookie: ${error.message || error}`)
|
||||
}
|
||||
|
||||
test("should successfully retrieve cookie data", function() {
|
||||
expect(data).to.have.property('key');
|
||||
expect(data).to.have.property('value');
|
||||
expect(data.key).to.equal("__cf_bm");
|
||||
expect(data.value).to.be.a('string');
|
||||
expect(data.value).to.not.be.empty;
|
||||
expect(data.domain).to.include('usebruno.com');
|
||||
console.log("Retrieved cookie:", data);
|
||||
});
|
||||
})
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
meta {
|
||||
name: getCookies
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
jar.getCookies("https://testbench-sanity.usebruno.com", function(error, data) {
|
||||
if(error) {
|
||||
console.error("Cookies retrieval error:", error)
|
||||
throw new Error(`Failed to get cookies: ${error.message || error}`)
|
||||
}
|
||||
|
||||
test("should successfully retrieve cookies array", function() {
|
||||
expect(error).to.be.null;
|
||||
expect(data).to.not.be.null;
|
||||
expect(data).to.be.an('array');
|
||||
console.log("Retrieved cookies count:", data.length);
|
||||
});
|
||||
|
||||
test("should have valid cookie structure in array", function() {
|
||||
data.forEach((cookie, index) => {
|
||||
expect(cookie).to.have.property('key');
|
||||
expect(cookie).to.have.property('value');
|
||||
expect(cookie.key).to.be.a('string');
|
||||
expect(cookie.value).to.be.a('string');
|
||||
expect(cookie.domain).to.include('usebruno.com');
|
||||
console.log(`Cookie ${index + 1}:`, cookie);
|
||||
});
|
||||
});
|
||||
|
||||
test("should contain expected cookie properties", function() {
|
||||
const cookieKeys = data.map(cookie => cookie.key);
|
||||
expect(cookieKeys).to.be.an('array');
|
||||
console.log("Found cookie keys:", cookieKeys);
|
||||
});
|
||||
})
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
meta {
|
||||
name: setCookie
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
// Set cookie before the request
|
||||
try {
|
||||
await jar.setCookie("https://testbench-sanity.usebruno.com", {
|
||||
key: "auth",
|
||||
value: "1234",
|
||||
path: "/path"
|
||||
});
|
||||
|
||||
console.log("Cookie set successfully in pre-request script");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Cookie setting error in pre-request:", error);
|
||||
throw new Error(`Pre-request setCookie failed: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
test("should have set cookie successfully", function() {
|
||||
console.log("Verifying cookie set in pre-request script");
|
||||
});
|
||||
|
||||
// Test: Verify the cookie was set by retrieving it
|
||||
const cookieData = await jar.getCookie("https://testbench-sanity.usebruno.com/path", "auth");
|
||||
|
||||
test("should retrieve the set cookie with correct properties", function() {
|
||||
expect(cookieData.key).to.equal("auth");
|
||||
expect(cookieData.value).to.equal("1234");
|
||||
expect(cookieData.path).to.equal("/path");
|
||||
expect(cookieData.domain).to.include('usebruno.com');
|
||||
console.log("Retrieved and verified cookie:", cookieData);
|
||||
});
|
||||
|
||||
// Test: Additional verification - check all cookies for the domain
|
||||
const allCookies = await jar.getCookies("https://testbench-sanity.usebruno.com/path");
|
||||
|
||||
test("should find the cookie in domain cookie list", function() {
|
||||
expect(allCookies).to.be.an('array');
|
||||
expect(allCookies.length).to.be.at.least(1);
|
||||
|
||||
const authCookie = allCookies.find(c => c.key === 'auth');
|
||||
expect(authCookie).to.not.be.undefined;
|
||||
expect(authCookie.value).to.equal("1234");
|
||||
|
||||
console.log("All cookies for domain:", allCookies.map(c => ({ key: c.key, value: c.value, path: c.path })));
|
||||
});
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
meta {
|
||||
name: setCookie header inclusion
|
||||
type: http
|
||||
seq: 6
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{echo-host}}
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar();
|
||||
|
||||
// Set a cookie that should be sent with the upcoming request
|
||||
await jar.setCookie('https://echo.usebruno.com', {
|
||||
key: 'auth',
|
||||
value: 'token123',
|
||||
path: '/',
|
||||
secure: false
|
||||
});
|
||||
}
|
||||
|
||||
tests {
|
||||
const cookieHeader = res.getHeader('cookie');
|
||||
|
||||
test('should attach auth cookie in request headers', function () {
|
||||
expect(cookieHeader).to.be.a('string');
|
||||
expect(cookieHeader).to.include('auth=token123');
|
||||
});
|
||||
|
||||
// Clean up the jar so other tests are not affected
|
||||
const jar = bru.cookies.jar();
|
||||
await jar.clear();
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: false
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
meta {
|
||||
name: setCookies
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
// Set multiple cookies before the request
|
||||
try {
|
||||
await jar.setCookies('https://example.com', [
|
||||
{
|
||||
key: 'auth',
|
||||
value: 'abc123',
|
||||
path: '/path',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
expires: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
},
|
||||
{
|
||||
key: 'session',
|
||||
value: 'xyz789',
|
||||
path: '/foo',
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
}
|
||||
]);
|
||||
|
||||
console.log("Multiple cookies set successfully in pre-request script");
|
||||
|
||||
} catch (error) {
|
||||
console.error("setCookies operation failed in pre-request:", error);
|
||||
throw new Error(`Pre-request setCookies failed: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
const jar = bru.cookies.jar()
|
||||
|
||||
test("should have set multiple cookies successfully", function() {
|
||||
console.log("Verifying cookies set in pre-request script");
|
||||
});
|
||||
|
||||
// Test: Verify first cookie was set correctly
|
||||
const authCookie = await jar.getCookie('https://example.com/path', 'auth');
|
||||
|
||||
test("should retrieve first cookie with correct properties", function() {
|
||||
expect(authCookie.key).to.equal("auth");
|
||||
expect(authCookie.value).to.equal("abc123");
|
||||
expect(authCookie.path).to.equal("/path");
|
||||
expect(authCookie.httpOnly).to.be.true;
|
||||
expect(authCookie.secure).to.be.true;
|
||||
expect(authCookie.domain).to.include('example.com');
|
||||
console.log("Auth cookie verified:", authCookie);
|
||||
});
|
||||
|
||||
// Test: Verify second cookie was set correctly
|
||||
const sessionCookie = await jar.getCookie('https://example.com/foo', 'session');
|
||||
|
||||
test("should retrieve second cookie with correct properties", function() {
|
||||
expect(sessionCookie).to.not.be.null;
|
||||
if (sessionCookie) {
|
||||
expect(sessionCookie.key).to.equal("session");
|
||||
expect(sessionCookie.value).to.equal("xyz789");
|
||||
expect(sessionCookie.path).to.equal("/foo");
|
||||
expect(sessionCookie.httpOnly).to.be.true;
|
||||
expect(sessionCookie.secure).to.be.true;
|
||||
expect(sessionCookie.domain).to.include('example.com');
|
||||
console.log("Session cookie verified:", sessionCookie);
|
||||
}
|
||||
});
|
||||
|
||||
jar.clear()
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
Reference in New Issue
Block a user