mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Add authorization header in webhooks stored in secrets table (#941)
Summary: Adding authorization header support for webhooks. Issue: https://github.com/gristlabs/grist-core/issues/827 --------- Co-authored-by: Florent <florent.git@zeteo.me>
This commit is contained in:
parent
2750ed6bd9
commit
0bfdaa9c02
@ -107,6 +107,12 @@ const WEBHOOK_COLUMNS = [
|
|||||||
type: 'Text',
|
type: 'Text',
|
||||||
label: t('Status'),
|
label: t('Status'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: VirtualId(),
|
||||||
|
colId: 'authorization',
|
||||||
|
type: 'Text',
|
||||||
|
label: t('Header Authorization'),
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,10 +120,11 @@ const WEBHOOK_COLUMNS = [
|
|||||||
*/
|
*/
|
||||||
const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [
|
const WEBHOOK_VIEW_FIELDS: Array<(typeof WEBHOOK_COLUMNS)[number]['colId']> = [
|
||||||
'name', 'memo',
|
'name', 'memo',
|
||||||
'eventTypes', 'url',
|
'eventTypes', 'tableId',
|
||||||
'tableId', 'isReadyColumn',
|
'watchedColIdsText', 'isReadyColumn',
|
||||||
'watchedColIdsText', 'webhookId',
|
'url', 'authorization',
|
||||||
'enabled', 'status'
|
'webhookId', 'enabled',
|
||||||
|
'status'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,7 +143,7 @@ class WebhookExternalTable implements IExternalTable {
|
|||||||
public name = 'GristHidden_WebhookTable';
|
public name = 'GristHidden_WebhookTable';
|
||||||
public initialActions = _prepareWebhookInitialActions(this.name);
|
public initialActions = _prepareWebhookInitialActions(this.name);
|
||||||
public saveableFields = [
|
public saveableFields = [
|
||||||
'tableId', 'watchedColIdsText', 'url', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn',
|
'tableId', 'watchedColIdsText', 'url', 'authorization', 'eventTypes', 'enabled', 'name', 'memo', 'isReadyColumn',
|
||||||
];
|
];
|
||||||
public webhooks: ObservableArray<UIWebhookSummary> = observableArray<UIWebhookSummary>([]);
|
public webhooks: ObservableArray<UIWebhookSummary> = observableArray<UIWebhookSummary>([]);
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export const Webhook = t.iface([], {
|
|||||||
|
|
||||||
export const WebhookFields = t.iface([], {
|
export const WebhookFields = t.iface([], {
|
||||||
"url": "string",
|
"url": "string",
|
||||||
|
"authorization": t.opt("string"),
|
||||||
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
|
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
|
||||||
"tableId": "string",
|
"tableId": "string",
|
||||||
"watchedColIds": t.opt(t.array("string")),
|
"watchedColIds": t.opt(t.array("string")),
|
||||||
@ -29,6 +30,7 @@ export const WebhookStatus = t.union(t.lit('idle'), t.lit('sending'), t.lit('ret
|
|||||||
|
|
||||||
export const WebhookSubscribe = t.iface([], {
|
export const WebhookSubscribe = t.iface([], {
|
||||||
"url": "string",
|
"url": "string",
|
||||||
|
"authorization": t.opt("string"),
|
||||||
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
|
"eventTypes": t.array(t.union(t.lit("add"), t.lit("update"))),
|
||||||
"watchedColIds": t.opt(t.array("string")),
|
"watchedColIds": t.opt(t.array("string")),
|
||||||
"enabled": t.opt("boolean"),
|
"enabled": t.opt("boolean"),
|
||||||
@ -45,6 +47,7 @@ export const WebhookSummary = t.iface([], {
|
|||||||
"id": "string",
|
"id": "string",
|
||||||
"fields": t.iface([], {
|
"fields": t.iface([], {
|
||||||
"url": "string",
|
"url": "string",
|
||||||
|
"authorization": t.opt("string"),
|
||||||
"unsubscribeKey": "string",
|
"unsubscribeKey": "string",
|
||||||
"eventTypes": t.array("string"),
|
"eventTypes": t.array("string"),
|
||||||
"isReadyColumn": t.union("string", "null"),
|
"isReadyColumn": t.union("string", "null"),
|
||||||
@ -64,6 +67,7 @@ export const WebhookUpdate = t.iface([], {
|
|||||||
|
|
||||||
export const WebhookPatch = t.iface([], {
|
export const WebhookPatch = t.iface([], {
|
||||||
"url": t.opt("string"),
|
"url": t.opt("string"),
|
||||||
|
"authorization": t.opt("string"),
|
||||||
"eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))),
|
"eventTypes": t.opt(t.array(t.union(t.lit("add"), t.lit("update")))),
|
||||||
"tableId": t.opt("string"),
|
"tableId": t.opt("string"),
|
||||||
"watchedColIds": t.opt(t.array("string")),
|
"watchedColIds": t.opt(t.array("string")),
|
||||||
|
@ -8,6 +8,7 @@ export interface Webhook {
|
|||||||
|
|
||||||
export interface WebhookFields {
|
export interface WebhookFields {
|
||||||
url: string;
|
url: string;
|
||||||
|
authorization?: string;
|
||||||
eventTypes: Array<"add"|"update">;
|
eventTypes: Array<"add"|"update">;
|
||||||
tableId: string;
|
tableId: string;
|
||||||
watchedColIds?: string[];
|
watchedColIds?: string[];
|
||||||
@ -26,6 +27,7 @@ export type WebhookStatus = 'idle'|'sending'|'retrying'|'postponed'|'error'|'inv
|
|||||||
// tableId from the url) but generics are not yet supported by ts-interface-builder
|
// tableId from the url) but generics are not yet supported by ts-interface-builder
|
||||||
export interface WebhookSubscribe {
|
export interface WebhookSubscribe {
|
||||||
url: string;
|
url: string;
|
||||||
|
authorization?: string;
|
||||||
eventTypes: Array<"add"|"update">;
|
eventTypes: Array<"add"|"update">;
|
||||||
watchedColIds?: string[];
|
watchedColIds?: string[];
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -42,6 +44,7 @@ export interface WebhookSummary {
|
|||||||
id: string;
|
id: string;
|
||||||
fields: {
|
fields: {
|
||||||
url: string;
|
url: string;
|
||||||
|
authorization?: string;
|
||||||
unsubscribeKey: string;
|
unsubscribeKey: string;
|
||||||
eventTypes: string[];
|
eventTypes: string[];
|
||||||
isReadyColumn: string|null;
|
isReadyColumn: string|null;
|
||||||
@ -64,6 +67,7 @@ export interface WebhookUpdate {
|
|||||||
// ts-interface-builder
|
// ts-interface-builder
|
||||||
export interface WebhookPatch {
|
export interface WebhookPatch {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
authorization?: string;
|
||||||
eventTypes?: Array<"add"|"update">;
|
eventTypes?: Array<"add"|"update">;
|
||||||
tableId?: string;
|
tableId?: string;
|
||||||
watchedColIds?: string[];
|
watchedColIds?: string[];
|
||||||
|
@ -1608,7 +1608,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
.where("id = :id AND doc_id = :docId", {id, docId})
|
.where("id = :id AND doc_id = :docId", {id, docId})
|
||||||
.execute();
|
.execute();
|
||||||
if (res.affected !== 1) {
|
if (res.affected !== 1) {
|
||||||
throw new ApiError('secret with given id not found', 404);
|
throw new ApiError('secret with given id not found or nothing was updated', 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1623,14 +1623,32 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
|
|
||||||
// Update the webhook url in the webhook's corresponding secret (note: the webhook identifier is
|
// Update the webhook url in the webhook's corresponding secret (note: the webhook identifier is
|
||||||
// its secret identifier).
|
// its secret identifier).
|
||||||
public async updateWebhookUrl(id: string, docId: string, url: string, outerManager?: EntityManager) {
|
public async updateWebhookUrlAndAuth(
|
||||||
|
props: {
|
||||||
|
id: string,
|
||||||
|
docId: string,
|
||||||
|
url: string | undefined,
|
||||||
|
auth: string | undefined,
|
||||||
|
outerManager?: EntityManager}
|
||||||
|
) {
|
||||||
|
const {id, docId, url, auth, outerManager} = props;
|
||||||
return await this._runInTransaction(outerManager, async manager => {
|
return await this._runInTransaction(outerManager, async manager => {
|
||||||
|
if (url === undefined && auth === undefined) {
|
||||||
|
throw new ApiError('None of the Webhook url and auth are defined', 404);
|
||||||
|
}
|
||||||
const value = await this.getSecret(id, docId, manager);
|
const value = await this.getSecret(id, docId, manager);
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new ApiError('Webhook with given id not found', 404);
|
throw new ApiError('Webhook with given id not found', 404);
|
||||||
}
|
}
|
||||||
const webhookSecret = JSON.parse(value);
|
const webhookSecret = JSON.parse(value);
|
||||||
|
// As we want to patch the webhookSecret object, only set the url and the authorization when they are defined.
|
||||||
|
// When the user wants to empty the value, we are expected to receive empty strings.
|
||||||
|
if (url !== undefined) {
|
||||||
webhookSecret.url = url;
|
webhookSecret.url = url;
|
||||||
|
}
|
||||||
|
if (auth !== undefined) {
|
||||||
|
webhookSecret.authorization = auth;
|
||||||
|
}
|
||||||
await this.updateSecret(id, docId, JSON.stringify(webhookSecret), manager);
|
await this.updateSecret(id, docId, JSON.stringify(webhookSecret), manager);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -324,7 +324,7 @@ export class DocWorkerApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const registerWebhook = async (activeDoc: ActiveDoc, req: RequestWithLogin, webhook: WebhookFields) => {
|
const registerWebhook = async (activeDoc: ActiveDoc, req: RequestWithLogin, webhook: WebhookFields) => {
|
||||||
const {fields, url} = await getWebhookSettings(activeDoc, req, null, webhook);
|
const {fields, url, authorization} = await getWebhookSettings(activeDoc, req, null, webhook);
|
||||||
if (!fields.eventTypes?.length) {
|
if (!fields.eventTypes?.length) {
|
||||||
throw new ApiError(`eventTypes must be a non-empty array`, 400);
|
throw new ApiError(`eventTypes must be a non-empty array`, 400);
|
||||||
}
|
}
|
||||||
@ -336,7 +336,7 @@ export class DocWorkerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribeKey = uuidv4();
|
const unsubscribeKey = uuidv4();
|
||||||
const webhookSecret: WebHookSecret = {unsubscribeKey, url};
|
const webhookSecret: WebHookSecret = {unsubscribeKey, url, authorization};
|
||||||
const secretValue = JSON.stringify(webhookSecret);
|
const secretValue = JSON.stringify(webhookSecret);
|
||||||
const webhookId = (await this._dbManager.addSecret(secretValue, activeDoc.docName)).id;
|
const webhookId = (await this._dbManager.addSecret(secretValue, activeDoc.docName)).id;
|
||||||
|
|
||||||
@ -392,7 +392,7 @@ export class DocWorkerApi {
|
|||||||
const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables");
|
const tablesTable = activeDoc.docData!.getMetaTable("_grist_Tables");
|
||||||
const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined;
|
const trigger = webhookId ? activeDoc.triggers.getWebhookTriggerRecord(webhookId) : undefined;
|
||||||
let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined;
|
let currentTableId = trigger ? tablesTable.getValue(trigger.tableRef, 'tableId')! : undefined;
|
||||||
const {url, eventTypes, watchedColIds, isReadyColumn, name} = webhook;
|
const {url, authorization, eventTypes, watchedColIds, isReadyColumn, name} = webhook;
|
||||||
const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables});
|
const tableId = await getRealTableId(req.params.tableId || webhook.tableId, {metaTables});
|
||||||
|
|
||||||
const fields: Partial<SchemaTypes['_grist_Triggers']> = {};
|
const fields: Partial<SchemaTypes['_grist_Triggers']> = {};
|
||||||
@ -454,6 +454,7 @@ export class DocWorkerApi {
|
|||||||
return {
|
return {
|
||||||
fields,
|
fields,
|
||||||
url,
|
url,
|
||||||
|
authorization,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,16 +927,16 @@ export class DocWorkerApi {
|
|||||||
|
|
||||||
const docId = activeDoc.docName;
|
const docId = activeDoc.docName;
|
||||||
const webhookId = req.params.webhookId;
|
const webhookId = req.params.webhookId;
|
||||||
const {fields, url} = await getWebhookSettings(activeDoc, req, webhookId, req.body);
|
const {fields, url, authorization} = await getWebhookSettings(activeDoc, req, webhookId, req.body);
|
||||||
if (fields.enabled === false) {
|
if (fields.enabled === false) {
|
||||||
await activeDoc.triggers.clearSingleWebhookQueue(webhookId);
|
await activeDoc.triggers.clearSingleWebhookQueue(webhookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerRowId = activeDoc.triggers.getWebhookTriggerRecord(webhookId).id;
|
const triggerRowId = activeDoc.triggers.getWebhookTriggerRecord(webhookId).id;
|
||||||
|
|
||||||
// update url in homedb
|
// update url and authorization header in homedb
|
||||||
if (url) {
|
if (url || authorization) {
|
||||||
await this._dbManager.updateWebhookUrl(webhookId, docId, url);
|
await this._dbManager.updateWebhookUrlAndAuth({id: webhookId, docId, url, auth: authorization});
|
||||||
activeDoc.triggers.webhookDeleted(webhookId); // clear cache
|
activeDoc.triggers.webhookDeleted(webhookId); // clear cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ type Trigger = MetaRowRecord<"_grist_Triggers">;
|
|||||||
export interface WebHookSecret {
|
export interface WebHookSecret {
|
||||||
url: string;
|
url: string;
|
||||||
unsubscribeKey: string;
|
unsubscribeKey: string;
|
||||||
|
authorization?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work to do after fetching values from the document
|
// Work to do after fetching values from the document
|
||||||
@ -259,6 +260,7 @@ export class DocTriggers {
|
|||||||
const getTableId = docData.getMetaTable("_grist_Tables").getRowPropFunc("tableId");
|
const getTableId = docData.getMetaTable("_grist_Tables").getRowPropFunc("tableId");
|
||||||
const getColId = docData.getMetaTable("_grist_Tables_column").getRowPropFunc("colId");
|
const getColId = docData.getMetaTable("_grist_Tables_column").getRowPropFunc("colId");
|
||||||
const getUrl = async (id: string) => (await this._getWebHook(id))?.url ?? '';
|
const getUrl = async (id: string) => (await this._getWebHook(id))?.url ?? '';
|
||||||
|
const getAuthorization = async (id: string) => (await this._getWebHook(id))?.authorization ?? '';
|
||||||
const getUnsubscribeKey = async (id: string) => (await this._getWebHook(id))?.unsubscribeKey ?? '';
|
const getUnsubscribeKey = async (id: string) => (await this._getWebHook(id))?.unsubscribeKey ?? '';
|
||||||
const resultTable: WebhookSummary[] = [];
|
const resultTable: WebhookSummary[] = [];
|
||||||
|
|
||||||
@ -271,6 +273,7 @@ export class DocTriggers {
|
|||||||
for (const act of webhookActions) {
|
for (const act of webhookActions) {
|
||||||
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
||||||
const url = await getUrl(act.id);
|
const url = await getUrl(act.id);
|
||||||
|
const authorization = await getAuthorization(act.id);
|
||||||
// Same story, should be hidden.
|
// Same story, should be hidden.
|
||||||
const unsubscribeKey = await getUnsubscribeKey(act.id);
|
const unsubscribeKey = await getUnsubscribeKey(act.id);
|
||||||
if (!url || !unsubscribeKey) {
|
if (!url || !unsubscribeKey) {
|
||||||
@ -285,6 +288,7 @@ export class DocTriggers {
|
|||||||
fields: {
|
fields: {
|
||||||
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
// Url, probably should be hidden for non-owners (but currently this API is owners only).
|
||||||
url,
|
url,
|
||||||
|
authorization,
|
||||||
unsubscribeKey,
|
unsubscribeKey,
|
||||||
// Other fields used to register this webhook.
|
// Other fields used to register this webhook.
|
||||||
eventTypes: decodeObject(t.eventTypes) as string[],
|
eventTypes: decodeObject(t.eventTypes) as string[],
|
||||||
@ -683,6 +687,7 @@ export class DocTriggers {
|
|||||||
const batch = _.takeWhile(this._webHookEventQueue.slice(0, 100), {id});
|
const batch = _.takeWhile(this._webHookEventQueue.slice(0, 100), {id});
|
||||||
const body = JSON.stringify(batch.map(e => e.payload));
|
const body = JSON.stringify(batch.map(e => e.payload));
|
||||||
const url = await this._getWebHookUrl(id);
|
const url = await this._getWebHookUrl(id);
|
||||||
|
const authorization = (await this._getWebHook(id))?.authorization || "";
|
||||||
if (this._loopAbort.signal.aborted) {
|
if (this._loopAbort.signal.aborted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -698,7 +703,8 @@ export class DocTriggers {
|
|||||||
this._activeDoc.logTelemetryEvent(null, 'sendingWebhooks', {
|
this._activeDoc.logTelemetryEvent(null, 'sendingWebhooks', {
|
||||||
limited: {numEvents: meta.numEvents},
|
limited: {numEvents: meta.numEvents},
|
||||||
});
|
});
|
||||||
success = await this._sendWebhookWithRetries(id, url, body, batch.length, this._loopAbort.signal);
|
success = await this._sendWebhookWithRetries(
|
||||||
|
id, url, authorization, body, batch.length, this._loopAbort.signal);
|
||||||
if (this._loopAbort.signal.aborted) {
|
if (this._loopAbort.signal.aborted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -770,7 +776,8 @@ export class DocTriggers {
|
|||||||
return this._drainingQueue ? Math.min(5, TRIGGER_MAX_ATTEMPTS) : TRIGGER_MAX_ATTEMPTS;
|
return this._drainingQueue ? Math.min(5, TRIGGER_MAX_ATTEMPTS) : TRIGGER_MAX_ATTEMPTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _sendWebhookWithRetries(id: string, url: string, body: string, size: number, signal: AbortSignal) {
|
private async _sendWebhookWithRetries(
|
||||||
|
id: string, url: string, authorization: string, body: string, size: number, signal: AbortSignal) {
|
||||||
const maxWait = 64;
|
const maxWait = 64;
|
||||||
let wait = 1;
|
let wait = 1;
|
||||||
for (let attempt = 0; attempt < this._maxWebhookAttempts; attempt++) {
|
for (let attempt = 0; attempt < this._maxWebhookAttempts; attempt++) {
|
||||||
@ -786,6 +793,7 @@ export class DocTriggers {
|
|||||||
body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
...(authorization ? {'Authorization': authorization} : {}),
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
agent: proxyAgent(new URL(url)),
|
agent: proxyAgent(new URL(url)),
|
||||||
|
@ -1241,7 +1241,8 @@
|
|||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Webhook Id": "Webhook Id",
|
"Webhook Id": "Webhook Id",
|
||||||
"Table": "Table",
|
"Table": "Table",
|
||||||
"Filter for changes in these columns (semicolon-separated ids)": "Filter for changes in these columns (semicolon-separated ids)"
|
"Filter for changes in these columns (semicolon-separated ids)": "Filter for changes in these columns (semicolon-separated ids)",
|
||||||
|
"Header Authorization": "Header Authorization"
|
||||||
},
|
},
|
||||||
"FormulaAssistant": {
|
"FormulaAssistant": {
|
||||||
"Ask the bot.": "Ask the bot.",
|
"Ask the bot.": "Ask the bot.",
|
||||||
|
@ -52,10 +52,11 @@ describe('WebhookPage', function () {
|
|||||||
'Name',
|
'Name',
|
||||||
'Memo',
|
'Memo',
|
||||||
'Event Types',
|
'Event Types',
|
||||||
'URL',
|
|
||||||
'Table',
|
'Table',
|
||||||
'Ready Column',
|
|
||||||
'Filter for changes in these columns (semicolon-separated ids)',
|
'Filter for changes in these columns (semicolon-separated ids)',
|
||||||
|
'Ready Column',
|
||||||
|
'URL',
|
||||||
|
'Header Authorization',
|
||||||
'Webhook Id',
|
'Webhook Id',
|
||||||
'Enabled',
|
'Enabled',
|
||||||
'Status',
|
'Status',
|
||||||
@ -81,7 +82,7 @@ describe('WebhookPage', function () {
|
|||||||
await gu.waitToPass(async () => {
|
await gu.waitToPass(async () => {
|
||||||
assert.equal(await getField(1, 'Webhook Id'), id);
|
assert.equal(await getField(1, 'Webhook Id'), id);
|
||||||
});
|
});
|
||||||
// Now other fields like name, memo and watchColIds are persisted.
|
// Now other fields like name, memo, watchColIds, and Header Auth are persisted.
|
||||||
await setField(1, 'Name', 'Test Webhook');
|
await setField(1, 'Name', 'Test Webhook');
|
||||||
await setField(1, 'Memo', 'Test Memo');
|
await setField(1, 'Memo', 'Test Memo');
|
||||||
await setField(1, 'Filter for changes in these columns (semicolon-separated ids)', 'A; B');
|
await setField(1, 'Filter for changes in these columns (semicolon-separated ids)', 'A; B');
|
||||||
@ -115,6 +116,27 @@ describe('WebhookPage', function () {
|
|||||||
assert.lengthOf((await docApi.getRows('Table2')).A, 0);
|
assert.lengthOf((await docApi.getRows('Table2')).A, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can create webhook with persistant header authorization', async function () {
|
||||||
|
// The webhook won't work because the header auth doesn't match the api key of the current test user.
|
||||||
|
await openWebhookPage();
|
||||||
|
await setField(1, 'Event Types', 'add\nupdate\n');
|
||||||
|
await setField(1, 'URL', `http://${host}/api/docs/${doc.id}/tables/Table2/records?flat=1`);
|
||||||
|
await setField(1, 'Table', 'Table1');
|
||||||
|
await gu.waitForServer();
|
||||||
|
await driver.navigate().refresh();
|
||||||
|
await waitForWebhookPage();
|
||||||
|
await setField(1, 'Header Authorization', 'Bearer 1234');
|
||||||
|
await gu.waitForServer();
|
||||||
|
await driver.navigate().refresh();
|
||||||
|
await waitForWebhookPage();
|
||||||
|
await gu.waitToPass(async () => {
|
||||||
|
assert.equal(await getField(1, 'Header Authorization'), 'Bearer 1234');
|
||||||
|
});
|
||||||
|
await gu.getDetailCell({col:'Header Authorization', rowNum: 1}).click();
|
||||||
|
await gu.enterCell(Key.DELETE, Key.ENTER);
|
||||||
|
await gu.waitForServer();
|
||||||
|
});
|
||||||
|
|
||||||
it('can create two webhooks', async function () {
|
it('can create two webhooks', async function () {
|
||||||
await openWebhookPage();
|
await openWebhookPage();
|
||||||
await setField(1, 'Event Types', 'add\nupdate\n');
|
await setField(1, 'Event Types', 'add\nupdate\n');
|
||||||
|
@ -4625,6 +4625,7 @@ function testDocApi() {
|
|||||||
id: first.webhookId,
|
id: first.webhookId,
|
||||||
fields: {
|
fields: {
|
||||||
url: `${serving.url}/200`,
|
url: `${serving.url}/200`,
|
||||||
|
authorization: '',
|
||||||
unsubscribeKey: first.unsubscribeKey,
|
unsubscribeKey: first.unsubscribeKey,
|
||||||
eventTypes: ['add', 'update'],
|
eventTypes: ['add', 'update'],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -4643,6 +4644,7 @@ function testDocApi() {
|
|||||||
id: second.webhookId,
|
id: second.webhookId,
|
||||||
fields: {
|
fields: {
|
||||||
url: `${serving.url}/404`,
|
url: `${serving.url}/404`,
|
||||||
|
authorization: '',
|
||||||
unsubscribeKey: second.unsubscribeKey,
|
unsubscribeKey: second.unsubscribeKey,
|
||||||
eventTypes: ['add', 'update'],
|
eventTypes: ['add', 'update'],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -5010,6 +5012,7 @@ function testDocApi() {
|
|||||||
|
|
||||||
const expectedFields = {
|
const expectedFields = {
|
||||||
url: `${serving.url}/foo`,
|
url: `${serving.url}/foo`,
|
||||||
|
authorization: '',
|
||||||
eventTypes: ['add'],
|
eventTypes: ['add'],
|
||||||
isReadyColumn: 'B',
|
isReadyColumn: 'B',
|
||||||
tableId: 'Table1',
|
tableId: 'Table1',
|
||||||
@ -5079,6 +5082,8 @@ function testDocApi() {
|
|||||||
|
|
||||||
await check({isReadyColumn: null}, 200);
|
await check({isReadyColumn: null}, 200);
|
||||||
await check({isReadyColumn: "bar"}, 404, `Column not found "bar"`);
|
await check({isReadyColumn: "bar"}, 404, `Column not found "bar"`);
|
||||||
|
|
||||||
|
await check({authorization: 'Bearer fake-token'}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user