mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
feat(Confluence Node): Add page add and remove label operations (#37002)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { confluenceApiRequest } from '../../transport';
|
||||
import { optionalSpaceRLC, pageRLC, resolvePageId } from '../common';
|
||||
import type { ConfluenceOperation } from '../router';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
optionalSpaceRLC,
|
||||
{
|
||||
...pageRLC,
|
||||
description: 'The page to add labels to',
|
||||
},
|
||||
{
|
||||
displayName: 'Labels',
|
||||
name: 'labels',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'e.g. runbook, q3-release',
|
||||
description:
|
||||
'The label names to add, comma-separated for several. Label names cannot contain spaces — use an underscore or hyphen instead. Existing labels are kept.',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['page'],
|
||||
operation: ['addLabels'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export const execute: ConfluenceOperation = async function (
|
||||
this: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
) {
|
||||
// `?? ''` guards an expression resolving to undefined, which would otherwise
|
||||
// stringify into a label literally named "undefined"
|
||||
const names = String(this.getNodeParameter('labels', itemIndex, '') ?? '')
|
||||
.split(',')
|
||||
.map((name) => name.trim())
|
||||
.filter((name) => name !== '');
|
||||
if (names.length === 0) {
|
||||
throw new NodeOperationError(this.getNode(), "The 'Labels' parameter is empty", { itemIndex });
|
||||
}
|
||||
// Confluence splits a name on whitespace into several labels rather than rejecting it,
|
||||
// so "release notes" would silently create "release" and "notes"
|
||||
const spaced = names.find((name) => /\s/.test(name));
|
||||
if (spaced !== undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The label "${spaced}" contains a space; use an underscore or hyphen instead`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
const pageId = await resolvePageId.call(this, itemIndex);
|
||||
|
||||
// v1 endpoint: v2's label surface is read-only. LabelCreate requires a prefix,
|
||||
// and 'global' is what the Confluence UI writes.
|
||||
return await confluenceApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/wiki/rest/api/content/${encodeURIComponent(pageId)}/label`,
|
||||
names.map((name) => ({ prefix: 'global', name })),
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as addComment from './addComment.operation';
|
||||
import * as addLabels from './addLabels.operation';
|
||||
import * as append from './append.operation';
|
||||
import * as create from './create.operation';
|
||||
import * as del from './delete.operation';
|
||||
@@ -9,10 +10,12 @@ import * as get from './get.operation';
|
||||
import * as getComments from './getComments.operation';
|
||||
import * as getLabels from './getLabels.operation';
|
||||
import * as getManyByLabel from './getManyByLabel.operation';
|
||||
import * as removeLabel from './removeLabel.operation';
|
||||
import * as update from './update.operation';
|
||||
|
||||
export {
|
||||
addComment,
|
||||
addLabels,
|
||||
append,
|
||||
create,
|
||||
del as delete,
|
||||
@@ -21,6 +24,7 @@ export {
|
||||
getComments,
|
||||
getLabels,
|
||||
getManyByLabel,
|
||||
removeLabel,
|
||||
update,
|
||||
};
|
||||
|
||||
@@ -42,6 +46,12 @@ export const description: INodeProperties[] = [
|
||||
description: 'Add a footer comment to a page, or reply to an existing comment',
|
||||
action: 'Add a comment to a page',
|
||||
},
|
||||
{
|
||||
name: 'Add Labels',
|
||||
value: 'addLabels',
|
||||
description: 'Add one or more labels to a page',
|
||||
action: 'Add labels to a page',
|
||||
},
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
@@ -90,6 +100,12 @@ export const description: INodeProperties[] = [
|
||||
description: 'Retrieve all pages carrying a label',
|
||||
action: 'Get many pages by label',
|
||||
},
|
||||
{
|
||||
name: 'Remove Label',
|
||||
value: 'removeLabel',
|
||||
description: 'Remove a label from a page by name',
|
||||
action: 'Remove a label from a page',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
@@ -100,6 +116,7 @@ export const description: INodeProperties[] = [
|
||||
default: 'create',
|
||||
},
|
||||
...addComment.description,
|
||||
...addLabels.description,
|
||||
...append.description,
|
||||
...create.description,
|
||||
...del.description,
|
||||
@@ -108,5 +125,6 @@ export const description: INodeProperties[] = [
|
||||
...getComments.description,
|
||||
...getLabels.description,
|
||||
...getManyByLabel.description,
|
||||
...removeLabel.description,
|
||||
...update.description,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { IExecuteFunctions, INodeProperties } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { confluenceApiRequest } from '../../transport';
|
||||
import { optionalSpaceRLC, pageRLC, resolvePageId } from '../common';
|
||||
import type { ConfluenceOperation } from '../router';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
optionalSpaceRLC,
|
||||
{
|
||||
...pageRLC,
|
||||
description: 'The page to remove the label from',
|
||||
},
|
||||
{
|
||||
displayName: 'Label',
|
||||
name: 'labelName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
placeholder: 'e.g. runbook',
|
||||
description: 'The name of a single label to remove',
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
show: {
|
||||
resource: ['page'],
|
||||
operation: ['removeLabel'],
|
||||
},
|
||||
};
|
||||
|
||||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export const execute: ConfluenceOperation = async function (
|
||||
this: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
) {
|
||||
// `?? ''` guards an expression resolving to undefined, which would otherwise
|
||||
// stringify into a request for a label literally named "undefined"
|
||||
const label = String(this.getNodeParameter('labelName', itemIndex, '') ?? '').trim();
|
||||
if (label === '') {
|
||||
throw new NodeOperationError(this.getNode(), "The 'Label' parameter is empty", { itemIndex });
|
||||
}
|
||||
// A comma-separated list would 204 as a no-op and report success — reject it here instead
|
||||
if (label.includes(',')) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
"The 'Label' parameter accepts one label name; commas are not allowed",
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
// A label can never hold a space, so this would 204 as a no-op and report success too
|
||||
if (/\s/.test(label)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The label "${label}" contains a space; Confluence labels use an underscore or hyphen instead`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
const pageId = await resolvePageId.call(this, itemIndex);
|
||||
|
||||
// The name goes in the query string, not the path: the path variant rejects "/" in the name.
|
||||
// No prefix: neither content-label DELETE variant declares one — the name alone identifies it.
|
||||
await confluenceApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/wiki/rest/api/content/${encodeURIComponent(pageId)}/label`,
|
||||
{},
|
||||
{ name: label },
|
||||
);
|
||||
|
||||
return { removed: true, pageId, label };
|
||||
};
|
||||
@@ -46,6 +46,9 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
|
||||
case 'page:addComment':
|
||||
responseData = await page.addComment.execute.call(this, i);
|
||||
break;
|
||||
case 'page:addLabels':
|
||||
responseData = await page.addLabels.execute.call(this, i);
|
||||
break;
|
||||
case 'page:append':
|
||||
responseData = await page.append.execute.call(this, i);
|
||||
break;
|
||||
@@ -70,6 +73,9 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
|
||||
case 'page:getManyByLabel':
|
||||
responseData = await page.getManyByLabel.execute.call(this, i);
|
||||
break;
|
||||
case 'page:removeLabel':
|
||||
responseData = await page.removeLabel.execute.call(this, i);
|
||||
break;
|
||||
case 'page:update':
|
||||
responseData = await page.update.execute.call(this, i);
|
||||
break;
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('Confluence Node', () => {
|
||||
expect(operationProperty('attachment')?.default).toBe('getMany');
|
||||
expect(operationOptions('page')).toEqual([
|
||||
expect.objectContaining({ value: 'addComment' }),
|
||||
expect.objectContaining({ value: 'addLabels' }),
|
||||
expect.objectContaining({ value: 'append' }),
|
||||
expect.objectContaining({ value: 'create' }),
|
||||
expect.objectContaining({ value: 'delete' }),
|
||||
@@ -44,6 +45,7 @@ describe('Confluence Node', () => {
|
||||
expect.objectContaining({ value: 'getComments' }),
|
||||
expect.objectContaining({ value: 'getLabels' }),
|
||||
expect.objectContaining({ value: 'getManyByLabel' }),
|
||||
expect.objectContaining({ value: 'removeLabel' }),
|
||||
expect.objectContaining({ value: 'update' }),
|
||||
]);
|
||||
expect(operationOptions('search')).toEqual([expect.objectContaining({ value: 'query' })]);
|
||||
@@ -66,6 +68,16 @@ describe('Confluence Node', () => {
|
||||
expect(limit?.displayOptions?.show?.returnAll).toEqual([false]);
|
||||
});
|
||||
|
||||
it('should render the label operations own fields', () => {
|
||||
const fieldsFor = (operation: string) =>
|
||||
node.description.properties
|
||||
.filter((p) => (p.displayOptions?.show?.operation ?? []).includes(operation))
|
||||
.map((p) => p.name);
|
||||
|
||||
expect(fieldsFor('addLabels')).toEqual(['space', 'page', 'labels']);
|
||||
expect(fieldsFor('removeLabel')).toEqual(['space', 'page', 'labelName']);
|
||||
});
|
||||
|
||||
it('should reference the confluenceCloudOAuth2Api credential by name', () => {
|
||||
expect(node.description.credentials).toEqual([
|
||||
{ name: 'confluenceCloudOAuth2Api', required: true },
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../actions/page/addLabels.operation';
|
||||
import { confluenceApiRequest } from '../../../transport';
|
||||
import { mockExecuteCtx } from '../../shared';
|
||||
|
||||
vi.mock('../../../transport', () => ({
|
||||
CONFLUENCE_CREDENTIAL_NAME: 'confluenceCloudOAuth2Api',
|
||||
confluenceApiRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
const apiRequest = vi.mocked(confluenceApiRequest);
|
||||
|
||||
describe('Confluence page:addLabels operation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiRequest.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it('posts a single label with the global prefix', async () => {
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'id', value: '123' }, labels: 'runbook' });
|
||||
|
||||
await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', '/wiki/rest/api/content/123/label', [
|
||||
{ prefix: 'global', name: 'runbook' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('trims a comma-separated list, drops empties and posts it in one request', async () => {
|
||||
const ctx = mockExecuteCtx({
|
||||
page: { mode: 'id', value: '123' },
|
||||
labels: ' alpha , ,beta, ',
|
||||
});
|
||||
|
||||
await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', '/wiki/rest/api/content/123/label', [
|
||||
{ prefix: 'global', name: 'alpha' },
|
||||
{ prefix: 'global', name: 'beta' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the label list response unchanged', async () => {
|
||||
const response = {
|
||||
results: [{ id: '9', name: 'runbook', prefix: 'global', label: 'runbook' }],
|
||||
start: 0,
|
||||
limit: 200,
|
||||
size: 1,
|
||||
_links: { base: 'https://example.atlassian.net/wiki' },
|
||||
};
|
||||
apiRequest.mockResolvedValueOnce(response);
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'id', value: '123' }, labels: 'runbook' });
|
||||
|
||||
expect(await execute.call(ctx, 0)).toEqual(response);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['whitespace only', ' '],
|
||||
['an expression resolving to undefined', undefined],
|
||||
])('throws without calling the API when the labels input is %s', async (_case, labels) => {
|
||||
// By Title, so a guard placed after the page lookup would still issue a request
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'title', value: 'Doc' }, labels });
|
||||
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(NodeOperationError);
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow("The 'Labels' parameter is empty");
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a label containing a space instead of letting Confluence split it', async () => {
|
||||
// By Title, so a guard placed after the page lookup would still issue a request
|
||||
const ctx = mockExecuteCtx({
|
||||
page: { mode: 'title', value: 'Doc' },
|
||||
labels: 'runbook, release notes',
|
||||
});
|
||||
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(NodeOperationError);
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(
|
||||
'The label "release notes" contains a space',
|
||||
);
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves a By Title selection to its page ID before posting', async () => {
|
||||
apiRequest.mockResolvedValueOnce({ results: [{ id: '777', title: 'Doc', spaceId: '1' }] });
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'title', value: 'Doc' }, labels: 'runbook' });
|
||||
|
||||
await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'GET',
|
||||
'/wiki/api/v2/pages',
|
||||
{},
|
||||
{ title: 'Doc', limit: 250 },
|
||||
);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(2, 'POST', '/wiki/rest/api/content/777/label', [
|
||||
{ prefix: 'global', name: 'runbook' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { execute } from '../../../actions/page/removeLabel.operation';
|
||||
import { confluenceApiRequest } from '../../../transport';
|
||||
import { mockExecuteCtx } from '../../shared';
|
||||
|
||||
vi.mock('../../../transport', () => ({
|
||||
CONFLUENCE_CREDENTIAL_NAME: 'confluenceCloudOAuth2Api',
|
||||
confluenceApiRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
const apiRequest = vi.mocked(confluenceApiRequest);
|
||||
|
||||
describe('Confluence page:removeLabel operation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// A 204 with an empty body comes back as '' under `json: true`
|
||||
apiRequest.mockResolvedValue('' as unknown as IDataObject);
|
||||
});
|
||||
|
||||
it('deletes the trimmed label by name in the query string and reports it', async () => {
|
||||
const ctx = mockExecuteCtx({
|
||||
page: { mode: 'id', value: '123' },
|
||||
labelName: ' runbook ',
|
||||
});
|
||||
|
||||
const result = await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(apiRequest).toHaveBeenCalledWith(
|
||||
'DELETE',
|
||||
'/wiki/rest/api/content/123/label',
|
||||
{},
|
||||
{ name: 'runbook' },
|
||||
);
|
||||
expect(result).toEqual({ removed: true, pageId: '123', label: 'runbook' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
['whitespace only', ' '],
|
||||
['an expression resolving to undefined', undefined],
|
||||
])('throws without calling the API when the label is %s', async (_case, labelName) => {
|
||||
// By Title, so a guard placed after the page lookup would still issue a request
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'title', value: 'Doc' }, labelName });
|
||||
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(NodeOperationError);
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow("The 'Label' parameter is empty");
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a comma-separated list instead of removing nothing', async () => {
|
||||
const ctx = mockExecuteCtx({
|
||||
page: { mode: 'title', value: 'Doc' },
|
||||
labelName: 'qa-alpha, qa-beta',
|
||||
});
|
||||
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(NodeOperationError);
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow('commas are not allowed');
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a label containing a space instead of removing nothing', async () => {
|
||||
const ctx = mockExecuteCtx({
|
||||
page: { mode: 'title', value: 'Doc' },
|
||||
labelName: 'release notes',
|
||||
});
|
||||
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(NodeOperationError);
|
||||
await expect(execute.call(ctx, 0)).rejects.toThrow(
|
||||
'The label "release notes" contains a space',
|
||||
);
|
||||
expect(apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves a By Title selection to its page ID before deleting', async () => {
|
||||
apiRequest.mockResolvedValueOnce({ results: [{ id: '777', title: 'Doc', spaceId: '1' }] });
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'title', value: 'Doc' }, labelName: 'runbook' });
|
||||
|
||||
const result = await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'GET',
|
||||
'/wiki/api/v2/pages',
|
||||
{},
|
||||
{ title: 'Doc', limit: 250 },
|
||||
);
|
||||
expect(apiRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'DELETE',
|
||||
'/wiki/rest/api/content/777/label',
|
||||
{},
|
||||
{ name: 'runbook' },
|
||||
);
|
||||
expect(result).toEqual({ removed: true, pageId: '777', label: 'runbook' });
|
||||
});
|
||||
|
||||
it('passes a label containing a slash through the query string untouched', async () => {
|
||||
const ctx = mockExecuteCtx({ page: { mode: 'id', value: '123' }, labelName: 'team/qa' });
|
||||
|
||||
const result = await execute.call(ctx, 0);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledWith(
|
||||
'DELETE',
|
||||
'/wiki/rest/api/content/123/label',
|
||||
{},
|
||||
{ name: 'team/qa' },
|
||||
);
|
||||
expect(result).toEqual({ removed: true, pageId: '123', label: 'team/qa' });
|
||||
});
|
||||
});
|
||||
@@ -148,6 +148,57 @@ describe('Confluence router', () => {
|
||||
expect(result).toEqual([[{ json: { id: '555', pageId: '1' }, pairedItem: { item: 0 } }]]);
|
||||
});
|
||||
|
||||
it('dispatches page:addLabels and returns the label list as one item', async () => {
|
||||
const response = {
|
||||
results: [
|
||||
{ id: '9', name: 'runbook', prefix: 'global', label: 'runbook' },
|
||||
{ id: '10', name: 'q3', prefix: 'global', label: 'q3' },
|
||||
],
|
||||
start: 0,
|
||||
limit: 200,
|
||||
size: 2,
|
||||
};
|
||||
apiRequest.mockResolvedValue(response);
|
||||
|
||||
const result = await router.call(
|
||||
mockExecuteCtx({
|
||||
resource: 'page',
|
||||
operation: 'addLabels',
|
||||
page: { mode: 'id', value: '1' },
|
||||
labels: 'runbook, q3',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledWith('POST', '/wiki/rest/api/content/1/label', [
|
||||
{ prefix: 'global', name: 'runbook' },
|
||||
{ prefix: 'global', name: 'q3' },
|
||||
]);
|
||||
expect(result).toEqual([[{ json: response, pairedItem: { item: 0 } }]]);
|
||||
});
|
||||
|
||||
it('dispatches page:removeLabel and returns the removal report', async () => {
|
||||
apiRequest.mockResolvedValue('');
|
||||
|
||||
const result = await router.call(
|
||||
mockExecuteCtx({
|
||||
resource: 'page',
|
||||
operation: 'removeLabel',
|
||||
page: { mode: 'id', value: '1' },
|
||||
labelName: 'runbook',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(apiRequest).toHaveBeenCalledWith(
|
||||
'DELETE',
|
||||
'/wiki/rest/api/content/1/label',
|
||||
{},
|
||||
{ name: 'runbook' },
|
||||
);
|
||||
expect(result).toEqual([
|
||||
[{ json: { removed: true, pageId: '1', label: 'runbook' }, pairedItem: { item: 0 } }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches page:deleteComment and returns the deletion report', async () => {
|
||||
const result = await router.call(
|
||||
mockExecuteCtx({
|
||||
|
||||
@@ -124,6 +124,18 @@ describe('confluenceApiRequest', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('sends an array body through as an array', async () => {
|
||||
const body = [{ prefix: 'global', name: 'a' }];
|
||||
|
||||
await confluenceApiRequest.call(ctx, 'POST', '/wiki/rest/api/content/1/label', body);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'confluenceCloudOAuth2Api',
|
||||
expect.objectContaining({ body: [{ prefix: 'global', name: 'a' }] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults body and qs to empty objects', async () => {
|
||||
await confluenceApiRequest.call(ctx, 'GET', '/wiki/api/v2/pages');
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function confluenceApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
body: IDataObject | IDataObject[] = {},
|
||||
qs: IDataObject = {},
|
||||
): Promise<IDataObject> {
|
||||
const credentials = await this.getCredentials(CONFLUENCE_CREDENTIAL_NAME);
|
||||
|
||||
Reference in New Issue
Block a user