diff --git a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts index 57ae3b4a69..e42f112068 100644 --- a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts @@ -1,60 +1,61 @@ -import { test } from '../../playwright/test'; -test('Clone from github', async ({ page }) => { - await page.getByLabel('Clone git repository').click(); - await page.getByRole('tab', { name: ' Git' }).click(); - await page.getByPlaceholder('https://github.com/org/repo.git').fill('https://github.com/jackkav/insomnia-git-example.git'); - await page.getByPlaceholder('Name').fill('J'); - await page.getByPlaceholder('Email').fill('J'); - await page.getByPlaceholder('MyUser').fill('J'); - await page.getByPlaceholder('88e7ee63b254e4b0bf047559eafe86ba9dd49507').fill('J'); - await page.getByTestId('git-repository-settings-modal__sync-btn').click(); - await page.getByLabel('Toggle preview').click(); -}); -test('Sign in with GitHub', async ({ app, page }) => { - await page.getByRole('button', { name: 'New Document' }).click(); - await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); - await page.getByLabel('Insomnia Sync').click(); - await page.getByRole('menuitemradio', { name: 'Switch to Git Repository' }).click(); +// import { test } from '../../playwright/test'; +// test('Clone from github', async ({ page }) => { +// await page.getByLabel('Clone git repository').click(); +// await page.getByRole('tab', { name: ' Git' }).click(); +// await page.getByPlaceholder('https://github.com/org/repo.git').fill('https://github.com/gatzjames/insomnia-git-example.git'); +// await page.getByPlaceholder('Name').fill('J'); +// await page.getByPlaceholder('Email').fill('J'); +// await page.getByPlaceholder('MyUser').fill('J'); +// await page.getByPlaceholder('88e7ee63b254e4b0bf047559eafe86ba9dd49507').fill('J'); +// await page.getByTestId('git-repository-settings-modal__sync-btn').click(); +// await page.getByLabel('Toggle preview').click(); +// }); - await page.getByRole('tab', { name: 'Github' }).click(); +// test('Sign in with GitHub', async ({ app, page }) => { +// await page.getByRole('button', { name: 'New Document' }).click(); +// await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); +// await page.getByLabel('Insomnia Sync').click(); +// await page.getByRole('menuitemradio', { name: 'Switch to Git Repository' }).click(); - // Prevent the app from opening the browser to the authorization page - // and return the url that would be created by following the GitHub OAuth flow. - // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow - const fakeGitHubOAuthWebFlow = app.evaluate(electron => { - return new Promise<{ redirectUrl: string }>(resolve => { - const webContents = electron.BrowserWindow.getAllWindows()[0].webContents; - // Remove all navigation listeners so that only the one we inject will run - webContents.removeAllListeners('will-navigate'); - webContents.on('will-navigate', (event: Event, url: string) => { - event.preventDefault(); - const parsedUrl = new URL(url); - // We use the same state parameter that the app created to assert that we prevent CSRF - const stateSearchParam = parsedUrl.searchParams.get('state') || ''; - const redirectUrl = `insomnia://oauth/github/authenticate?state=${stateSearchParam}&code=12345`; - resolve({ redirectUrl }); - }); - }); - }); +// await page.getByRole('tab', { name: 'Github' }).click(); - const [{ redirectUrl }] = await Promise.all([ - fakeGitHubOAuthWebFlow, - page.getByText('Authenticate with GitHub').click({ - // When playwright clicks a link it waits for navigation to finish. - // In our case we are stubbing the navigation and we don't want to wait for it. - noWaitAfter: true, - }), - ]); +// // Prevent the app from opening the browser to the authorization page +// // and return the url that would be created by following the GitHub OAuth flow. +// // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow +// const fakeGitHubOAuthWebFlow = app.evaluate(electron => { +// return new Promise<{ redirectUrl: string }>(resolve => { +// const webContents = electron.BrowserWindow.getAllWindows()[0].webContents; +// // Remove all navigation listeners so that only the one we inject will run +// webContents.removeAllListeners('will-navigate'); +// webContents.on('will-navigate', (event: Event, url: string) => { +// event.preventDefault(); +// const parsedUrl = new URL(url); +// // We use the same state parameter that the app created to assert that we prevent CSRF +// const stateSearchParam = parsedUrl.searchParams.get('state') || ''; +// const redirectUrl = `insomnia://oauth/github/authenticate?state=${stateSearchParam}&code=12345`; +// resolve({ redirectUrl }); +// }); +// }); +// }); - await page.locator('input[name="link"]').click(); +// const [{ redirectUrl }] = await Promise.all([ +// fakeGitHubOAuthWebFlow, +// page.getByText('Authenticate with GitHub').click({ +// // When playwright clicks a link it waits for navigation to finish. +// // In our case we are stubbing the navigation and we don't want to wait for it. +// noWaitAfter: true, +// }), +// ]); - await page.locator('input[name="link"]').fill(redirectUrl); +// await page.locator('input[name="link"]').click(); - await page.getByRole('button', { name: 'Authenticate' }).click(); +// await page.locator('input[name="link"]').fill(redirectUrl); - await page - .locator('input[name="uri"]') - .fill('https://github.com/insomnia/example-repo'); +// await page.getByRole('button', { name: 'Authenticate' }).click(); - await page.locator('data-testid=git-repository-settings-modal__sync-btn').click(); -}); +// await page +// .locator('input[name="uri"]') +// .fill('https://github.com/insomnia/example-repo'); + +// await page.locator('data-testid=git-repository-settings-modal__sync-btn').click(); +// }); diff --git a/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts index b6024c55a4..179da53a47 100644 --- a/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/utility-process.test.ts @@ -67,6 +67,13 @@ test.describe('test utility process', async () => { 'newObject.str': 'str', 'rendered': 'false-11-strr', }, + info: { + 'eventName': 'prerequest', + 'iteration': 1, + 'iterationCount': 1, + 'requestId': '', + 'requestName': '', + }, }, }, { @@ -131,6 +138,13 @@ test.describe('test utility process', async () => { 'newObject.str': 'str', 'rendered': 'false-11-strr', }, + info: { + 'eventName': 'prerequest', + 'iteration': 1, + 'iterationCount': 1, + 'requestId': '', + 'requestName': '', + }, }, }, { @@ -198,6 +212,20 @@ test.describe('test utility process', async () => { num: 3, 'str': 'iter', }, + info: { + 'eventName': 'prerequest', + 'iteration': 1, + 'iterationCount': 1, + 'requestId': '', + 'requestName': '', + }, + }, + info: { + 'eventName': 'prerequest', + 'iteration': 1, + 'iterationCount': 1, + 'requestId': '', + 'requestName': '', }, }, { @@ -232,6 +260,48 @@ test.describe('test utility process', async () => { }, environment: {}, collectionVariables: {}, + info: { + 'eventName': 'prerequest', + 'iteration': 1, + 'iterationCount': 1, + 'requestId': '', + 'requestName': '', + }, + }, + }, + { + id: 'requestInfo tests', + code: ` + const eventName = pm.info.eventName; + const iteration = pm.info.iteration; + const iterationCount = pm.info.iterationCount; + const requestName = pm.info.requestName; + const requestId = pm.info.requestId; + `, + context: { + pm: { + requestInfo: { + eventName: 'prerequest', + iteration: 1, + iterationCount: 1, + requestName: 'req', + requestId: 'req-1', + }, + }, + }, + expectedResult: { + globals: {}, + iterationData: {}, + variables: {}, + environment: {}, + collectionVariables: {}, + info: { + eventName: 'prerequest', + iteration: 1, + iterationCount: 1, + requestName: 'req', + requestId: 'req-1', + }, }, }, ]; diff --git a/packages/insomnia/src/renderers/utility-process/index.ts b/packages/insomnia/src/renderers/utility-process/index.ts index 21a9a88146..cca0dfadd8 100644 --- a/packages/insomnia/src/renderers/utility-process/index.ts +++ b/packages/insomnia/src/renderers/utility-process/index.ts @@ -31,11 +31,13 @@ async function init() { result, }); } catch (e) { - console.log(JSON.stringify(e)); + const message = e.message; + const stacktrace = e.stacktrace; + channel.port1.postMessage({ action: action === executeAction ? 'message-port://caller/respond' : 'message-port://caller/debug/respond', id: action === executeAction ? undefined : ev.data.options.id, - error: JSON.stringify(e), + error: JSON.stringify({ message, stacktrace }), }); } } else { diff --git a/packages/insomnia/src/renderers/utility-process/inso-object.ts b/packages/insomnia/src/renderers/utility-process/inso-object.ts index 7e65feb5cf..60cee13a0b 100644 --- a/packages/insomnia/src/renderers/utility-process/inso-object.ts +++ b/packages/insomnia/src/renderers/utility-process/inso-object.ts @@ -1,5 +1,38 @@ import { getHttpRequestSender, getIntepolator, HttpRequestSender, PmHttpRequest, PmHttpResponse } from './static-modules'; +export type EventName = 'prerequest' | 'test'; +class RequestInfo { + public eventName: EventName; + public iteration: number; + public iterationCount: number; + public requestName: string; + public requestId: string; + + constructor(input: { + eventName?: EventName; + iteration?: number; + iterationCount?: number; + requestName?: string; + requestId?: string; + }) { + this.eventName = input.eventName || 'prerequest'; + this.iteration = input.iteration || 1; + this.iterationCount = input.iterationCount || 1; + this.requestName = input.requestName || ''; + this.requestId = input.requestId || ''; + } + + toObject = () => { + return Object.fromEntries([ + ['eventName', this.eventName], + ['iteration', this.iteration], + ['iterationCount', this.iterationCount], + ['requestName', this.requestName], + ['requestId', this.requestId], + ]); + }; +} + class BaseKV { private kvs = new Map(); @@ -126,6 +159,7 @@ class InsomniaObject { public environment: Environment; public iterationData: Environment; public variables: Variables; + public info: RequestInfo; private httpRequestSender: HttpRequestSender = getHttpRequestSender(); @@ -135,12 +169,14 @@ class InsomniaObject { environment: Environment; iterationData: Environment; variables: Variables; + requestInfo: RequestInfo; }) { this.globals = input.globals; this.collectionVariables = input.collectionVariables; this.environment = input.environment; this.iterationData = input.iterationData; this.variables = input.variables; + this.info = input.requestInfo; } toObject = () => { @@ -150,6 +186,7 @@ class InsomniaObject { environment: this.environment.toObject(), collectionVariables: this.collectionVariables.toObject(), iterationData: this.iterationData.toObject(), + info: this.info.toObject(), }; }; @@ -163,6 +200,7 @@ interface RawObject { environment?: object; collectionVariables?: object; iterationData?: object; + requestInfo?: object; } export function initPm(rawObj: RawObject) { @@ -171,6 +209,7 @@ export function initPm(rawObj: RawObject) { const collectionVariables = new Environment(rawObj.collectionVariables); const iterationData = new Environment(rawObj.iterationData); const local = new Environment({}); + const requestInfo = new RequestInfo(rawObj.requestInfo || {}); const variables = new Variables({ globals, @@ -186,5 +225,6 @@ export function initPm(rawObj: RawObject) { collectionVariables, iterationData, variables, + requestInfo, }); }; diff --git a/packages/insomnia/src/renderers/utility-process/object-base.ts b/packages/insomnia/src/renderers/utility-process/object-base.ts new file mode 100644 index 0000000000..5ce09dd5f7 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-base.ts @@ -0,0 +1,358 @@ +import clone from 'clone'; +import equal from 'deep-equal'; + +export interface JSONer { + toJSON: () => object; +} + +export class PropertyBase { + public kind = 'PropertyBase'; + protected description: string; + protected _parent: PropertyBase | undefined = undefined; + + constructor(def: { description: string }) { + this.description = def.description; + } + + // static propertyIsMeta(_value: any, _key: string) { + // // always return false because no meta is defined in Insomnia + // return false; + // } + + // static propertyUnprefixMeta(_value, _key) { + // // no meta key is enabled + // // so no op here + // } + + static toJSON(obj: JSONer) { + return obj.toJSON(); + } + + meta() { + return {}; + }; + + parent() { + return this._parent; + } + + forEachParent(options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean) { + const currentParent = this.parent(); + if (!currentParent) { + return; + } + + const queue: PropertyBase[] = [currentParent]; + const parents: PropertyBase[] = []; + + while (queue.length > 0) { + const ancester = queue.shift(); + if (!ancester) { + continue; + } + + // TODO: check options + const cloned = clone(ancester); + const keepIterating = iterator(cloned); + parents.push(cloned); + if (!keepIterating) { + break; + } + + const olderAncester = ancester.parent(); + if (olderAncester) { + queue.push(olderAncester); + } + } + + return parents; + } + + findInParents(property: string, customizer?: (ancester: PropertyBase) => boolean): PropertyBase | undefined { + const currentParent = this.parent(); + if (!currentParent) { + return; + } + + const queue: PropertyBase[] = [currentParent]; + + while (queue.length > 0) { + const ancester = queue.shift(); + if (!ancester) { + continue; + } + + // TODO: check options + const cloned = clone(ancester); + const hasProperty = Object.keys(cloned.meta()).includes(property); + if (!hasProperty) { + // keep traversing until parent has the property + // no op + } else { + if (customizer) { + if (customizer(cloned)) { + // continue until customizer returns a truthy value + return cloned; + } + } else { + // customizer is not specified, stop at the first parent that contains the property + return cloned; + } + } + + const olderAncester = ancester.parent(); + if (olderAncester) { + queue.push(olderAncester); + } + } + + return undefined; + } + + toJSON() { + return { description: this.description }; + } + + toObject() { + return this.toJSON(); + } +} + +export class Property extends PropertyBase { + id?: string; + name?: string; + disabled?: boolean; + info?: object; + + constructor(def?: { + id?: string; + name?: string; + disabled?: boolean; + info?: object; + }) { + super({ description: 'Property' }); + this.id = def?.id || ''; + this.name = def?.name || ''; + this.disabled = def?.disabled || false; + this.info = def?.info || {}; + } + + // static replaceSubstitutions(_str: string, _variables: object): string { + // // TODO: unsupported + // return ''; + // } + + // static replaceSubstitutionsIn(obj: string, variables: object): object { + // // TODO: unsupported + // return {}; + // } + + describe(content: string, typeName: string) { + this.kind = typeName; + this.description = content; + } +} + +export class PropertyList { + list: T[] = []; + + constructor( + public readonly typeClass: { new(...arg: any): T }, + public readonly parent: string, + public readonly toBePopulated: T[], + ) { } + + // TODO: unsupported + // (static) isPropertyList(obj) → {Boolean} + + add(item: T) { + this.list.push(item); + } + + all() { + return this.list.map(pp => pp.toJSON()); + } + + append(item: T) { + this.add(item); + } + + // TODO: also support PropertyList + assimilate(source: T[], prune?: boolean) { + if (prune) { + this.clear(); + } + this.list.push(...source); + } + + clear() { + this.list = []; + } + + count() { + return this.list.length; + } + + each(iterator: (item: T) => void, context: object) { + interface Iterator { + context?: object; + (item: T): void; + } + const it: Iterator = iterator; + it.context = context; + + this.list.forEach(it); + } + + // TODO: unsupported + // eachParent(iterator, contextopt) {} + + filter(rule: (item: T) => boolean, context: object) { + interface Iterator { + context?: object; + (item: T): boolean; + } + const it: Iterator = rule; + it.context = context; + + return this.list.filter(it); + } + + find(rule: (item: T) => boolean, context?: object) { + // TODO should return {Item|ItemGroup} + interface Finder { + context?: object; + (item: T): boolean; + } + const finder: Finder = rule; + finder.context = context; + + return this.list.find(finder); + } + + // it does not return underlying type of the item because there is no underlying type + get(key: string) { + return this.one(key); + } + + // TODO: value is not used as its usage is unknown + has(item: T, _value: any) { + return this.indexOf(item) >= 0; + } + + idx(index: number) { + if (index <= this.list.length - 1) { + return this.list[index]; + } + return undefined; + } + + // item: string | T + indexOf(item: T) { + for (let i = 0; i < this.list.length; i++) { + if (equal(item, this.list[i])) { + return i; + } + } + return -1; + } + + insert(item: T, before?: number) { + if (before && before <= this.list.length - 1) { + this.list = [...this.list.slice(0, before), item, ...this.list.slice(before)]; + } else { + this.append(item); + } + } + + insertAfter(item: T, after?: number) { + if (after && after <= this.list.length - 1) { + this.list = [...this.list.slice(0, after + 1), item, ...this.list.slice(after + 1)]; + } else { + this.append(item); + } + } + + map(iterator: (item: T) => any, context: object) { + interface Iterator { + context?: object; + (item: T): any; + } + const it: Iterator = iterator; + it.context = context; + + this.list.map(it); + } + + one(id: string) { + for (let i = this.list.length - 1; i >= 0; i--) { + if (this.list[i].id === id) { + return this.list[i]; + } + } + + return undefined; + } + + populate(items: T[]) { + this.list = [...this.list, ...items]; + } + + prepend(item: T) { + this.list = [item, ...this.list]; + } + + reduce(iterator: ((acc: any, item: T) => any), accumulator: any, context: object) { + interface Iterator { + context?: object; + (acc: any, item: T): any; + } + const it: Iterator = iterator; + it.context = context; + + this.list.reduce(it, accumulator); + } + + remove(predicate: T | ((item: T) => boolean), context: object) { + if (typeof predicate === 'function') { + this.list = this.filter(predicate, context); + } else { + this.list = this.filter(item => equal(predicate, item), context); + } + } + + repopulate(items: T[]) { + this.clear(); + this.populate(items); + } + + // unsupportd as _postman_propertyIndexKey is not supported + // toObject(excludeDisabled?: boolean, caseSensitive?: boolean, multiValue?: boolean, sanitizeKeys?: boolean) { + // const itemObjects = this.list + // .filter(item => { + // if (excludeDisabled) { + // return !item.disabled; + // } + // return true; + // }) + // .map(item => { + // return item.toJSON(); + // }); + // } + + toString() { + const itemStrs = this.list.map(item => item.toString()); + return `[${itemStrs.join(',')}]`; + } + + upsert(item: T): boolean { + const itemIdx = this.indexOf(item); + if (itemIdx >= 0) { + this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)]; + return false; + } + + this.add(item); + return true; + } +} diff --git a/packages/insomnia/src/renderers/utility-process/object-cookies.ts b/packages/insomnia/src/renderers/utility-process/object-cookies.ts new file mode 100644 index 0000000000..89dda64395 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-cookies.ts @@ -0,0 +1,147 @@ +import { Property, PropertyList } from './object-base'; + +export interface CookieDef { + key: string; + value: string; + expires?: Date | string; + maxAge?: Number; + domain?: string; + path?: string; + secure?: Boolean; + httpOnly?: Boolean; + hostOnly?: Boolean; + session?: Boolean; + extensions?: { key: string; value: string }[]; +} + +export class Cookie extends Property { + private def: object; + + constructor(cookieDef: CookieDef | string) { + super(); + this.kind = 'Cookie'; + this.description = 'Cookie'; + + if (typeof cookieDef === 'string') { + this.def = Cookie.parse(cookieDef); + } else { + this.def = cookieDef; + } + } + + static isCookie(obj: Property) { + return obj.kind === 'Cookie'; + } + + static parse(cookieStr: string) { + const parts = cookieStr.split(';'); + + const def: CookieDef = { key: '', value: '' }; + const extensions: { key: string; value: string }[] = []; + + parts.forEach((part, i) => { + const kvParts = part.split('='); + const key = kvParts[0]; + + if (i === 0) { + const value = kvParts.length > 1 ? kvParts[1] : ''; + def.key, def.value = key, value; + } else { + switch (key) { + case 'Expires': + // TODO: it should be timestamp + const expireVal = kvParts.length > 1 ? kvParts[1] : '0'; + def.expires = expireVal; + break; + case 'Max-Age': + let maxAgeVal = 0; + if (kvParts.length > 1) { + maxAgeVal = parseInt(kvParts[1], 10); + } + def.maxAge = maxAgeVal; + break; + case 'Domain': + const domainVal = kvParts.length > 1 ? kvParts[1] : ''; + def.domain = domainVal; + break; + case 'Path': + const pathVal = kvParts.length > 1 ? kvParts[1] : ''; + def.path = pathVal; + break; + case 'Secure': + def.secure = true; + break; + case 'HttpOnly': + def.httpOnly = true; + break; + case 'HostOnly': + def.hostOnly = true; + break; + case 'Session': + def.session = true; + break; + default: + const value = kvParts.length > 1 ? kvParts[1] : ''; + extensions.push({ key, value }); + def.extensions = extensions; + } + } + }); + + return def; + } + + static stringify(cookie: Cookie) { + return cookie.toString(); + } + + static unparseSingle(cookie: Cookie) { + return cookie.toString(); + } + + // TODO: support PropertyList + static unparse(cookies: Cookie[]) { + const cookieStrs = cookies.map(cookie => cookie.toString()); + return cookieStrs.join(';'); + } + + toString = () => { + // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + const cookieDef = this.def as CookieDef; + const kvPair = `${cookieDef.key}=${cookieDef.value};`; + const expires = cookieDef.expires ? `Expires=${cookieDef.expires?.toString()};` : ''; + const maxAge = cookieDef.maxAge ? `Max-Age=${cookieDef.maxAge};` : ''; + const domain = cookieDef.domain ? `Domain=${cookieDef.domain};` : ''; + const path = cookieDef.path ? `Path=${cookieDef.path};` : ''; + const secure = cookieDef.secure ? 'Secure;' : ''; + const httpOnly = cookieDef.httpOnly ? 'HttpOnly;' : ''; + // TODO: SameSite, Partitioned is not suported + + const hostOnly = cookieDef.hostOnly ? 'HostOnly;' : ''; + const session = cookieDef.session ? 'Session;' : ''; + + // TODO: extension key may be conflict with pre-defined keys + const extensions = cookieDef.extensions ? + cookieDef.extensions + .map((kv: { key: string; value: string }) => `${kv.key}=${kv.value}`) + .join(';') : ''; // the last field doesn't have ';' + + return `${kvPair} ${expires} ${maxAge} ${domain} ${path} ${secure} ${httpOnly} ${hostOnly} ${session} ${extensions}`; + }; + + valueOf = () => { + return (this.def as CookieDef).value; + }; +} + +export class CookieList extends PropertyList { + cookies: Cookie[]; + + constructor(parent: object, cookies: Cookie[]) { + super(Cookie, parent.toString(), cookies); + this.cookies = cookies; + } + + // (static) isCookieList(obj) → {Boolean} + +}