feat: add cookie related objects

This commit is contained in:
George He
2023-12-12 22:51:32 +08:00
committed by jackkav
parent f9badb3c28
commit 8261c04769
6 changed files with 672 additions and 54 deletions

View File

@@ -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();
// });

View File

@@ -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',
},
},
},
];

View File

@@ -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 {

View File

@@ -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<string, boolean | number | string>();
@@ -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,
});
};

View File

@@ -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<T extends Property> {
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<T>
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;
}
}

View File

@@ -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<Cookie> {
cookies: Cookie[];
constructor(parent: object, cookies: Cookie[]) {
super(Cookie, parent.toString(), cookies);
this.cookies = cookies;
}
// (static) isCookieList(obj) → {Boolean}
}