Template response plugin

This commit is contained in:
Gregory Schier
2024-08-19 19:11:36 -07:00
parent 54efb6ae4e
commit d1871b19ee
30 changed files with 2272 additions and 329 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "template-function-response",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli src/index.ts"
},
"dependencies": {
"@yaakapp/api": "^0.1.9",
"jsonpath-plus": "^9.0.0",
"xpath": "^0.0.34",
"@xmldom/xmldom": "^0.8.10"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4",
"@types/node": "^20.14.9",
"typescript": "^5.5.2",
"vitest": "^1.4.0"
}
}

View File

@@ -0,0 +1,110 @@
import { DOMParser } from '@xmldom/xmldom';
import { CallTemplateFunctionArgs, Context, Plugin } from '@yaakapp/api';
import { JSONPath } from 'jsonpath-plus';
import { readFileSync } from 'node:fs';
import xpath from 'xpath';
export const plugin: Plugin = {
templateFunctions: [{
name: 'Response',
args: [
{
type: 'http_request',
name: 'request',
label: 'Request',
},
{
type: 'text',
name: 'path',
label: 'JSONPath or XPath',
placeholder: '$.books[0].id or /books[0]/id'
},
{
type: 'select',
name: 'behavior',
label: 'Sending Behavior',
defaultValue: 'smart',
options: [
{ name: 'When no responses', value: 'smart' },
{ name: 'Always', value: 'always' },
{ name: 'Never', value: 'never' },
],
},
],
async onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null> {
if (!args.values.request || !args.values.path) {
return null;
}
const httpRequest = await ctx.httpRequest.getById({ id: args.values.request ?? 'n/a' });
if (httpRequest == null) {
return null;
}
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest });
const responses = await ctx.httpResponse.find({ requestId: httpRequest.id, limit: 1 });
if (args.values.behavior === 'never' && responses.length === 0) {
return null;
}
const response = (args.values.behavior === 'always' || responses[0] == null)
? await ctx.httpRequest.send({ httpRequest: renderedHttpRequest })
: responses[0];
if (response.bodyPath == null) {
return null;
}
let body;
try {
body = readFileSync(response.bodyPath, 'utf-8');
} catch (_) {
return null;
}
try {
return filterJSONPath(body, args.values.path);
} catch (err) {
// Probably not JSON, try XPath
}
try {
return filterXPath(body, args.values.path);
} catch (err) {
// Probably not XML
}
return null; // Bail out
},
}],
};
function filterJSONPath(body: string, path: string): string {
const parsed = JSON.parse(body);
const items = JSONPath({ path, json: parsed })[0];
if (items == null) {
return '';
}
if (
Object.prototype.toString.call(items) === '[object Array]' ||
Object.prototype.toString.call(items) === '[object Object]'
) {
return JSON.stringify(items);
} else {
return String(items);
}
}
function filterXPath(body: string, path: string): string {
const doc = new DOMParser().parseFromString(body, 'text/xml');
const items = xpath.select(path, doc, false);
if (Array.isArray(items)) {
return String(items[0] ?? '');
} else {
// Not sure what cases this happens in (?)
return String(items);
}
}

View File

@@ -0,0 +1,177 @@
import { describe, expect, test } from 'vitest';
import { pluginHookExport } from '../src';
const ctx = {};
describe('exporter-curl', () => {
test('Exports GET with params', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
urlParameters: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual(
[`curl 'https://yaak.app'`, `--url-query 'a=aaa'`, `--url-query 'b=bbb'`].join(` \\\n `),
);
});
test('Exports POST with url form data', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
},
}),
).toEqual(
[`curl -X POST 'https://yaak.app'`, `--data 'a=aaa'`, `--data 'b=bbb'`].join(` \\\n `),
);
});
test('Exports PUT with multipart form', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'PUT',
bodyType: 'multipart/form-data',
body: {
form: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
{ name: 'f', file: '/foo/bar.png', contentType: 'image/png' },
],
},
}),
).toEqual(
[
`curl -X PUT 'https://yaak.app'`,
`--form 'a=aaa'`,
`--form 'b=bbb'`,
`--form f=@/foo/bar.png;type=image/png`,
].join(` \\\n `),
);
});
test('Exports JSON body', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar's"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw $'{"foo":"bar\\'s"}'`,
].join(` \\\n `),
);
});
test('Exports multi-line JSON body', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
body: {
text: `{"foo":"bar",\n"baz":"qux"}`,
},
headers: [{ name: 'Content-Type', value: 'application/json' }],
}),
).toEqual(
[
`curl -X POST 'https://yaak.app'`,
`--header 'Content-Type: application/json'`,
`--data-raw $'{"foo":"bar",\n"baz":"qux"}'`,
].join(` \\\n `),
);
});
test('Exports headers', () => {
expect(
pluginHookExport(ctx, {
headers: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
{ name: 'c', value: 'ccc', enabled: false },
],
}),
).toEqual([`curl`, `--header 'a: aaa'`, `--header 'b: bbb'`].join(` \\\n `));
});
test('Basic auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--user 'user:pass'`].join(` \\\n `));
});
test('Broken basic auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {},
}),
).toEqual([`curl 'https://yaak.app'`, `--user ':'`].join(` \\\n `));
});
test('Digest auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--digest --user 'user:pass'`].join(` \\\n `));
});
test('Bearer auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
token: 'tok',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer tok'`].join(` \\\n `));
});
test('Broken bearer auth', () => {
expect(
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
username: 'user',
password: 'pass',
},
}),
).toEqual([`curl 'https://yaak.app'`, `--header 'Authorization: Bearer '`].join(` \\\n `));
});
});