More Plugin Hooks (#352)

* Playing around with pre-request hooks

* Added response hooks

* More flow types

* Flow types on wrapper.js

* Flow types on plugin folder

* Basic tests for plugin hooks

* Make DB initilize for all tests no matter what

* Touch
This commit is contained in:
Gregory Schier 2017-07-19 18:55:40 -07:00 committed by GitHub
parent 0902aa6ef9
commit 09c219fb6d
70 changed files with 956 additions and 250 deletions

View File

@ -0,0 +1,7 @@
import * as db from '../common/database';
import * as models from '../models';
export default function () {
// Setup the local database in case it's used
db.init(models.types(), {inMemoryOnly: true}, true);
}

View File

@ -1,4 +1,5 @@
import 'whatwg-fetch';
import beforeEach from './before-each';
const localStorageMock = (function () {
let store = {};
@ -18,3 +19,4 @@ const localStorageMock = (function () {
global.localStorage = localStorageMock;
global.require = require;
global.insomniaBeforeEach = beforeEach;

View File

@ -2,6 +2,7 @@ import * as appPackage from '../package.json';
import * as globalPackage from '../../package.json';
describe('package.json', () => {
beforeEach(global.insomniaBeforeEach);
it('all app dependencies should be same in global', () => {
for (const name of Object.keys(appPackage.dependencies)) {
const expected = globalPackage.dependencies[name];

View File

@ -1,6 +1,7 @@
import * as renderer from '../renderer';
describe('imports', () => {
beforeEach(global.insomniaBeforeEach);
it('ui module should import successfully', () => {
expect(renderer).toBeDefined();
});

View File

@ -1,10 +1,10 @@
import * as analytics from '../index';
import {GA_HOST, getAppVersion, getAppPlatform} from '../../common/constants';
import * as db from '../../common/database';
import {GA_HOST, getAppPlatform, getAppVersion} from '../../common/constants';
import * as models from '../../models';
describe('init()', () => {
beforeEach(() => {
beforeEach(async () => {
await global.insomniaBeforeEach();
window.localStorage = {};
global.document = {
getElementsByTagName () {
@ -16,7 +16,6 @@ describe('init()', () => {
};
}
};
return db.init(models.types(), {inMemoryOnly: true}, true);
});
afterEach(() => {

View File

@ -27,10 +27,12 @@ export async function init (accountId) {
if (window) {
window.addEventListener('error', e => {
trackEvent('Error', 'Uncaught Error');
console.error('Uncaught Error', e);
});
window.addEventListener('unhandledrejection', e => {
trackEvent('Error', 'Uncaught Promise');
console.error('Unhandled Promise', e);
});
}
}

View File

@ -1,5 +1,6 @@
import {FLEXIBLE_URL_REGEX} from '../constants';
describe('URL Regex', () => {
beforeEach(global.insomniaBeforeEach);
it('matches valid URLs', () => {
expect('https://google.com').toMatch(FLEXIBLE_URL_REGEX);
expect('http://google.com').toMatch(FLEXIBLE_URL_REGEX);

View File

@ -2,6 +2,7 @@ import {CookieJar} from 'tough-cookie';
import * as cookieUtils from '../cookies';
describe('jarFromCookies()', () => {
beforeEach(global.insomniaBeforeEach);
it('returns valid cookies', done => {
const jar = cookieUtils.jarFromCookies([{
key: 'foo',
@ -27,6 +28,7 @@ describe('jarFromCookies()', () => {
});
describe('cookiesFromJar()', () => {
beforeEach(global.insomniaBeforeEach);
it('returns valid jar', async () => {
const d = new Date();
const initialCookies = [{
@ -64,6 +66,7 @@ describe('cookiesFromJar()', () => {
});
describe('cookieHeaderValueForUri()', () => {
beforeEach(global.insomniaBeforeEach);
it('gets cookies for valid case', async () => {
const jar = cookieUtils.jarFromCookies([{
key: 'foo',
@ -97,6 +100,7 @@ describe('cookieHeaderValueForUri()', () => {
});
describe('cookieToString()', () => {
beforeEach(global.insomniaBeforeEach);
it('does it\'s thing', async () => {
const jar = cookieUtils.jarFromCookies([{
key: 'foo',

View File

@ -14,6 +14,7 @@ function loadFixture (name) {
}
describe('init()', () => {
beforeEach(global.insomniaBeforeEach);
it('handles being initialized twice', async () => {
await db.init(models.types(), {inMemoryOnly: true});
await db.init(models.types(), {inMemoryOnly: true});
@ -22,6 +23,7 @@ describe('init()', () => {
});
describe('onChange()', () => {
beforeEach(global.insomniaBeforeEach);
it('handles change listeners', async () => {
const doc = {
type: models.request.type,
@ -51,6 +53,7 @@ describe('onChange()', () => {
});
describe('bufferChanges()', () => {
beforeEach(global.insomniaBeforeEach);
it('properly buffers changes', async () => {
const doc = {
type: models.request.type,
@ -88,8 +91,7 @@ describe('bufferChanges()', () => {
});
describe('requestCreate()', () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('creates a valid request', async () => {
const now = Date.now();
@ -124,7 +126,7 @@ describe('requestCreate()', () => {
describe('requestGroupDuplicate()', () => {
beforeEach(async () => {
await db.init(models.types(), {inMemoryOnly: true}, true);
await global.insomniaBeforeEach();
await loadFixture('nestedfolders');
});

View File

@ -1,11 +1,10 @@
import * as harUtils from '../har';
import * as db from '../database';
import * as render from '../render';
import * as models from '../../models';
import {AUTH_BASIC} from '../constants';
describe('exportHarWithRequest()', () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('renders does it correctly', async () => {
const workspace = await models.workspace.create();
const cookies = [{

View File

@ -1,13 +1,9 @@
import * as db from '../database';
import * as models from '../../models';
import * as importUtil from '../import';
import {getAppVersion} from '../constants';
describe('export()', () => {
beforeEach(async () => {
await db.init(models.types(), {inMemoryOnly: true}, true);
});
beforeEach(global.insomniaBeforeEach);
it('succeed with username and password', async () => {
const w = await models.workspace.create({name: 'Workspace'});
const r1 = await models.request.create({name: 'Request', parentId: w._id});

View File

@ -3,7 +3,8 @@ import fs from 'fs';
import LocalStorage from '../local-storage';
describe('LocalStorage()', () => {
beforeEach(() => {
beforeEach(async () => {
await global.insomniaBeforeEach();
jest.useFakeTimers();
// There has to be a better way to reset this...

View File

@ -1,6 +1,7 @@
import * as misc from '../misc';
describe('getBasicAuthHeader()', () => {
beforeEach(global.insomniaBeforeEach);
it('succeed with username and password', () => {
const header = misc.getBasicAuthHeader('user', 'password');
expect(header).toEqual({
@ -35,6 +36,7 @@ describe('getBasicAuthHeader()', () => {
});
describe('hasAuthHeader()', () => {
beforeEach(global.insomniaBeforeEach);
it('finds valid header', () => {
const yes = misc.hasAuthHeader([
{name: 'foo', value: 'bar'},
@ -55,6 +57,7 @@ describe('hasAuthHeader()', () => {
});
describe('generateId()', () => {
beforeEach(global.insomniaBeforeEach);
it('generates a valid ID', () => {
const id = misc.generateId('foo');
expect(id).toMatch(/^foo_[a-z0-9]{32}$/);
@ -67,6 +70,7 @@ describe('generateId()', () => {
});
describe('setDefaultProtocol()', () => {
beforeEach(global.insomniaBeforeEach);
it('no-ops on empty url', () => {
const url = misc.setDefaultProtocol('');
expect(url).toBe('');
@ -94,6 +98,7 @@ describe('setDefaultProtocol()', () => {
});
describe('prepareUrlForSending()', () => {
beforeEach(global.insomniaBeforeEach);
it('does not touch normal url', () => {
const url = misc.prepareUrlForSending('http://google.com');
expect(url).toBe('http://google.com/');
@ -151,6 +156,7 @@ describe('prepareUrlForSending()', () => {
});
describe('filterHeaders()', () => {
beforeEach(global.insomniaBeforeEach);
it('handles bad headers', () => {
expect(misc.filterHeaders(null, null)).toEqual([]);
expect(misc.filterHeaders([], null)).toEqual([]);
@ -164,7 +170,8 @@ describe('filterHeaders()', () => {
});
describe('keyedDebounce()', () => {
beforeEach(() => {
beforeEach(async () => {
await global.insomniaBeforeEach();
jest.useFakeTimers();
// There has to be a better way to reset this...
@ -199,7 +206,8 @@ describe('keyedDebounce()', () => {
});
describe('debounce()', () => {
beforeEach(() => {
beforeEach(async () => {
await global.insomniaBeforeEach();
jest.useFakeTimers();
// There has to be a better way to reset this...

View File

@ -3,6 +3,7 @@ import fs from 'fs';
import path from 'path';
describe('prettify()', () => {
beforeEach(global.insomniaBeforeEach);
const basePath = path.join(__dirname, '../__fixtures__/prettify');
const files = fs.readdirSync(basePath);
for (const file of files) {

View File

@ -1,6 +1,7 @@
import * as querystringUtils from '../querystring';
describe('getBasicAuthHeader()', () => {
beforeEach(global.insomniaBeforeEach);
it('gets joiner for bare URL', () => {
const joiner = querystringUtils.getJoiner('http://google.com');
expect(joiner).toBe('?');
@ -35,6 +36,7 @@ describe('getBasicAuthHeader()', () => {
});
describe('joinUrl()', () => {
beforeEach(global.insomniaBeforeEach);
it('joins bare URL', () => {
const url = querystringUtils.joinUrl(
'http://google.com',
@ -77,6 +79,7 @@ describe('joinUrl()', () => {
});
describe('build()', () => {
beforeEach(global.insomniaBeforeEach);
it('builds simple param', () => {
const str = querystringUtils.build({name: 'foo', value: 'bar??'});
expect(str).toBe('foo=bar%3F%3F');
@ -102,6 +105,7 @@ describe('build()', () => {
});
describe('buildFromParams()', () => {
beforeEach(global.insomniaBeforeEach);
it('builds from params', () => {
const str = querystringUtils.buildFromParams([
{name: 'foo', value: 'bar??'},
@ -127,6 +131,7 @@ describe('buildFromParams()', () => {
});
describe('deconstructToParams()', () => {
beforeEach(global.insomniaBeforeEach);
it('builds from params', () => {
const str = querystringUtils.deconstructToParams(
'foo=bar%3F%3F&hello&hi%20there=bar%3F%3F&=&=val'
@ -152,6 +157,7 @@ describe('deconstructToParams()', () => {
});
describe('deconstructToParams()', () => {
beforeEach(global.insomniaBeforeEach);
it('builds from params not strict', () => {
const str = querystringUtils.deconstructToParams(
'foo=bar%3F%3F&hello&hi%20there=bar%3F%3F&=&=val',

View File

@ -4,6 +4,7 @@ import * as models from '../../models';
jest.mock('electron');
describe('render()', () => {
beforeEach(global.insomniaBeforeEach);
it('renders hello world', async () => {
const rendered = await renderUtils.render('Hello {{ msg }}!', {msg: 'World'});
expect(rendered).toBe('Hello World!');
@ -30,6 +31,7 @@ describe('render()', () => {
});
describe('buildRenderContext()', () => {
beforeEach(global.insomniaBeforeEach);
it('cascades properly', async () => {
const ancestors = [
{
@ -270,6 +272,7 @@ describe('buildRenderContext()', () => {
});
describe('render()', () => {
beforeEach(global.insomniaBeforeEach);
it('correctly renders simple Object', async () => {
const newObj = await renderUtils.render({
foo: '{{ foo }}',

View File

@ -53,7 +53,7 @@ export const GA_ID = 'UA-86416787-1';
export const GA_HOST = 'desktop.insomnia.rest';
export const CHANGELOG_URL = process.env.INSOMNIA_SYNC_URL || 'https://changelog.insomnia.rest/changelog.json';
export const CHANGELOG_PAGE = 'https://insomnia.rest/changelog/';
export const STATUS_CODE_RENDER_FAILED = -333;
export const STATUS_CODE_PLUGIN_ERROR = -222;
export const LARGE_RESPONSE_MB = 5;
export const FLEXIBLE_URL_REGEX = /^(http|https):\/\/[0-9a-zA-Z\-_.]+[/\w.\-+=:\][@%^*&!#?;]*/;
export const CHECK_FOR_UPDATES_INTERVAL = 1000 * 60 * 60 * 3; // 3 hours
@ -253,6 +253,9 @@ export const RESPONSE_CODE_REASONS = {
// Sourced from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
export const RESPONSE_CODE_DESCRIPTIONS = {
// Special
[STATUS_CODE_PLUGIN_ERROR]: 'An Insomnia plugin threw an error which prevented the request from sending',
// 100s
100: 'This interim response indicates that everything so far is OK and that the client should continue with the request or ignore it if it is already finished.',

View File

@ -1,6 +1,7 @@
// @flow
import uuid from 'uuid';
import zlib from 'zlib';
import {join as pathJoin} from 'path';
import {format as urlFormat, parse as urlParse} from 'url';
import {DEBOUNCE_MILLIS, getAppVersion, isDevelopment} from './constants';
import * as querystring from './querystring';
@ -299,3 +300,11 @@ export function compress (inputBuffer: Buffer | string): Buffer {
export function decompress (inputBuffer: Buffer | string): Buffer {
return zlib.gunzipSync(inputBuffer);
}
export function resolveHomePath (p: string): string {
if (p.indexOf('~/') === 0) {
return pathJoin(process.env.HOME || '/', p.slice(1));
} else {
return p;
}
}

View File

@ -1,3 +1,7 @@
// @flow
import type {Request} from '../models/request';
import type {BaseModel} from '../models/index';
import clone from 'clone';
import * as models from '../models';
import {setDefaultProtocol} from './misc';
@ -7,11 +11,27 @@ import * as templating from '../templating';
export const KEEP_ON_ERROR = 'keep';
export const THROW_ON_ERROR = 'throw';
export async function buildRenderContext (ancestors, rootEnvironment, subEnvironment, baseContext = {}) {
if (!Array.isArray(ancestors)) {
ancestors = [];
}
type Cookie = {
domain: string,
path: string,
key: string,
value: string,
expires: number
}
export type RenderedRequest = Request & {
cookies: Array<{name: string, value: string, disabled?: boolean}>,
cookieJar: {
cookies: Array<Cookie>
}
};
export async function buildRenderContext (
ancestors: Array<BaseModel> | null,
rootEnvironment: {data: Object},
subEnvironment: {data: Object},
baseContext: Object = {}
): Object {
const environments = [];
if (rootEnvironment) {
@ -22,11 +42,10 @@ export async function buildRenderContext (ancestors, rootEnvironment, subEnviron
environments.push(subEnvironment.data);
}
for (const doc of ancestors.reverse()) {
if (!doc.environment) {
continue;
for (const doc of (ancestors || []).reverse()) {
if (typeof doc.environment === 'object' && doc.environment !== null) {
environments.push(doc.environment);
}
environments.push(doc.environment);
}
// At this point, environments is a list of environments ordered
@ -34,7 +53,7 @@ export async function buildRenderContext (ancestors, rootEnvironment, subEnviron
// Do an Object.assign, but render each property as it overwrites. This
// way we can keep same-name variables from the parent context.
const renderContext = baseContext;
for (const environment of environments) {
for (const environment: Object of environments) {
// Sort the keys that may have Nunjucks last, so that other keys get
// defined first. Very important if env variables defined in same obj
// (eg. {"foo": "{{ bar }}", "bar": "Hello World!"})
@ -94,11 +113,17 @@ export async function buildRenderContext (ancestors, rootEnvironment, subEnviron
* @param name - name to include in error message
* @return {Promise.<*>}
*/
export async function render (obj, context = {}, blacklistPathRegex = null, errorMode = THROW_ON_ERROR, name = '') {
export async function render<T> (
obj: T,
context: Object = {},
blacklistPathRegex: RegExp | null = null,
errorMode: string = THROW_ON_ERROR,
name: string = ''
): Promise<T> {
// Make a deep copy so no one gets mad :)
const newObj = clone(obj);
async function next (x, path = name) {
async function next (x: any, path: string = name): Promise<any> {
if (blacklistPathRegex && path.match(blacklistPathRegex)) {
return x;
}
@ -116,7 +141,7 @@ export async function render (obj, context = {}, blacklistPathRegex = null, erro
asStr === '[object Undefined]'
) {
// Do nothing to these types
} else if (asStr === '[object String]') {
} else if (typeof x === 'string') {
try {
x = await templating.render(x, {context, path});
@ -135,7 +160,7 @@ export async function render (obj, context = {}, blacklistPathRegex = null, erro
for (let i = 0; i < x.length; i++) {
x[i] = await next(x[i], `${path}[${i}]`);
}
} else if (typeof x === 'object') {
} else if (typeof x === 'object' && x !== null) {
// Don't even try rendering disabled objects
// Note, this logic probably shouldn't be here, but w/e for now
if (x.disabled) {
@ -155,7 +180,11 @@ export async function render (obj, context = {}, blacklistPathRegex = null, erro
return next(newObj);
}
export async function getRenderContext (request, environmentId, ancestors = null) {
export async function getRenderContext (
request: Request,
environmentId: string,
ancestors: Array<BaseModel> | null = null
): Promise<Object> {
if (!request) {
return {};
}
@ -175,7 +204,7 @@ export async function getRenderContext (request, environmentId, ancestors = null
const baseContext = {};
baseContext.getMeta = () => ({
requestId: request._id,
workspaceId: workspace._id
workspaceId: workspace ? workspace._id : 'n/a'
});
// Generate the context we need to render
@ -189,7 +218,10 @@ export async function getRenderContext (request, environmentId, ancestors = null
return context;
}
export async function getRenderedRequest (request, environmentId) {
export async function getRenderedRequest (
request: Request,
environmentId: string
): Promise<RenderedRequest> {
const ancestors = await db.withAncestors(request, [
models.requestGroup.type,
models.workspace.type
@ -225,9 +257,31 @@ export async function getRenderedRequest (request, environmentId) {
// Default the proto if it doesn't exist
renderedRequest.url = setDefaultProtocol(renderedRequest.url);
// Add the yummy cookies
// TODO: Don't deal with cookies in here
renderedRequest.cookieJar = cookieJar;
return {
// Add the yummy cookies
// TODO: Eventually get rid of RenderedRequest type and put these elsewhere
cookieJar,
cookies: [],
return renderedRequest;
// NOTE: Flow doesn't like Object.assign, so we have to do each property manually
// for now to convert Request to RenderedRequest.
_id: renderedRequest._id,
authentication: renderedRequest.authentication,
body: renderedRequest.body,
created: renderedRequest.created,
modified: renderedRequest.modified,
description: renderedRequest.description,
headers: renderedRequest.headers,
metaSortKey: renderedRequest.metaSortKey,
method: renderedRequest.method,
name: renderedRequest.name,
parameters: renderedRequest.parameters,
parentId: renderedRequest.parentId,
settingDisableRenderRequestBody: renderedRequest.settingDisableRenderRequestBody,
settingEncodeUrl: renderedRequest.settingEncodeUrl,
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies,
type: renderedRequest.type,
url: renderedRequest.url
};
}

View File

@ -15,6 +15,7 @@ const v1UUIDs = [
];
const v4UUIDs = [
'cc1dd2ca-4275-747a-a881-99e8efd42403',
'dd2ccc1a-2745-477a-881a-9e8ef9d42403',
'e3e96e5f-dd68-4229-8b66-dee1f0940f3d',
'a262d22b-5fa8-491c-9bd9-58fba03e301e',

View File

@ -1,12 +1,8 @@
import * as db from '../../common/database';
import * as requestModel from '../../models/request';
import * as models from '../index';
describe('init()', () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
});
beforeEach(global.insomniaBeforeEach);
it('contains all required fields', async () => {
Date.now = jest.fn().mockReturnValue(1478795580200);
expect(requestModel.init()).toEqual({
@ -28,16 +24,13 @@ describe('init()', () => {
});
describe('create()', async () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
});
beforeEach(global.insomniaBeforeEach);
it('creates a valid request', async () => {
Date.now = jest.fn().mockReturnValue(1478795580200);
const request = await requestModel.create({name: 'Test Request', parentId: 'fld_124', description: 'A test Request'});
const expected = {
_id: 'req_dd2ccc1a2745477a881a9e8ef9d42403',
_id: 'req_cc1dd2ca4275747aa88199e8efd42403',
created: 1478795580200,
modified: 1478795580200,
parentId: 'fld_124',
@ -68,10 +61,7 @@ describe('create()', async () => {
});
describe('updateMimeType()', async () => {
beforeEach(() => {
return db.init(models.types(), {inMemoryOnly: true}, true);
});
beforeEach(global.insomniaBeforeEach);
it('adds header when does not exist', async () => {
const request = await requestModel.create({name: 'My Request', parentId: 'fld_1'});
expect(request).not.toBeNull();
@ -129,6 +119,7 @@ describe('updateMimeType()', async () => {
});
describe('migrate()', () => {
beforeEach(global.insomniaBeforeEach);
it('migrates basic case', () => {
const original = {
headers: [],

View File

@ -3,7 +3,8 @@ import * as electron from 'electron';
import * as models from '../../models';
describe('migrate()', () => {
beforeEach(() => {
beforeEach(async () => {
await global.insomniaBeforeEach();
Date.now = jest.genMockFunction().mockReturnValue(1234567890);
jest.useFakeTimers();
});

View File

@ -55,7 +55,6 @@ export function init (): BaseResponse {
bytesRead: 0,
elapsedTime: 0,
headers: [],
cookies: [],
timeline: [],
bodyPath: '', // Actual bodies are stored on the filesystem
error: '',

View File

@ -19,7 +19,8 @@ type BaseSettings = {
forceVerticalLayout: boolean,
autoHideMenuBar: boolean,
theme: string,
disableAnalyticsTracking: boolean
disableAnalyticsTracking: boolean,
pluginPath: string
};
export type Settings = BaseModel & Settings;
@ -47,7 +48,8 @@ export function init (): BaseSettings {
forceVerticalLayout: false,
autoHideMenuBar: false,
theme: 'default',
disableAnalyticsTracking: false
disableAnalyticsTracking: false,
pluginPath: ''
};
}

View File

@ -2,6 +2,7 @@ import certificateUrlParse from '../certificate-url-parse';
import {parse as urlParse} from 'url';
describe('certificateUrlParse', () => {
beforeEach(global.insomniaBeforeEach);
it('should return the result of url.parse if no wildcard paths are supplied', () => {
const url = 'https://www.example.org:80/some/resources?query=1&other=2#myfragment';
const expected = urlParse(url);

View File

@ -1,14 +1,12 @@
import * as networkUtils from '../network';
import * as db from '../../common/database';
import {join as pathJoin, resolve as pathResolve} from 'path';
import {getRenderedRequest} from '../../common/render';
import * as models from '../../models';
import {AUTH_BASIC, AUTH_AWS_IAM, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants';
import {AUTH_AWS_IAM, AUTH_BASIC, CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../../common/constants';
import {filterHeaders} from '../../common/misc';
describe('actuallySend()', () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('sends a generic request', async () => {
const workspace = await models.workspace.create();
const settings = await models.settings.create();
@ -403,6 +401,7 @@ describe('actuallySend()', () => {
});
describe('_getAwsAuthHeaders', () => {
beforeEach(global.insomniaBeforeEach);
it('should generate expected headers', () => {
const req = {
authentication: {

View File

@ -1,7 +1,9 @@
import urlMatchesCertHost from '../url-matches-cert-host';
describe('urlMatchesCertHost', () => {
beforeEach(global.insomniaBeforeEach);
describe('when the certificate host has no wildcard', () => {
beforeEach(global.insomniaBeforeEach);
it('should return false if the requested host does not match the certificate host', () => {
const requestUrl = 'https://www.example.org';
const certificateHost = 'https://www.example.com';
@ -52,6 +54,7 @@ describe('urlMatchesCertHost', () => {
});
describe('when using wildcard certificate hosts', () => {
beforeEach(global.insomniaBeforeEach);
it('should return true if the certificate host is only a wildcard', () => {
const requestUrl = 'https://www.example.org/some/resources?query=1';
const certificateHost = '*';
@ -114,6 +117,7 @@ describe('urlMatchesCertHost', () => {
});
describe('when an invalid certificate host is supplied', () => {
beforeEach(global.insomniaBeforeEach);
it('should return false if the certificate host contains invalid characters', () => {
const requestUrl = 'https://www.example.org/some/resources?query=1';
const certificateHost = 'https://example!.org';

View File

@ -1,8 +1,9 @@
// @flow
import type {ResponseTimelineEntry} from '../models/response';
import type {Request, RequestHeader} from '../models/request';
import type {ResponseHeader, ResponseTimelineEntry} from '../models/response';
import type {RequestHeader} from '../models/request';
import type {Workspace} from '../models/workspace';
import type {Settings} from '../models/settings';
import type {RenderedRequest} from '../common/render';
import electron from 'electron';
import mkdirp from 'mkdirp';
@ -14,36 +15,34 @@ import {join as pathJoin} from 'path';
import * as models from '../models';
import * as querystring from '../common/querystring';
import * as util from '../common/misc.js';
import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_DIGEST, AUTH_NTLM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion} from '../common/constants';
import {AUTH_AWS_IAM, AUTH_BASIC, AUTH_DIGEST, AUTH_NTLM, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion, STATUS_CODE_PLUGIN_ERROR} from '../common/constants';
import {describeByteSize, hasAuthHeader, hasContentTypeHeader, hasUserAgentHeader, setDefaultProtocol} from '../common/misc';
import {getRenderedRequest} from '../common/render';
import fs from 'fs';
import * as db from '../common/database';
import * as CACerts from './cacert';
import * as plugins from '../plugins/index';
import * as pluginContexts from '../plugins/context/index';
import {getAuthHeader} from './authentication';
import {cookiesFromJar, jarFromCookies} from '../common/cookies';
import urlMatchesCertHost from './url-matches-cert-host';
import aws4 from 'aws4';
type Cookie = {
domain: string,
path: string,
key: string,
value: string,
expires: number
}
type CookieJar = {
cookies: Array<Cookie>
}
type RenderedRequest = Request & {
cookies: Array<{name: string, value: string, disabled: boolean}>,
cookieJar: CookieJar
export type ResponsePatch = {
statusMessage?: string,
error?: string,
url?: string,
statusCode?: number,
headers?: Array<ResponseHeader>,
elapsedTime?: number,
contentType?: string,
bytesRead?: number,
parentId?: string,
settingStoreCookies?: boolean,
settingSendCookies?: boolean,
timeline?: Array<ResponseTimelineEntry>
};
type ResponsePatch = {};
// Time since user's last keypress to wait before making the request
const MAX_DELAY_TIME = 1000;
@ -69,14 +68,24 @@ export function _actuallySend (
/** Helper function to respond with a success */
function respond (patch: ResponsePatch, bodyBuffer: ?Buffer = null): void {
const response = Object.assign({
const response = Object.assign(({
parentId: renderedRequest._id,
timeline: timeline,
settingSendCookies: renderedRequest.settingSendCookies,
settingStoreCookies: renderedRequest.settingStoreCookies
}, patch);
}: ResponsePatch), patch);
resolve({bodyBuffer, response});
// Apply plugin hooks and don't wait for them and don't throw from them
process.nextTick(async () => {
try {
await _applyResponsePluginHooks(response, bodyBuffer);
} catch (err) {
// TODO: Better error handling here
console.warn('Response plugin failed', err);
}
});
}
/** Helper function to respond with an error */
@ -267,6 +276,10 @@ export function _actuallySend (
].join('\t'));
}
for (const {name, value} of renderedRequest.cookies) {
setOpt(Curl.option.COOKIE, `${name}=${value}`);
}
timeline.push({
name: 'TEXT',
value: 'Enable cookie sending with jar of ' +
@ -421,7 +434,8 @@ export function _actuallySend (
setOpt(Curl.option.PASSWORD, password || '');
} else if (renderedRequest.authentication.type === AUTH_AWS_IAM) {
if (!requestBody) {
return handleError(new Error('AWS authentication not supported for provided body type'));
return handleError(
new Error('AWS authentication not supported for provided body type'));
}
const extraHeaders = _getAwsAuthHeaders(
renderedRequest.authentication.accessKeyId || '',
@ -533,7 +547,7 @@ export function _actuallySend (
const bodyBuffer = Buffer.concat(dataBuffers, dataBuffersLength);
// Return the response data
respond({
const responsePatch = {
headers,
contentType,
statusCode,
@ -541,10 +555,12 @@ export function _actuallySend (
elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) * 1000,
bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD),
url: curl.getInfo(Curl.info.EFFECTIVE_URL)
}, bodyBuffer);
};
// Close the request
this.close();
respond(responsePatch, bodyBuffer);
});
curl.on('error', function (err, code) {
@ -584,27 +600,88 @@ export async function send (requestId: string, environmentId: string) {
// Fetch some things
const request = await models.request.getById(requestId);
const settings = await models.settings.getOrCreate();
// This may throw
const renderedRequest = await getRenderedRequest(request, environmentId);
// Get the workspace for the request
const ancestors = await db.withAncestors(request, [
models.requestGroup.type,
models.workspace.type
]);
if (!request) {
throw new Error(`Failed to find request to send for ${requestId}`);
}
const renderedRequestBeforePlugins = await getRenderedRequest(request, environmentId);
let renderedRequest: RenderedRequest;
try {
renderedRequest = await _applyRequestPluginHooks(renderedRequestBeforePlugins);
} catch (err) {
return {
response: {
url: renderedRequestBeforePlugins.url,
parentId: renderedRequestBeforePlugins._id,
error: err.message,
statusCode: STATUS_CODE_PLUGIN_ERROR,
statusMessage: err.plugin ? `Plugin ${err.plugin}` : 'Plugin',
settingSendCookies: renderedRequestBeforePlugins.settingSendCookies,
settingStoreCookies: renderedRequestBeforePlugins.settingStoreCookies
}
};
}
const workspaceDoc = ancestors.find(doc => doc.type === models.workspace.type);
const workspace = await models.workspace.getById(workspaceDoc ? workspaceDoc._id : 'n/a');
if (!workspace) {
throw new Error(`Failed to find workspace for request: ${requestId}`);
}
// Render succeeded so we're good to go!
return _actuallySend(renderedRequest, workspace, settings);
}
function _getCurlHeader (curlHeadersObj: {[string]: string}, name: string, fallback: any): string {
async function _applyRequestPluginHooks (renderedRequest: RenderedRequest): Promise<RenderedRequest> {
let newRenderedRequest = renderedRequest;
for (const {plugin, hook} of await plugins.getRequestHooks()) {
newRenderedRequest = clone(newRenderedRequest);
const context = {
...pluginContexts.app.init(plugin),
...pluginContexts.request.init(plugin, newRenderedRequest)
};
try {
await hook(context);
} catch (err) {
err.plugin = plugin;
throw err;
}
}
return newRenderedRequest;
}
async function _applyResponsePluginHooks (
response: ResponsePatch,
bodyBuffer: ?Buffer = null
): Promise<void> {
for (const {plugin, hook} of await plugins.getResponseHooks()) {
const context = {
...pluginContexts.app.init(plugin),
...pluginContexts.response.init(plugin, response, bodyBuffer)
};
try {
await hook(context);
} catch (err) {
err.plugin = plugin;
throw err;
}
}
}
function _getCurlHeader (
curlHeadersObj: {[string]: string},
name: string,
fallback: any
): string {
const headerName = Object.keys(curlHeadersObj).find(
n => n.toLowerCase() === name.toLowerCase()
);

View File

@ -11,6 +11,7 @@ const SCOPE = 'scope_123';
const STATE = 'state_123';
describe('authorization_code', () => {
beforeEach(global.insomniaBeforeEach);
it('gets token with JSON and basic auth', async () => {
createBWRedirectMock(`${REDIRECT_URI}?code=code_123&state=${STATE}`);
window.fetch = jest.fn(() => new window.Response(

View File

@ -7,6 +7,7 @@ const CLIENT_SECRET = 'secret_12345456677756343';
const SCOPE = 'scope_123';
describe('client_credentials', () => {
beforeEach(global.insomniaBeforeEach);
it('gets token with JSON and basic auth', async () => {
window.fetch = jest.fn(() => new window.Response(
JSON.stringify({access_token: 'token_123', token_type: 'token_type', scope: SCOPE}),

View File

@ -9,6 +9,7 @@ const SCOPE = 'scope_123';
const STATE = 'state_123';
describe('implicit', () => {
beforeEach(global.insomniaBeforeEach);
it('works in default case', async () => {
createBWRedirectMock(`${REDIRECT_URI}#access_token=token_123&state=${STATE}&foo=bar`);

View File

@ -9,6 +9,7 @@ const PASSWORD = 'password';
const SCOPE = 'scope_123';
describe('password', () => {
beforeEach(global.insomniaBeforeEach);
it('gets token with JSON and basic auth', async () => {
window.fetch = jest.fn(() => new window.Response(
JSON.stringify({access_token: 'token_123', token_type: 'token_type', scope: SCOPE}),

View File

@ -0,0 +1,36 @@
import * as plugin from '../app';
import * as modals from '../../../ui/components/modals';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
describe('init()', () => {
beforeEach(global.insomniaBeforeEach);
it('initializes correctly', () => {
const result = plugin.init({name: PLUGIN});
expect(Object.keys(result)).toEqual(['app']);
expect(Object.keys(result.app)).toEqual(['alert', 'getPath']);
});
});
describe('app.alert()', () => {
beforeEach(global.insomniaBeforeEach);
it('shows alert with message', async () => {
modals.showAlert = jest.fn().mockReturnValue('dummy-return-value');
const result = plugin.init(PLUGIN);
// Make sure it returns result of showAlert()
expect(result.app.alert()).toBe('dummy-return-value');
expect(result.app.alert('My message')).toBe('dummy-return-value');
// Make sure it passes correct arguments
expect(modals.showAlert.mock.calls).toEqual([
[{message: '', title: 'Plugin my-plugin'}],
[{message: 'My message', title: 'Plugin my-plugin'}]
]);
});
});

View File

@ -0,0 +1,99 @@
import * as plugin from '../request';
import * as models from '../../../models';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
describe('init()', () => {
beforeEach(async () => {
await global.insomniaBeforeEach();
await models.workspace.create({_id: 'wrk_1', name: 'My Workspace'});
await models.request.create({_id: 'req_1', parentId: 'wrk_1', name: 'My Request'});
});
it('initializes correctly', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'));
expect(Object.keys(result)).toEqual(['request']);
expect(Object.keys(result.request)).toEqual([
'getId',
'getName',
'getUrl',
'getMethod',
'getHeader',
'hasHeader',
'removeHeader',
'setHeader',
'addHeader',
'setCookie'
]);
});
it('fails to initialize without request', () => {
expect(() => plugin.init(PLUGIN))
.toThrowError('contexts.request initialized without request');
});
});
describe('request.*', () => {
beforeEach(async () => {
await global.insomniaBeforeEach();
await models.workspace.create({_id: 'wrk_1', name: 'My Workspace'});
await models.request.create({
_id: 'req_1',
parentId: 'wrk_1',
name: 'My Request',
headers: [
{name: 'hello', value: 'world'},
{name: 'Content-Type', value: 'application/json'}
]
});
});
it('works for basic getters', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'));
expect(result.request.getId()).toBe('req_1');
expect(result.request.getName()).toBe('My Request');
expect(result.request.getUrl()).toBe('');
expect(result.request.getMethod()).toBe('GET');
});
it('works for headers', async () => {
const result = plugin.init(PLUGIN, await models.request.getById('req_1'));
// getHeader()
expect(result.request.getHeader('content-type')).toBe('application/json');
expect(result.request.getHeader('CONTENT-TYPE')).toBe('application/json');
expect(result.request.getHeader('does-not-exist')).toBe(null);
expect(result.request.hasHeader('Content-Type')).toBe(true);
// setHeader()
result.request.setHeader('content-type', 'text/plain');
expect(result.request.getHeader('Content-Type')).toBe('text/plain');
// addHeader()
result.request.addHeader('content-type', 'new/type');
result.request.addHeader('something-else', 'foo');
expect(result.request.getHeader('Content-Type')).toBe('text/plain');
expect(result.request.getHeader('something-else')).toBe('foo');
// removeHeader()
result.request.removeHeader('content-type');
expect(result.request.getHeader('Content-Type')).toBe(null);
expect(result.request.hasHeader('Content-Type')).toBe(false);
});
it('works for cookies', async () => {
const request = await models.request.getById('req_1');
request.cookies = []; // Because the plugin technically needs a RenderedRequest
const result = plugin.init(PLUGIN, request);
result.request.setCookie('foo', 'bar');
result.request.setCookie('foo', 'baz');
expect(request.cookies).toEqual([{name: 'foo', value: 'baz'}]);
});
});

View File

@ -0,0 +1,58 @@
import * as plugin from '../response';
const PLUGIN = {
name: 'my-plugin',
version: '1.0.0',
directory: '/plugins/my-plugin',
module: {}
};
describe('init()', () => {
beforeEach(global.insomniaBeforeEach);
it('initializes correctly', async () => {
const result = plugin.init(PLUGIN, {});
expect(Object.keys(result)).toEqual(['response']);
expect(Object.keys(result.response)).toEqual([
'getRequestId',
'getStatusCode',
'getStatusMessage',
'getBytesRead',
'getTime',
'getBody'
]);
});
it('fails to initialize without response', () => {
expect(() => plugin.init(PLUGIN))
.toThrowError('contexts.response initialized without response');
});
});
describe('response.*', () => {
beforeEach(global.insomniaBeforeEach);
it('works for basic and full response', async () => {
const response = {
parentId: 'req_1',
url: 'https://insomnia.rest',
statusCode: 200,
statusMessage: 'OK',
bytesRead: 123,
elapsedTime: 321
};
const result = plugin.init(PLUGIN, response, Buffer.from('Hello World!'));
expect(result.response.getRequestId()).toBe('req_1');
expect(result.response.getStatusCode()).toBe(200);
expect(result.response.getBytesRead()).toBe(123);
expect(result.response.getTime()).toBe(321);
expect(result.response.getBody().toString()).toBe('Hello World!');
});
it('works for basic and empty response', async () => {
const result = plugin.init(PLUGIN, {});
expect(result.response.getRequestId()).toBe('');
expect(result.response.getStatusCode()).toBe(0);
expect(result.response.getBytesRead()).toBe(0);
expect(result.response.getTime()).toBe(0);
expect(result.response.getBody()).toBeNull();
});
});

View File

@ -0,0 +1,22 @@
// @flow
import type {Plugin} from '../';
import * as electron from 'electron';
import {showAlert} from '../../ui/components/modals/index';
export function init (plugin: Plugin): {app: Object} {
return {
app: {
alert (message: string): Promise<void> {
return showAlert({title: `Plugin ${plugin.name}`, message: message || ''});
},
getPath (name: string): string {
switch (name.toLowerCase()) {
case 'desktop':
return electron.remote.app.getPath('desktop');
default:
throw new Error(`Unknown path name ${name}`);
}
}
}
};
}

View File

@ -0,0 +1,8 @@
// @flow
import * as _app from './app';
import * as _request from './request';
import * as _response from './response';
export const app = _app;
export const request = _request;
export const response = _response;

View File

@ -0,0 +1,74 @@
// @flow
import type {Plugin} from '../';
import type {RenderedRequest} from '../../common/render';
import * as misc from '../../common/misc';
export function init (plugin: Plugin, renderedRequest: RenderedRequest): {request: Object} {
if (!renderedRequest) {
throw new Error('contexts.request initialized without request');
}
return {
request: {
getId (): string {
return renderedRequest._id;
},
getName (): string {
return renderedRequest.name;
},
getUrl (): string {
// TODO: Get full URL, including querystring
return renderedRequest.url;
},
getMethod (): string {
return renderedRequest.method;
},
getHeader (name: string): string | null {
const headers = misc.filterHeaders(renderedRequest.headers, name);
if (headers.length) {
// Use the last header if there are multiple of the same
const header = headers[headers.length - 1];
return header.value || '';
} else {
return null;
}
},
hasHeader (name: string): boolean {
return this.getHeader(name) !== null;
},
removeHeader (name: string): void {
const headers = misc.filterHeaders(renderedRequest.headers, name);
renderedRequest.headers = renderedRequest.headers.filter(
h => !headers.includes(h)
);
},
setHeader (name: string, value: string): void {
const header = misc.filterHeaders(renderedRequest.headers, name)[0];
if (header) {
header.value = value;
} else {
this.addHeader(name, value);
}
},
addHeader (name: string, value: string): void {
const header = misc.filterHeaders(renderedRequest.headers, name)[0];
if (!header) {
renderedRequest.headers.push({name, value});
}
},
setCookie (name: string, value: string): void {
const cookie = renderedRequest.cookies.find(c => c.name === name);
if (cookie) {
cookie.value = value;
} else {
renderedRequest.cookies.push({name, value});
}
}
// NOTE: For these to make sense, we'd need to account for cookies in the jar as well
// addCookie (name: string, value: string): void {}
// getCookie (name: string): string | null {}
// removeCookie (name: string): void {}
}
};
}

View File

@ -0,0 +1,48 @@
// @flow
import type {Plugin} from '../';
type MaybeResponse = {
parentId?: string,
statusCode?: number,
statusMessage?: string,
bytesRead?: number,
elapsedTime?: number,
}
export function init (
plugin: Plugin,
response: MaybeResponse,
bodyBuffer: Buffer | null = null
): {response: Object} {
if (!response) {
throw new Error('contexts.response initialized without response');
}
return {
response: {
// TODO: Make this work. Right now it doesn't because _id is
// not generated in network.js
// getId () {
// return response.parentId;
// },
getRequestId (): string {
return response.parentId || '';
},
getStatusCode (): number {
return response.statusCode || 0;
},
getStatusMessage (): string {
return response.statusMessage || '';
},
getBytesRead (): number {
return response.bytesRead || 0;
},
getTime (): number {
return response.elapsedTime || 0;
},
getBody (): Buffer | null {
return bodyBuffer;
}
}
};
}

View File

@ -1,44 +1,84 @@
// @flow
import mkdirp from 'mkdirp';
import * as models from '../models';
import fs from 'fs';
import path from 'path';
import {PLUGIN_PATHS} from '../common/constants';
import {render} from '../templating';
import skeletonPackageJson from './skeleton/package.json.js';
import skeletonPluginJs from './skeleton/plugin.js.js';
import {resolveHomePath} from '../common/misc';
let plugins = null;
export type Plugin = {
name: string,
version: string,
directory: string,
module: *
};
export async function init () {
// Force plugins to load.
getPlugins(true);
export type TemplateTag = {
plugin: string,
templateTag: Function
}
export function getPlugins (force = false) {
if (!plugins || force) {
// Make sure the directories exist
export type RequestHook = {
plugin: Plugin,
hook: Function
}
export type ResponseHook = {
plugin: Plugin,
hook: Function
}
let plugins: ?Array<Plugin> = null;
export async function init (): Promise<void> {
// Force plugins to load.
await getPlugins(true);
}
export async function getPlugins (force: boolean = false): Promise<Array<Plugin>> {
if (force) {
plugins = null;
}
if (!plugins) {
const settings = await models.settings.getOrCreate();
const extraPaths = settings.pluginPath.split(':').filter(p => p).map(resolveHomePath);
const allPaths = [...PLUGIN_PATHS, ...extraPaths];
// Make sure the default directories exist
for (const p of PLUGIN_PATHS) {
mkdirp.sync(p);
}
plugins = [];
for (const p of PLUGIN_PATHS) {
for (const p of allPaths) {
for (const dir of fs.readdirSync(p)) {
if (dir.indexOf('.') === 0) {
continue;
}
const moduleDirectory = path.join(p, dir);
const modulePath = path.join(p, dir);
const packageJSONPath = path.join(modulePath, 'package.json');
// Use global.require() instead of require() because Webpack wraps require()
const pluginJson = global.require(path.join(moduleDirectory, 'package.json'));
const module = global.require(moduleDirectory);
delete global.require.cache[global.require.resolve(packageJSONPath)];
const pluginJson = global.require(packageJSONPath);
// Delete require cache entry and re-require
delete global.require.cache[global.require.resolve(modulePath)];
const module = global.require(modulePath);
plugins.push({
name: pluginJson.name,
version: pluginJson.version || '0.0.0',
directory: moduleDirectory,
directory: modulePath,
module
});
// console.log(`[plugin] Loaded ${modulePath}`);
}
}
}
@ -46,7 +86,7 @@ export function getPlugins (force = false) {
return plugins;
}
export async function createPlugin (displayName) {
export async function createPlugin (displayName: string): Promise<void> {
// Create root plugin dir
const name = displayName.replace(/\s/g, '-').toLowerCase();
const dir = path.join(PLUGIN_PATHS[0], name);
@ -58,12 +98,42 @@ export async function createPlugin (displayName) {
fs.writeFileSync(path.join(dir, 'package.json'), renderedPackageJson);
}
export function getTemplateTags () {
export async function getTemplateTags (): Promise<Array<TemplateTag>> {
console.log('GETTING TEMPLATE TAGS');
let extensions = [];
for (const plugin of getPlugins()) {
for (const plugin of await getPlugins()) {
const templateTags = plugin.module.templateTags || [];
extensions = [...extensions, ...templateTags];
extensions = [
...extensions,
...templateTags.map(tt => ({plugin: plugin.name, templateTag: tt}))
];
}
return extensions;
}
export async function getRequestHooks (): Promise<Array<RequestHook>> {
let functions = [];
for (const plugin of await getPlugins()) {
const moreFunctions = plugin.module.requestHooks || [];
functions = [
...functions,
...moreFunctions.map(hook => ({plugin, hook}))
];
}
return functions;
}
export async function getResponseHooks (): Promise<Array<ResponseHook>> {
let functions = [];
for (const plugin of await getPlugins()) {
const moreFunctions = plugin.module.responseHooks || [];
functions = [
...functions,
...moreFunctions.map(hook => ({plugin, hook}))
];
}
return functions;
}

View File

@ -1,4 +1,10 @@
module.exports = `
module.exports.preRequestHooks = [
context => {
context.addHeader('foo', 'bar');
}
];
module.exports.templateTags = [{
name: 'hello',
displayName: 'Say Hello',

View File

@ -1,6 +1,7 @@
import * as crypt from '../crypt';
describe('deriveKey()', () => {
beforeEach(global.insomniaBeforeEach);
it('derives a key properly', async () => {
const result = await crypt.deriveKey('Password', 'email', 'salt');
@ -11,6 +12,7 @@ describe('deriveKey()', () => {
});
describe('encryptRSA', () => {
beforeEach(global.insomniaBeforeEach);
it('encrypts and decrypts', () => {
const resultEncrypted = crypt.encryptAES('rawkey', 'Hello World!', 'additional data');
const resultDecrypted = crypt.decryptAES('rawkey', resultEncrypted);

View File

@ -7,6 +7,8 @@ import * as crypt from '../crypt';
describe('Test push/pull behaviour', () => {
beforeEach(async () => {
await global.insomniaBeforeEach();
// Reset some things
sync._testReset();
await _setSessionData();
@ -15,7 +17,6 @@ describe('Test push/pull behaviour', () => {
// Init sync and storage
const config = {inMemoryOnly: true, autoload: false, filename: null};
await syncStorage.initDB(config, true);
await db.init(models.types(), config, true);
// Add some data
await models.workspace.create({_id: 'wrk_1', name: 'Workspace 1'});
@ -226,6 +227,8 @@ describe('Test push/pull behaviour', () => {
describe('Integration tests for creating Resources and pushing', () => {
beforeEach(async () => {
await global.insomniaBeforeEach();
// Reset some things
await _setSessionData();
sync._testReset();
@ -237,7 +240,6 @@ describe('Integration tests for creating Resources and pushing', () => {
// Init storage
const config = {inMemoryOnly: true, autoload: false, filename: null};
await syncStorage.initDB(config, true);
await db.init(models.types(), config, true);
// Add some data
await models.workspace.create({_id: 'wrk_empty', name: 'Workspace Empty'});

View File

@ -1,6 +1,7 @@
import * as utils from '../utils';
describe('getKeys()', () => {
beforeEach(global.insomniaBeforeEach);
it('flattens complex object', () => {
const obj = {
foo: 'bar',
@ -48,6 +49,7 @@ describe('getKeys()', () => {
});
describe('tokenizeTag()', () => {
beforeEach(global.insomniaBeforeEach);
it('tokenizes complex tag', () => {
const actual = utils.tokenizeTag(
`{% name bar, "baz \\"qux\\"" , 1 + 5 | default("foo") %}`
@ -149,6 +151,7 @@ describe('tokenizeTag()', () => {
});
describe('unTokenizeTag()', () => {
beforeEach(global.insomniaBeforeEach);
it('untokenizes a tag', () => {
const tagStr = `{% name bar, "baz \\"qux\\"" , 1 + 5, 'hi' %}`;

View File

@ -19,6 +19,7 @@ function assertTemplateFails (txt, expected) {
}
describe('Base64EncodeExtension', () => {
beforeEach(global.insomniaBeforeEach);
it('encodes nothing', assertTemplate("{% base64 'encode' %}", ''));
it('encodes something', assertTemplate("{% base64 'encode', 'my string' %}", 'bXkgc3RyaW5n'));
it('decodes nothing', assertTemplate("{% base64 'decode' %}", ''));

View File

@ -23,6 +23,7 @@ const secondsRe = /^\d{10}$/;
const millisRe = /^\d{13}$/;
describe('NowExtension', () => {
beforeEach(global.insomniaBeforeEach);
it('renders default ISO', assertTemplate('{% now %}', isoRe));
it('renders ISO-8601', assertTemplate('{% now "ISO-8601" %}', isoRe));
it('renders seconds', assertTemplate('{% now "seconds" %}', secondsRe));

View File

@ -1,12 +1,10 @@
import * as templating from '../../index';
import * as db from '../../../common/database';
import * as models from '../../../models';
import {cookiesFromJar, jarFromCookies} from '../../../common/cookies';
import {getRenderContext} from '../../../common/render';
describe('RequestExtension cookie', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('should get cookie by name', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
@ -33,8 +31,7 @@ describe('RequestExtension cookie', async () => {
});
describe('RequestExtension url', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('should get url', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});
@ -68,8 +65,7 @@ describe('RequestExtension url', async () => {
});
describe('RequestExtension header', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('should get url', async () => {
// Create necessary models
const workspace = await models.workspace.create({name: 'Workspace'});

View File

@ -1,9 +1,8 @@
import * as templating from '../../index';
import * as db from '../../../common/database';
import * as models from '../../../models';
describe('ResponseExtension General', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('fails on no responses', async () => {
const request = await models.request.create({parentId: 'foo'});
@ -39,8 +38,7 @@ describe('ResponseExtension General', async () => {
});
describe('ResponseExtension JSONPath', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('renders basic response "body", query', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({
@ -115,8 +113,7 @@ describe('ResponseExtension JSONPath', async () => {
});
describe('ResponseExtension XPath', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('renders basic response "body", query', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({
@ -191,8 +188,7 @@ describe('ResponseExtension XPath', async () => {
});
describe('ResponseExtension Header', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('renders basic response "header"', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({
@ -237,8 +233,7 @@ describe('ResponseExtension Header', async () => {
});
describe('ResponseExtension Raw', async () => {
beforeEach(() => db.init(models.types(), {inMemoryOnly: true}, true));
beforeEach(global.insomniaBeforeEach);
it('renders basic response "header"', async () => {
const request = await models.request.create({parentId: 'foo'});
await models.response.create({

View File

@ -10,5 +10,6 @@ function assertTemplate (txt, expected) {
const millisRe = /^\d{13}$/;
describe('TimestampExtension', () => {
beforeEach(global.insomniaBeforeEach);
it('renders basic', assertTemplate('{% timestamp %}', millisRe));
});

View File

@ -19,6 +19,7 @@ function assertTemplateFails (txt, expected) {
}
describe('UuidExtension', () => {
beforeEach(global.insomniaBeforeEach);
it('renders default v4', assertTemplate('{% uuid %}', 'dd2ccc1a-2745-477a-881a-9e8ef9d42403'));
it('renders 4', assertTemplate('{% uuid "4" %}', 'e3e96e5f-dd68-4229-8b66-dee1f0940f3d'));
it('renders 4 num', assertTemplate('{% uuid 4 %}', 'a262d22b-5fa8-491c-9bd9-58fba03e301e'));

View File

@ -16,6 +16,10 @@ const DEFAULT_EXTENSIONS = [
responseExtension
];
export function all () {
return [...DEFAULT_EXTENSIONS, ...plugins.getTemplateTags()];
export async function all () {
const templateTags = await plugins.getTemplateTags();
return [
...DEFAULT_EXTENSIONS,
...templateTags.map(p => p.templateTag)
];
}

View File

@ -41,8 +41,8 @@ export function render (text: string, config: Object = {}): Promise<string> {
const path = config.path || null;
const renderMode = config.renderMode || RENDER_ALL;
return new Promise((resolve, reject) => {
const nj = getNunjucks(renderMode);
return new Promise(async (resolve, reject) => {
const nj = await getNunjucks(renderMode);
nj.renderString(text, context, (err, result) => {
if (err) {
@ -85,8 +85,8 @@ export function reload (): void {
/**
* Get definitions of template tags
*/
export function getTagDefinitions () {
const env = getNunjucks();
export async function getTagDefinitions (): Promise<Array<NunjucksTag>> {
const env = await getNunjucks(RENDER_ALL);
return Object.keys(env.extensions)
.map(k => env.extensions[k])
@ -100,7 +100,7 @@ export function getTagDefinitions () {
}));
}
function getNunjucks (renderMode) {
async function getNunjucks (renderMode: string) {
if (renderMode === RENDER_VARS && nunjucksVariablesOnly) {
return nunjucksVariablesOnly;
}
@ -148,7 +148,7 @@ function getNunjucks (renderMode) {
const nj = nunjucks.configure(config);
const allExtensions = extensions.all();
const allExtensions = await extensions.all();
for (let i = 0; i < allExtensions.length; i++) {
const ext = allExtensions[i];
ext.priority = ext.priority || i * 100;
@ -158,7 +158,6 @@ function getNunjucks (renderMode) {
// Hidden helper filter to debug complicated things
// eg. `{{ foo | urlencode | debug | upper }}`
nj.addFilter('debug', o => {
console.log('DEBUG', {o}, JSON.stringify(o));
return o;
});
}

View File

@ -410,9 +410,9 @@ class CodeEditor extends PureComponent {
};
// Only allow tags if we have variables too
getTags = () => {
getTags = async () => {
const expandedTags = [];
for (const tagDef of getTagDefinitions()) {
for (const tagDef of await getTagDefinitions()) {
if (tagDef.args[0].type !== 'enum') {
expandedTags.push(tagDef);
continue;

View File

@ -225,7 +225,7 @@ async function _updateElementText (render, mark, text) {
if (tagMatch) {
const tagData = tokenizeTag(str);
const tagDefinition = getTagDefinitions().find(d => d.name === tagData.name);
const tagDefinition = (await getTagDefinitions()).find(d => d.name === tagData.name);
if (tagDefinition) {
// Try rendering these so we can show errors if needed

View File

@ -21,7 +21,7 @@ class MethodDropdown extends PureComponent {
// Prompt user for the method
showPrompt({
defaultValue: this.props.method,
headerName: 'HTTP Method',
title: 'HTTP Method',
submitName: 'Done',
upperCase: true,
selectText: true,

View File

@ -18,7 +18,7 @@ class RequestGroupActionsDropdown extends PureComponent {
const {requestGroup} = this.props;
showPrompt({
headerName: 'Rename Folder',
title: 'Rename Folder',
defaultValue: requestGroup.name,
onComplete: name => {
models.requestGroup.update(requestGroup, {name});

View File

@ -63,7 +63,7 @@ class WorkspaceDropdown extends PureComponent {
_handleWorkspaceCreate (noTrack) {
showPrompt({
headerName: 'Create New Workspace',
title: 'Create New Workspace',
defaultValue: 'My Workspace',
submitName: 'Create',
selectText: true,

View File

@ -13,7 +13,7 @@ class PromptModal extends PureComponent {
constructor (props) {
super(props);
this.state = {
headerName: 'Not Set',
title: 'Not Set',
defaultValue: '',
submitName: 'Not Set',
selectText: false,
@ -60,7 +60,7 @@ class PromptModal extends PureComponent {
show (options) {
const {
headerName,
title,
defaultValue,
submitName,
selectText,
@ -87,7 +87,7 @@ class PromptModal extends PureComponent {
this._onDeleteHint = onDeleteHint;
this.setState({
headerName,
title,
defaultValue,
submitName,
selectText,
@ -125,7 +125,7 @@ class PromptModal extends PureComponent {
render () {
const {
submitName,
headerName,
title,
hint,
inputType,
placeholder,
@ -153,7 +153,7 @@ class PromptModal extends PureComponent {
return (
<Modal ref={this._setModalRef}>
<ModalHeader>{headerName}</ModalHeader>
<ModalHeader>{title}</ModalHeader>
<ModalBody className="wide">
<form onSubmit={this._handleSubmit} className="wide pad">
<div className="form-control form-control--outlined form-control--wide">

View File

@ -51,7 +51,7 @@ class WorkspaceShareSettingsModal extends PureComponent {
_handleShareWithTeam (team) {
showPrompt({
headerName: 'Share Workspace',
title: 'Share Workspace',
label: 'Confirm password to share workspace',
placeholder: '•••••••••••••••••',
submitName: 'Share with Team',

View File

@ -134,7 +134,7 @@ class RequestUrlBar extends PureComponent {
_handleSendAfterDelay () {
showPrompt({
inputType: 'decimal',
headerName: 'Send After Delay',
title: 'Send After Delay',
label: 'Delay in seconds',
defaultValue: 3,
submitName: 'Start',
@ -151,7 +151,7 @@ class RequestUrlBar extends PureComponent {
_handleSendOnInterval () {
showPrompt({
inputType: 'decimal',
headerName: 'Send on Interval',
title: 'Send on Interval',
label: 'Interval in seconds',
defaultValue: 3,
submitName: 'Start',

View File

@ -202,6 +202,23 @@ class General extends PureComponent {
</label>
</div>
</div>
<hr className="pad-top"/>
<h2>Plugins</h2>
<div className="form-control form-control--outlined">
<label>
Additional Plugin Path <HelpTooltip>Tell Insomnia to look for plugins in a different
directory</HelpTooltip>
<input placeholder="~/.insomnia:/other/path"
name="pluginPath"
type="text"
defaultValue={settings.pluginPath}
onChange={this._handleUpdateSetting}/>
</label>
</div>
<br/>
</div>
);

View File

@ -8,7 +8,7 @@ import {showPrompt} from '../modals/index';
class ImportExport extends PureComponent {
_handleImportUri () {
showPrompt({
headerName: 'Import Data from URL',
title: 'Import Data from URL',
submitName: 'Fetch and Import',
label: 'URL',
placeholder: 'https://website.com/insomnia-import.json',

View File

@ -1,29 +1,50 @@
import React, {PureComponent} from 'react';
// @flow
import type {Plugin} from '../../../plugins/index';
import {createPlugin, getPlugins} from '../../../plugins/index';
import React from 'react';
import autobind from 'autobind-decorator';
import * as electron from 'electron';
import {createPlugin, getPlugins} from '../../../plugins/index';
import Button from '../base/button';
import CopyButton from '../base/copy-button';
import {showPrompt} from '../modals/index';
import {trackEvent} from '../../../analytics/index';
import {reload} from '../../../templating/index';
type DefaultProps = void;
type Props = void;
type State = {
plugins: Array<Plugin>,
npmPluginValue: string
};
@autobind
class Plugins extends PureComponent {
constructor (props) {
class Plugins extends React.PureComponent<DefaultProps, Props, State> {
props: Props;
state: State;
constructor (props: Props) {
super(props);
this.state = {
plugins: getPlugins(true)
plugins: [],
npmPluginValue: ''
};
}
_handleOpenDirectory (directory) {
_handleAddNpmPluginChange (e: Event & {target: HTMLButtonElement}) {
this.setState({npmPluginValue: e.target.value});
}
_handleAddFromNpm () {
console.log('ADD FROM NPM', this.state.npmPluginValue);
}
_handleOpenDirectory (directory: string) {
electron.remote.shell.showItemInFolder(directory);
}
_handleGeneratePlugin () {
showPrompt({
headerName: 'Plugin Name',
title: 'Plugin Name',
defaultValue: 'My Plugin',
submitName: 'Generate Plugin',
selectText: true,
@ -31,21 +52,25 @@ class Plugins extends PureComponent {
label: 'Plugin Name',
onComplete: async name => {
await createPlugin(name);
this._handleRefreshPlugins();
await this._handleRefreshPlugins();
trackEvent('Plugins', 'Generate');
}
});
}
_handleRefreshPlugins () {
async _handleRefreshPlugins () {
// Get and reload plugins
const plugins = getPlugins(true);
const plugins = await getPlugins(true);
reload();
this.setState({plugins});
trackEvent('Plugins', 'Refresh');
}
componentDidMount () {
this._handleRefreshPlugins();
}
render () {
const {plugins} = this.state;
@ -85,19 +110,32 @@ class Plugins extends PureComponent {
</tbody>
</table>
<p className="text-right">
<button className="btn btn--clicky" onClick={this._handleRefreshPlugins}>
Reload Plugins
</button>
{' '}
<button className="btn btn--clicky" onClick={this._handleGeneratePlugin}>
Generate New Plugin
</button>
</p>
<div className="form-row">
<div className="form-control form-control--outlined">
<input onChange={this._handleAddNpmPluginChange}
type="text"
placeholder="insomnia-foo-bar"/>
</div>
<div className="form-control width-auto">
<button className="btn btn--clicky" onClick={this._handleAddFromNpm}>
Add From NPM
</button>
</div>
<div className="form-control width-auto">
<button className="btn btn--clicky" onClick={this._handleRefreshPlugins}>
Reload
</button>
</div>
<div className="form-control width-auto">
<button className="btn btn--clicky" onClick={this._handleGeneratePlugin}>
New Plugin
</button>
</div>
</div>
</div>
);
}
}
Plugins.propTypes = {};
export default Plugins;

View File

@ -40,6 +40,7 @@ class StatusTag extends PureComponent {
default:
colorClass = 'bg-danger';
genericStatusMessage = 'UNKNOWN';
statusCodeToDisplay = '';
break;
}

View File

@ -1,12 +1,13 @@
import React, {PropTypes, PureComponent} from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import clone from 'clone';
import * as templating from '../../../templating';
import * as templateUtils from '../../../templating/utils';
import * as db from '../../../common/database';
import * as models from '../../../models';
import HelpTooltip from '../help-tooltip';
import {fnOrString} from '../../../common/misc';
import {delay, fnOrString} from '../../../common/misc';
import {trackEvent} from '../../../analytics/index';
@autobind
@ -14,29 +15,31 @@ class TagEditor extends PureComponent {
constructor (props) {
super(props);
const activeTagData = templateUtils.tokenizeTag(props.defaultValue);
const tagDefinitions = templating.getTagDefinitions();
const activeTagDefinition = tagDefinitions.find(d => d.name === activeTagData.name);
// Edit tags raw that we don't know about
if (!activeTagDefinition) {
activeTagData.rawValue = props.defaultValue;
}
this.state = {
activeTagData,
activeTagDefinition,
loadingDocs: true,
activeTagData: null,
activeTagDefinition: null,
tagDefinitions: [],
loadingDocs: false,
allDocs: {},
rendering: true,
preview: '',
error: ''
};
}
async componentWillMount () {
async componentDidMount () {
const activeTagData = templateUtils.tokenizeTag(this.props.defaultValue);
const tagDefinitions = await templating.getTagDefinitions();
const activeTagDefinition = tagDefinitions.find(d => d.name === activeTagData.name);
// Edit tags raw that we don't know about
if (!activeTagDefinition) {
activeTagData.rawValue = this.props.defaultValue;
}
await this._refreshModels(this.props.workspace);
await this._update(this.state.activeTagDefinition, this.state.activeTagData, true);
await this._update(tagDefinitions, activeTagDefinition, activeTagData, true);
}
componentWillReceiveProps (nextProps) {
@ -47,6 +50,15 @@ class TagEditor extends PureComponent {
}
}
_handleRefresh () {
this._update(
this.state.tagDefinitions,
this.state.activeTagDefinition,
this.state.activeTagData,
true
);
}
async _refreshModels (workspace) {
const allDocs = {};
for (const type of models.types()) {
@ -61,12 +73,12 @@ class TagEditor extends PureComponent {
}
_updateArg (argValue, argIndex) {
const {activeTagData, activeTagDefinition} = this.state;
const {tagDefinitions, activeTagData, activeTagDefinition} = this.state;
const tagData = clone(activeTagData);
tagData.args[argIndex].value = argValue;
this._update(activeTagDefinition, tagData, false);
this._update(tagDefinitions, activeTagDefinition, tagData, false);
}
_handleChange (e) {
@ -81,18 +93,18 @@ class TagEditor extends PureComponent {
}
_handleChangeCustomArg (e) {
const {activeTagData, activeTagDefinition} = this.state;
const {tagDefinitions, activeTagData, activeTagDefinition} = this.state;
const tagData = clone(activeTagData);
tagData.rawValue = e.target.value;
this._update(activeTagDefinition, tagData, false);
this._update(tagDefinitions, activeTagDefinition, tagData, false);
}
_handleChangeTag (e) {
async _handleChangeTag (e) {
const name = e.target.value;
const tagDefinition = templating.getTagDefinitions().find(d => d.name === name);
this._update(tagDefinition, false);
const tagDefinition = (await templating.getTagDefinitions()).find(d => d.name === name);
this._update(this.state.tagDefinitions, tagDefinition, false);
trackEvent('Tag Editor', 'Change Tag', name);
}
@ -114,8 +126,13 @@ class TagEditor extends PureComponent {
return templateUtils.tokenizeTag(defaultFill);
}
async _update (tagDefinition, tagData, noCallback = false) {
async _update (tagDefinitions, tagDefinition, tagData, noCallback = false) {
const {handleRender} = this.props;
this.setState({rendering: true});
// Start render loader
const start = Date.now();
this.setState({rendering: true});
let preview = '';
let error = '';
@ -140,15 +157,17 @@ class TagEditor extends PureComponent {
error = err.message;
}
const isMounted = !!this._select;
if (isMounted) {
this.setState({
activeTagData,
preview,
error,
activeTagDefinition: tagDefinition
});
}
// Make rendering take at least this long so we can see a spinner
await delay(300 - (Date.now() - start));
this.setState({
tagDefinitions,
activeTagData,
preview,
error,
rendering: false,
activeTagDefinition: tagDefinition
});
// Call the callback if we need to
if (!noCallback) {
@ -278,7 +297,26 @@ class TagEditor extends PureComponent {
}
render () {
const {error, preview, activeTagDefinition, activeTagData} = this.state;
const {error, preview, activeTagDefinition, activeTagData, rendering} = this.state;
if (!activeTagData) {
return null;
}
let previewElement;
if (error) {
previewElement = (
<code className="block danger selectable">{error || <span>&nbsp;</span>}</code>
);
} else if (rendering) {
previewElement = (
<code className="block"><span className="faint italic">rendering...</span></code>
);
} else {
previewElement = (
<code className="block selectable">{preview || <span>&nbsp;</span>}</code>
);
}
return (
<div>
@ -287,7 +325,7 @@ class TagEditor extends PureComponent {
<select ref={this._setSelectRef}
onChange={this._handleChangeTag}
value={activeTagDefinition ? activeTagDefinition.name : ''}>
{templating.getTagDefinitions().map((tagDefinition, i) => (
{this.state.tagDefinitions.map((tagDefinition, i) => (
<option key={`${i}::${tagDefinition.name}`} value={tagDefinition.name}>
{tagDefinition.displayName} {tagDefinition.description}
</option>
@ -308,13 +346,17 @@ class TagEditor extends PureComponent {
</label>
</div>
)}
<div className="form-control form-control--outlined">
<label>Live Preview
{error
? <code className="block danger selectable">{error || <span>&nbsp;</span>}</code>
: <code className="block selectable">{preview || <span>&nbsp;</span>}</code>
}
</label>
<div className="form-row">
<div className="form-control form-control--outlined">
<button type="button"
className="txt-sm pull-right icon inline-block"
onClick={this._handleRefresh}>
refresh <i className={classnames('fa fa-refresh', {'fa-spin': rendering})}/>
</button>
<label>Live Preview
{previewElement}
</label>
</div>
</div>
</div>
);

View File

@ -209,7 +209,7 @@ class App extends PureComponent {
_requestGroupCreate (parentId) {
showPrompt({
headerName: 'New Folder',
title: 'New Folder',
defaultValue: 'My Folder',
submitName: 'Create',
label: 'Name',
@ -245,7 +245,7 @@ class App extends PureComponent {
async _workspaceDuplicate (callback) {
const workspace = this.props.activeWorkspace;
showPrompt({
headerName: 'Duplicate Workspace',
title: 'Duplicate Workspace',
defaultValue: `${workspace.name} (Copy)`,
submitName: 'Duplicate',
selectText: true,

View File

@ -142,8 +142,8 @@
& > * {
width: 100%;
margin-left: 0.5rem;
margin-right: 0.5rem;
margin-left: @padding-xxs;
margin-right: @padding-xxs;
}
& > p {

View File

@ -1,38 +1,39 @@
declare class Curl {
static option: {
ACCEPT_ENCODING: string,
CAINFO: string,
COOKIE: string,
COOKIEFILE: string,
COOKIELIST: string,
CUSTOMREQUEST: string,
DEBUGFUNCTION: string,
FOLLOWLOCATION: string,
HTTPAUTH: string,
PASSWORD: string,
USERNAME: string,
USERAGENT: string,
POSTFIELDS: string,
READDATA: string,
UPLOAD: string,
HTTPHEADER: string,
HTTPPOST: string,
INFILESIZE: string,
KEYPASSWD: string,
HTTPHEADER: string,
SSLCERTTYPE: string,
SSLCERT: string,
SSLKEY: string,
PROXY: string,
NOPROXY: string,
PROXYAUTH: string,
COOKIELIST: string,
COOKIEFILE: string,
CAINFO: string,
SSL_VERIFYPEER: string,
SSL_VERIFYHOST: string,
UNIX_SOCKET_PATH: string,
URL: string,
XFERINFOFUNCTION: string,
DEBUGFUNCTION: string,
ACCEPT_ENCODING: string,
NOPROGRESS: string,
VERBOSE: string,
TIMEOUT_MS: string,
FOLLOWLOCATION: string,
NOBODY: string,
CUSTOMREQUEST: string,
NOPROGRESS: string,
NOPROXY: string,
PASSWORD: string,
POSTFIELDS: string,
PROXY: string,
PROXYAUTH: string,
READDATA: string,
SSLCERT: string,
SSLCERTTYPE: string,
SSLKEY: string,
SSL_VERIFYHOST: string,
SSL_VERIFYPEER: string,
TIMEOUT_MS: string,
UNIX_SOCKET_PATH: string,
UPLOAD: string,
URL: string,
USERAGENT: string,
USERNAME: string,
VERBOSE: string,
XFERINFOFUNCTION: string,
};
static auth: {

3
flow-typed/react-flow-types.js vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'react-flow-types' {
declare module.exports: *
}

12
package-lock.json generated
View File

@ -4285,9 +4285,9 @@
"dev": true
},
"jest-runtime": {
"version": "19.0.3",
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-19.0.3.tgz",
"integrity": "sha1-oWM1Ss5GkQ7jPwKCtr/2sLh9QzA=",
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-19.0.4.tgz",
"integrity": "sha1-8WfZ8TR3UvICc2EGeSZIU0n8wkU=",
"dev": true,
"dependencies": {
"camelcase": {
@ -5991,9 +5991,9 @@
"dev": true,
"dependencies": {
"ansi-styles": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.0.0.tgz",
"integrity": "sha1-VATpOlRMT+x/BIJil3vr/jFV4ME=",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.1.0.tgz",
"integrity": "sha1-CcIC1ckX7CMYjKpcnLkXnNlUd1A=",
"dev": true
}
}