fix(Nextcloud Node): Fix broken user actions and folder sharing, add internal link share type (#32002)

Co-authored-by: Jörn Bungartz <joern@bungartz.pm>
Co-authored-by: Dawid Myslak <dawid.myslak@gmail.com>
This commit is contained in:
Srinjoy
2026-06-19 21:13:20 +05:30
committed by GitHub
parent 6db66b050c
commit 37674499b0
4 changed files with 1423 additions and 68 deletions
@@ -19,9 +19,8 @@ export async function nextCloudApiRequest(
headers?: IDataObject,
encoding?: null,
query?: IDataObject,
useWebDavEndpoint: boolean = true,
) {
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const authenticationMethod = this.getNodeParameter('authentication', 0);
let credentials;
@@ -45,15 +44,11 @@ export async function nextCloudApiRequest(
options.encoding = null;
}
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
if (resource === 'user' && operation === 'create') {
options.uri = options.uri.replace('/remote.php/webdav', '');
}
if (resource === 'file' && operation === 'share') {
options.uri = options.uri.replace('/remote.php/webdav', '');
}
// Preserve the existing WebDAV path behavior: endpoints may start with '/', producing '//'.
// For non-WebDAV requests, strip the WebDAV suffix while preserving any subpath prefix.
options.uri = useWebDavEndpoint
? `${credentials.webDavUrl}/${encodeURI(endpoint)}`
: `${credentials.webDavUrl.replace(/\/remote\.php\/webdav\/?$/, '')}/${encodeURI(endpoint)}`;
const credentialType =
authenticationMethod === 'accessToken' ? 'nextCloudApi' : 'nextCloudOAuth2Api';
@@ -476,6 +476,12 @@ export class NextCloud implements INodeType {
name: 'Group',
value: 1,
},
{
name: 'Internal Link',
value: 200,
description:
'Generates an internal Nextcloud URL (not a public share). Uses the file/folder ID from a PROPFIND call. The output is { link: "..." }. Do not use with shareWith fields.',
},
{
name: 'Public Link',
value: 3,
@@ -876,18 +882,29 @@ export class NextCloud implements INodeType {
credentials = await this.getCredentials('nextCloudOAuth2Api');
}
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
let endpoint = '';
let requestMethod: IHttpRequestMethods = 'GET';
let responseData: any;
let body: string | Buffer | IDataObject = '';
const headers: IDataObject = {};
let qs;
let resource: string = '';
let operation: string = '';
let lastOperationWasDownload = false;
for (let i = 0; i < items.length; i++) {
let endpoint = '';
let requestMethod: IHttpRequestMethods = 'GET';
let responseData: any;
let body: string | Buffer | IDataObject = '';
const headers: IDataObject = {};
let qs;
// Reinitialize per-iteration so state from a previous item never leaks.
let useWebDavEndpoint = true;
resource = this.getNodeParameter('resource', i);
operation = this.getNodeParameter('operation', i);
// Must be set before the try block so it still runs when download fails with continueOnFail
if (resource === 'file' && operation === 'download') {
lastOperationWasDownload = true;
}
try {
if (resource === 'file') {
if (operation === 'download') {
@@ -927,6 +944,7 @@ export class NextCloud implements INodeType {
// list
// ----------------------------------
// PROPFIND is not in the IHttpRequestMethods enum but is required for WebDAV PROPFIND requests
requestMethod = 'PROPFIND' as IHttpRequestMethods;
endpoint = this.getNodeParameter('path', i) as string;
}
@@ -963,32 +981,49 @@ export class NextCloud implements INodeType {
// share
// ----------------------------------
requestMethod = 'POST';
const shareType = this.getNodeParameter('shareType', i) as number;
const sharePath = this.getNodeParameter('path', i) as string;
endpoint = 'ocs/v2.php/apps/files_sharing/api/v1/shares';
if (shareType === 200) {
// Internal Link: not a real OCS share, derive the link from oc:fileid via PROPFIND.
// PROPFIND is not in the IHttpRequestMethods enum but is required for WebDAV PROPFIND requests
requestMethod = 'PROPFIND' as IHttpRequestMethods;
endpoint = sharePath;
headers['Content-Type'] = 'application/xml';
headers.Depth = '0';
body = `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop><oc:fileid/></d:prop>
</d:propfind>`;
// useWebDavEndpoint stays true (default) for WebDAV PROPFIND.
} else {
// Regular OCS share.
requestMethod = 'POST';
useWebDavEndpoint = false;
endpoint = 'ocs/v2.php/apps/files_sharing/api/v1/shares';
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
headers['OCS-APIRequest'] = true;
headers['Content-Type'] = 'application/x-www-form-urlencoded';
const bodyParameters = this.getNodeParameter('options', i) as IDataObject;
const bodyParameters = this.getNodeParameter('options', i);
bodyParameters.path = sharePath;
bodyParameters.shareType = shareType;
bodyParameters.path = this.getNodeParameter('path', i) as string;
bodyParameters.shareType = this.getNodeParameter('shareType', i) as number;
if (shareType === 0) {
bodyParameters.shareWith = this.getNodeParameter('user', i) as string;
} else if (shareType === 7) {
bodyParameters.shareWith = this.getNodeParameter('circleId', i) as string;
} else if (shareType === 4) {
bodyParameters.shareWith = this.getNodeParameter('email', i) as string;
} else if (shareType === 1) {
bodyParameters.shareWith = this.getNodeParameter('groupId', i) as string;
}
if (bodyParameters.shareType === 0) {
bodyParameters.shareWith = this.getNodeParameter('user', i) as string;
} else if (bodyParameters.shareType === 7) {
bodyParameters.shareWith = this.getNodeParameter('circleId', i) as number;
} else if (bodyParameters.shareType === 4) {
bodyParameters.shareWith = this.getNodeParameter('email', i) as string;
} else if (bodyParameters.shareType === 1) {
bodyParameters.shareWith = this.getNodeParameter('groupId', i) as number;
body = new URLSearchParams(bodyParameters as Record<string, string>).toString();
}
// @ts-ignore
body = new URLSearchParams(bodyParameters).toString();
}
} else if (resource === 'user') {
useWebDavEndpoint = false;
if (operation === 'create') {
// ----------------------------------
// user:create
@@ -1102,6 +1137,7 @@ export class NextCloud implements INodeType {
headers,
encoding,
qs,
useWebDavEndpoint,
);
} catch (error) {
if (this.continueOnFail()) {
@@ -1139,39 +1175,152 @@ export class NextCloud implements INodeType {
endpoint,
);
} else if (['file', 'folder'].includes(resource) && operation === 'share') {
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
parseString(
responseData as string,
{
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
},
(err, data) => {
if (err) {
return reject(err);
}
const shareType = this.getNodeParameter('shareType', i) as number;
if (data.ocs.meta.status !== 'ok') {
return reject(
new NodeApiError(
this.getNode(),
(data.ocs.meta.message as JsonObject) || (data.ocs.meta.status as JsonObject),
),
);
}
if (shareType === 200) {
// Internal Link: responseData is the PROPFIND multistatus XML.
if (typeof responseData !== 'string') {
throw new NodeOperationError(
this.getNode(),
'Could not retrieve internal link: unexpected response type from NextCloud',
{ itemIndex: i },
);
}
resolve(data.ocs.data as IDataObject);
},
const propfindData: IDataObject = await new Promise((resolve, reject) => {
parseString(
responseData,
{
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
},
(err, data) => {
if (err) {
return reject(err);
}
if (!data || typeof data !== 'object') {
return reject(
new NodeOperationError(
this.getNode(),
'Could not retrieve internal link: invalid XML response structure',
{ itemIndex: i },
),
);
}
resolve(data);
},
);
});
const multistatus = propfindData['d:multistatus'] as IDataObject | undefined;
if (!multistatus) {
throw new NodeOperationError(
this.getNode(),
'Could not retrieve internal link: malformed PROPFIND response',
{ itemIndex: i },
);
}
const responses = multistatus['d:response'];
if (!responses) {
throw new NodeOperationError(
this.getNode(),
'Could not retrieve internal link: malformed PROPFIND response',
{ itemIndex: i },
);
}
const responseList: IDataObject[] = Array.isArray(responses)
? (responses as IDataObject[])
: [responses as IDataObject];
const matchedResponse = responseList[0];
let props: IDataObject | undefined;
const propstat = matchedResponse['d:propstat'];
if (Array.isArray(propstat)) {
props = (propstat[0] as IDataObject)['d:prop'] as IDataObject | undefined;
} else if (propstat && typeof propstat === 'object') {
props = (propstat as IDataObject)['d:prop'] as IDataObject | undefined;
}
const fileid = props?.['oc:fileid'];
if (typeof fileid !== 'string' || fileid.length === 0) {
throw new NodeOperationError(
this.getNode(),
'Could not retrieve internal link: oc:fileid not found in PROPFIND response',
{ itemIndex: i },
);
}
const webDavBase = (credentials.webDavUrl as string).replace(
/\/remote\.php\/webdav\/?$/,
'',
);
});
const executionData = this.helpers.constructExecutionMetaData(
wrapData(jsonResponseData),
{ itemData: { item: i } },
);
if (webDavBase === credentials.webDavUrl) {
throw new NodeOperationError(
this.getNode(),
'WebDAV URL must end with /remote.php/webdav for generating an internal link. Please check your Nextcloud credentials.',
{ itemIndex: i },
);
}
returnData.push(...executionData);
const internalLink = `${webDavBase}/f/${fileid}`;
const executionData = this.helpers.constructExecutionMetaData(
wrapData({ link: internalLink }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} else {
if (typeof responseData !== 'string') {
throw new NodeOperationError(
this.getNode(),
'Unexpected response type from NextCloud OCS share endpoint',
{ itemIndex: i },
);
}
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
parseString(
responseData,
{
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
},
(err, data) => {
if (err) {
return reject(err);
}
if (data.ocs.meta.status !== 'ok') {
return reject(
new NodeApiError(
this.getNode(),
(data.ocs.meta.message as JsonObject) ||
(data.ocs.meta.status as JsonObject),
),
);
}
if (!data?.ocs?.data || typeof data.ocs.data !== 'object') {
return reject(
new NodeApiError(this.getNode(), { error: 'Invalid OCS response structure' }),
);
}
resolve(data.ocs.data);
},
);
});
const executionData = this.helpers.constructExecutionMetaData(
wrapData(jsonResponseData),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
} else if (resource === 'user') {
if (operation !== 'getAll') {
const jsonResponseData: IDataObject = await new Promise((resolve, reject) => {
@@ -1331,9 +1480,12 @@ export class NextCloud implements INodeType {
}
throw error;
}
if (resource === 'file' && operation === 'download') {
lastOperationWasDownload = true;
}
}
if (resource === 'file' && operation === 'download') {
if (lastOperationWasDownload) {
// For file downloads the files get attached to the existing items
return [items];
} else {
@@ -0,0 +1,258 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
IHttpRequestMethods,
INode,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { Mock } from 'vitest';
import { nextCloudApiRequest } from '../GenericFunctions';
const webDavUrl = 'https://nextcloud.example.com/remote.php/webdav';
const baseUrl = 'https://nextcloud.example.com';
type Authentication = 'accessToken' | 'oAuth2';
function buildFunctions(authentication: Authentication = 'accessToken') {
const requestWithAuthentication = vi.fn();
const getCredentials = vi.fn(async () => ({ webDavUrl }));
const getNodeParameter = vi.fn((parameterName: string) => {
if (parameterName === 'authentication') return authentication;
return undefined;
});
const functions = {
getCredentials,
getNode: vi.fn(
() =>
({
id: 'nextcloud-node',
name: 'Nextcloud',
type: 'n8n-nodes-base.nextCloud',
typeVersion: 1,
position: [0, 0],
parameters: {},
}) as INode,
),
getNodeParameter,
helpers: {
requestWithAuthentication,
},
} as unknown as IHookFunctions & IExecuteFunctions;
return { functions, getCredentials, getNodeParameter, requestWithAuthentication };
}
function requestOptions(requestWithAuthentication: Mock) {
return requestWithAuthentication.mock.calls[0][1] as IDataObject;
}
describe('NextCloud GenericFunctions', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('uses access token credentials and builds a WebDAV request by default', async () => {
const { functions, getCredentials, requestWithAuthentication } = buildFunctions();
requestWithAuthentication.mockResolvedValue({ status: 'ok' });
const response = await nextCloudApiRequest.call(functions, 'GET', '/test.txt', '');
expect(response).toEqual({ status: 'ok' });
expect(getCredentials).toHaveBeenCalledWith('nextCloudApi');
expect(requestWithAuthentication).toHaveBeenCalledWith(
'nextCloudApi',
expect.objectContaining({
method: 'GET',
uri: `${webDavUrl}//test.txt`,
body: '',
headers: undefined,
qs: {},
json: false,
}),
);
expect(requestOptions(requestWithAuthentication).uri).toEqual(
expect.stringContaining('/remote.php/webdav'),
);
});
it('handles non-standard WebDAV URLs gracefully', async () => {
const customUrl = 'https://custom.example.com/dav';
const { functions, getCredentials, requestWithAuthentication } = buildFunctions();
getCredentials.mockResolvedValue({ webDavUrl: customUrl });
requestWithAuthentication.mockResolvedValue({ status: 'ok' });
await nextCloudApiRequest.call(
functions,
'GET',
'/test.txt',
'',
undefined,
undefined,
undefined,
true,
);
expect(requestOptions(requestWithAuthentication).uri).toBe(`${customUrl}//test.txt`);
expect(requestOptions(requestWithAuthentication).uri).toEqual(expect.stringContaining('/dav'));
});
it('uses OAuth2 credentials when OAuth2 authentication is selected', async () => {
const { functions, getCredentials, requestWithAuthentication } = buildFunctions('oAuth2');
requestWithAuthentication.mockResolvedValue({ status: 'ok' });
await nextCloudApiRequest.call(functions, 'GET', '/test.txt', '');
expect(getCredentials).toHaveBeenCalledWith('nextCloudOAuth2Api');
expect(requestWithAuthentication).toHaveBeenCalledWith(
'nextCloudOAuth2Api',
expect.objectContaining({
method: 'GET',
uri: `${webDavUrl}//test.txt`,
}),
);
});
it('removes the WebDAV path for OCS requests', async () => {
const { functions, requestWithAuthentication } = buildFunctions();
requestWithAuthentication.mockResolvedValue('<ocs />');
await nextCloudApiRequest.call(
functions,
'POST',
'ocs/v1.php/cloud/users',
'userid=alice',
{ 'OCS-APIRequest': true },
undefined,
undefined,
false,
);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'POST',
uri: `${baseUrl}/ocs/v1.php/cloud/users`,
body: 'userid=alice',
headers: { 'OCS-APIRequest': true },
qs: {},
json: false,
});
expect(requestOptions(requestWithAuthentication).uri).not.toEqual(
expect.stringContaining('/remote.php/webdav'),
);
});
it('strips non-standard WebDAV path for OCS requests while preserving subpath', async () => {
const customUrl = 'https://custom.example.com/nextcloud/remote.php/webdav';
const { functions, getCredentials, requestWithAuthentication } = buildFunctions();
getCredentials.mockResolvedValue({ webDavUrl: customUrl });
requestWithAuthentication.mockResolvedValue('<ocs />');
await nextCloudApiRequest.call(
functions,
'POST',
'ocs/v1.php/cloud/users',
'',
{},
undefined,
undefined,
false,
);
expect(requestOptions(requestWithAuthentication).uri).toBe(
'https://custom.example.com/nextcloud/ocs/v1.php/cloud/users',
);
});
it('passes body, headers, query, and null encoding to requestWithAuthentication', async () => {
const { functions, requestWithAuthentication } = buildFunctions();
const body = Buffer.from('file content');
const headers = { Depth: '0', Destination: `${webDavUrl}//to.txt` };
const query = { limit: 1 };
requestWithAuthentication.mockResolvedValue(Buffer.from('response'));
await nextCloudApiRequest.call(
functions,
'PROPFIND' as IHttpRequestMethods,
'/test.txt',
body,
headers,
null,
query,
true,
);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUrl}//test.txt`,
body,
headers,
encoding: null,
qs: query,
json: false,
});
});
it('URL-encodes endpoint characters while preserving path separators', async () => {
const { functions, requestWithAuthentication } = buildFunctions();
requestWithAuthentication.mockResolvedValue({});
await nextCloudApiRequest.call(functions, 'GET', '/folder name/test file.txt', '');
expect(requestOptions(requestWithAuthentication).uri).toBe(
`${webDavUrl}//folder%20name/test%20file.txt`,
);
});
it('throws NodeOperationError when Nextcloud responds with a fatal error page', async () => {
const { functions, requestWithAuthentication } = buildFunctions();
requestWithAuthentication.mockResolvedValue('<b>Fatal error</b> broken response');
const promise = nextCloudApiRequest.call(functions, 'GET', '/test.txt', '');
await expect(promise).rejects.toThrow(NodeOperationError);
await expect(promise).rejects.toThrow("NextCloud responded with a 'Fatal error'");
});
it('strips standard remote.php/webdav path for OCS requests', async () => {
const { functions, requestWithAuthentication } = buildFunctions();
requestWithAuthentication.mockResolvedValue('<ocs />');
await nextCloudApiRequest.call(
functions,
'POST',
'ocs/v1.php/cloud/users',
'',
{},
undefined,
undefined,
false,
);
expect(requestOptions(requestWithAuthentication).uri).toBe(
'https://nextcloud.example.com/ocs/v1.php/cloud/users',
);
});
it('handles non-standard WebDAV URLs with subpath gracefully', async () => {
const customUrl = 'https://custom.example.com/nextcloud/dav';
const { functions, getCredentials, requestWithAuthentication } = buildFunctions();
getCredentials.mockResolvedValue({ webDavUrl: customUrl });
requestWithAuthentication.mockResolvedValue({ status: 'ok' });
await nextCloudApiRequest.call(
functions,
'GET',
'/test.txt',
'',
undefined,
undefined,
undefined,
true,
);
expect(requestOptions(requestWithAuthentication).uri).toBe(`${customUrl}//test.txt`);
expect(requestOptions(requestWithAuthentication).uri).toEqual(expect.stringContaining('/dav'));
});
});
@@ -0,0 +1,950 @@
import type {
IDataObject,
IExecuteFunctions,
INode,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import type { Mock } from 'vitest';
import { NextCloud } from '../NextCloud.node';
const webDavUrl = 'https://nextcloud.example.com/remote.php/webdav';
const baseUrl = 'https://nextcloud.example.com';
const ocsSuccessResponse = `<?xml version="1.0"?>
<ocs>
<meta><status>ok</status></meta>
<data><id>123</id><url>https://nc.example.com/s/abc</url></data>
</ocs>`;
const ocsUserResponse = `<?xml version="1.0"?>
<ocs>
<meta><status>ok</status></meta>
<data><id>alice</id><email>alice@example.com</email><displayname>Alice</displayname></data>
</ocs>`;
const ocsUserListResponse = `<?xml version="1.0"?>
<ocs>
<meta><status>ok</status></meta>
<data><users><element>alice</element><element>bob</element></users></data>
</ocs>`;
const ocsErrorResponse = `<?xml version="1.0"?>
<ocs>
<meta><status>failure</status><message>User not found</message></meta>
</ocs>`;
const webDavFilePropfindResponse = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/webdav/test.txt</d:href>
<d:propstat><d:prop><oc:fileid>55555</oc:fileid></d:prop></d:propstat>
</d:response>
</d:multistatus>`;
const webDavFolderListResponse = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/webdav/projects/</d:href>
<d:propstat><d:prop><d:getlastmodified>Mon, 01 Jan 2024</d:getlastmodified><d:resourcetype><d:collection /></d:resourcetype><d:getetag>"folder-etag"</d:getetag></d:prop></d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/webdav/projects/file1.txt</d:href>
<d:propstat><d:prop><d:getlastmodified>Tue, 02 Jan 2024</d:getlastmodified><d:getcontentlength>1024</d:getcontentlength><d:getcontenttype>text/plain</d:getcontenttype><d:resourcetype></d:resourcetype><d:getetag>"file-etag"</d:getetag></d:prop></d:propstat>
</d:response>
</d:multistatus>`;
const webDavMissingFileIdResponse = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/webdav/test.txt</d:href>
<d:propstat><d:prop></d:prop></d:propstat>
</d:response>
</d:multistatus>`;
const webDavFolderPropfindResponse = `<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/webdav/projects/</d:href>
<d:propstat><d:prop><oc:fileid>77777</oc:fileid></d:prop></d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/webdav/projects/file1.txt</d:href>
<d:propstat><d:prop><oc:fileid>88888</oc:fileid></d:prop></d:propstat>
</d:response>
</d:multistatus>`;
type ParameterValue = string | number | boolean | IDataObject;
interface BuildExecuteFunctionsOptions {
parameters: Record<string, ParameterValue> | Array<Record<string, ParameterValue>>;
inputData?: INodeExecutionData[];
authentication?: 'accessToken' | 'oAuth2';
continueOnFail?: boolean;
}
const nextCloudNode = new NextCloud();
function buildExecuteFunctions({
parameters,
inputData = [{ json: {} }],
authentication = 'accessToken',
continueOnFail = false,
}: BuildExecuteFunctionsOptions) {
const requestWithAuthentication = vi.fn();
const getCredentials = vi.fn(async () => ({ webDavUrl }));
const prepareBinaryData = vi.fn(async () => ({
data: 'prepared-binary-data',
mimeType: 'text/plain',
fileName: 'test.txt',
}));
const getBinaryDataBuffer = vi.fn(async () => Buffer.from('binary upload'));
const assertBinaryData = vi.fn();
const constructExecutionMetaData = vi.fn(
(data: INodeExecutionData[], metadata?: { itemData?: { item: number } }) =>
data.map((item) => ({ ...item, pairedItem: metadata?.itemData })),
);
const parameterForItem = (itemIndex: number) =>
Array.isArray(parameters) ? (parameters[itemIndex] ?? parameters[0]) : parameters;
const executeFunctions = {
continueOnFail: vi.fn(() => continueOnFail),
getCredentials,
getInputData: vi.fn(() => inputData),
getNode: vi.fn(
() =>
({
id: 'nextcloud-node',
name: 'Nextcloud',
type: 'n8n-nodes-base.nextCloud',
typeVersion: 1,
position: [0, 0],
parameters: {},
}) as INode,
),
getNodeParameter: vi.fn(
(parameterName: string, itemIndex: number, defaultValue?: ParameterValue) => {
if (parameterName === 'authentication') return authentication;
const itemParameters = parameterForItem(itemIndex);
if (parameterName in itemParameters) return itemParameters[parameterName];
return defaultValue;
},
),
helpers: {
assertBinaryData,
constructExecutionMetaData,
getBinaryDataBuffer,
prepareBinaryData,
requestWithAuthentication,
},
} as unknown as IExecuteFunctions;
return {
assertBinaryData,
constructExecutionMetaData,
executeFunctions,
getBinaryDataBuffer,
getCredentials,
prepareBinaryData,
requestWithAuthentication,
};
}
async function executeNode(executeFunctions: IExecuteFunctions) {
return (await (nextCloudNode as INodeType).execute!.call(
executeFunctions,
)) as INodeExecutionData[][];
}
function requestOptions(requestWithAuthentication: Mock, callIndex = 0) {
return requestWithAuthentication.mock.calls[callIndex][1] as IDataObject;
}
function expectWebDavUri(uri: unknown) {
expect(uri).toEqual(expect.stringContaining('/remote.php/webdav'));
}
function expectOcsUri(uri: unknown) {
expect(uri).not.toEqual(expect.stringContaining('/remote.php/webdav'));
}
function webDavUri(path: string) {
return `${webDavUrl}/${path}`;
}
describe('NextCloud Node', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe.each(['accessToken', 'oAuth2'] as const)('authentication: %s', (authentication) => {
it('uses the matching credential type', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
authentication,
parameters: {
resource: 'file',
operation: 'delete',
path: '/test.txt',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'deleted' });
await executeNode(executeFunctions);
expect(requestWithAuthentication).toHaveBeenCalledWith(
authentication === 'accessToken' ? 'nextCloudApi' : 'nextCloudOAuth2Api',
expect.any(Object),
);
});
});
describe('file', () => {
it('downloads a file as binary data', async () => {
const downloadBuffer = Buffer.from('downloaded file');
const { executeFunctions, prepareBinaryData, requestWithAuthentication } =
buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'download',
path: '/test.txt',
binaryPropertyName: 'data',
},
});
requestWithAuthentication.mockResolvedValue(downloadBuffer);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'GET',
uri: `${webDavUri('/test.txt')}`,
encoding: null,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(prepareBinaryData).toHaveBeenCalledWith(downloadBuffer, '/test.txt');
expect(result[0][0]).toEqual({
json: {},
pairedItem: { item: 0 },
binary: {
data: {
data: 'prepared-binary-data',
mimeType: 'text/plain',
fileName: 'test.txt',
},
},
});
});
it('uploads a text file', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'upload',
path: '/test.txt',
binaryDataUpload: false,
fileContent: 'hello world',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'uploaded' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PUT',
uri: `${webDavUri('/test.txt')}`,
body: 'hello world',
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'uploaded' }, pairedItem: { item: 0 } }]]);
});
it('uploads a binary file', async () => {
const { executeFunctions, getBinaryDataBuffer, requestWithAuthentication } =
buildExecuteFunctions({
inputData: [
{ json: {}, binary: { data: { data: 'binary-data', mimeType: 'text/plain' } } },
],
parameters: {
resource: 'file',
operation: 'upload',
path: '/test.txt',
binaryDataUpload: true,
binaryPropertyName: 'data',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'uploaded' });
const result = await executeNode(executeFunctions);
expect(getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data');
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PUT',
uri: `${webDavUri('/test.txt')}`,
body: Buffer.from('binary upload'),
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'uploaded' }, pairedItem: { item: 0 } }]]);
});
it('copies a file', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'copy',
path: '/from.txt',
toPath: '/to.txt',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'copied' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'COPY',
uri: `${webDavUri('/from.txt')}`,
headers: { Destination: `${webDavUri('/to.txt')}` },
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'copied' }, pairedItem: { item: 0 } }]]);
});
it('moves a file', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'move',
path: '/from.txt',
toPath: '/to.txt',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'moved' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'MOVE',
uri: `${webDavUri('/from.txt')}`,
headers: { Destination: `${webDavUri('/to.txt')}` },
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'moved' }, pairedItem: { item: 0 } }]]);
});
it('deletes a file', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'delete',
path: '/test.txt',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'deleted' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'DELETE',
uri: `${webDavUri('/test.txt')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'deleted' }, pairedItem: { item: 0 } }]]);
});
});
describe('folder', () => {
it('creates a folder', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'create',
path: '/projects',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'created' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'MKCOL',
uri: `${webDavUri('/projects')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'created' }, pairedItem: { item: 0 } }]]);
});
it('lists a folder', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'list',
path: '/projects',
},
});
requestWithAuthentication.mockResolvedValue(webDavFolderListResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUri('/projects')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([
[
{
json: {
path: 'projects/file1.txt',
type: 'file',
lastModified: 'Tue, 02 Jan 2024',
contentLength: '1024',
contentType: 'text/plain',
eTag: 'file-etag',
},
pairedItem: { item: 0 },
},
],
]);
});
it('copies a folder', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'copy',
path: '/projects',
toPath: '/archive/projects',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'copied' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'COPY',
uri: `${webDavUri('/projects')}`,
headers: { Destination: `${webDavUri('/archive/projects')}` },
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'copied' }, pairedItem: { item: 0 } }]]);
});
it('moves a folder', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'move',
path: '/projects',
toPath: '/archive/projects',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'moved' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'MOVE',
uri: `${webDavUri('/projects')}`,
headers: { Destination: `${webDavUri('/archive/projects')}` },
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'moved' }, pairedItem: { item: 0 } }]]);
});
it('deletes a folder', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'delete',
path: '/projects',
},
});
requestWithAuthentication.mockResolvedValue({ status: 'deleted' });
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'DELETE',
uri: `${webDavUri('/projects')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'deleted' }, pairedItem: { item: 0 } }]]);
});
});
describe('user', () => {
it('creates a user', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'create',
userId: 'alice',
email: 'alice@example.com',
additionalFields: { displayName: 'Alice' },
},
});
requestWithAuthentication.mockResolvedValue(ocsSuccessResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'POST',
uri: `${baseUrl}/ocs/v1.php/cloud/users`,
headers: {
'OCS-APIRequest': true,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'userid=alice&email=alice@example.com&displayName=Alice',
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([
[{ json: { id: '123', url: 'https://nc.example.com/s/abc' }, pairedItem: { item: 0 } }],
]);
});
it('deletes a user', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'delete',
userId: 'alice',
},
});
requestWithAuthentication.mockResolvedValue(ocsSuccessResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'DELETE',
uri: `${baseUrl}/ocs/v1.php/cloud/users/alice`,
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'ok' }, pairedItem: { item: 0 } }]]);
});
it('gets a user', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'get',
userId: 'alice',
},
});
requestWithAuthentication.mockResolvedValue(ocsUserResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'GET',
uri: `${baseUrl}/ocs/v1.php/cloud/users/alice`,
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([
[
{
json: { id: 'alice', email: 'alice@example.com', displayname: 'Alice' },
pairedItem: { item: 0 },
},
],
]);
});
it('gets all users without a limit', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'getAll',
returnAll: true,
options: { search: 'a' },
},
});
requestWithAuthentication.mockResolvedValue(ocsUserListResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'GET',
uri: `${baseUrl}/ocs/v1.php/cloud/users`,
qs: { search: 'a' },
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([
[
{ json: { id: 'alice' }, pairedItem: { item: 0 } },
{ json: { id: 'bob' }, pairedItem: { item: 0 } },
],
]);
});
it('gets users with a limit', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'getAll',
returnAll: false,
limit: 1,
options: {},
},
});
requestWithAuthentication.mockResolvedValue(ocsUserListResponse);
await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'GET',
uri: `${baseUrl}/ocs/v1.php/cloud/users`,
qs: { limit: 1 },
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
});
it('updates a user', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'update',
userId: 'alice',
updateFields: {
field: {
key: 'email',
value: 'alice.updated@example.com',
},
},
},
});
requestWithAuthentication.mockResolvedValue(ocsSuccessResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PUT',
uri: `${baseUrl}/ocs/v1.php/cloud/users/alice`,
body: 'key=email&value=alice.updated@example.com',
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(result).toEqual([[{ json: { status: 'ok' }, pairedItem: { item: 0 } }]]);
});
});
describe.each([
{ resource: 'file', path: '/test.txt' },
{ resource: 'folder', path: '/projects' },
])('share: $resource', ({ resource, path }) => {
it.each([
{ shareType: 0, name: 'user', parameterName: 'user', shareWith: 'alice' },
{ shareType: 1, name: 'group', parameterName: 'groupId', shareWith: 'engineering' },
{ shareType: 3, name: 'public link', parameterName: undefined, shareWith: undefined },
{ shareType: 4, name: 'email', parameterName: 'email', shareWith: 'alice@example.com' },
{ shareType: 7, name: 'circle', parameterName: 'circleId', shareWith: 'circle-1' },
])('creates a $name share', async ({ shareType, parameterName, shareWith }) => {
const parameters: Record<string, ParameterValue> = {
resource,
operation: 'share',
path,
shareType,
options: shareType === 3 ? { password: 'secret' } : {},
};
if (parameterName && shareWith) parameters[parameterName] = shareWith;
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({ parameters });
requestWithAuthentication.mockResolvedValue(ocsSuccessResponse);
const result = await executeNode(executeFunctions);
const body = requestOptions(requestWithAuthentication).body as string;
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'POST',
uri: `${baseUrl}/ocs/v2.php/apps/files_sharing/api/v1/shares`,
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
expect(body).toContain(`path=${encodeURIComponent(path)}`);
expect(body).toContain(`shareType=${shareType}`);
if (shareWith) expect(body).toContain(`shareWith=${encodeURIComponent(shareWith)}`);
if (shareType === 3) expect(body).toContain('password=secret');
expect(result).toEqual([
[{ json: { id: '123', url: 'https://nc.example.com/s/abc' }, pairedItem: { item: 0 } }],
]);
});
it('returns an internal link from the WebDAV file id', async () => {
const { constructExecutionMetaData, executeFunctions, requestWithAuthentication } =
buildExecuteFunctions({
parameters: {
resource,
operation: 'share',
path,
shareType: 200,
options: {},
},
});
requestWithAuthentication.mockResolvedValue(webDavFilePropfindResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUrl}/${encodeURI(path)}`,
headers: {
Depth: '0',
'Content-Type': 'application/xml',
},
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(constructExecutionMetaData).toHaveBeenCalledWith(
[{ json: { link: `${baseUrl}/f/55555` } }],
{ itemData: { item: 0 } },
);
expect(result).toEqual([[{ json: { link: `${baseUrl}/f/55555` }, pairedItem: { item: 0 } }]]);
});
it('returns an internal link from folder PROPFIND with multiple responses', async () => {
const { constructExecutionMetaData, executeFunctions, requestWithAuthentication } =
buildExecuteFunctions({
parameters: {
resource: 'folder',
operation: 'share',
path: '/projects',
shareType: 200,
options: {},
},
});
requestWithAuthentication.mockResolvedValue(webDavFolderPropfindResponse);
const result = await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUrl}//projects`,
headers: {
Depth: '0',
'Content-Type': 'application/xml',
},
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
// Should use the first response (folder) fileid, not the child file
expect(constructExecutionMetaData).toHaveBeenCalledWith(
[{ json: { link: `${baseUrl}/f/77777` } }],
{ itemData: { item: 0 } },
);
expect(result).toEqual([[{ json: { link: `${baseUrl}/f/77777` }, pairedItem: { item: 0 } }]]);
});
it('throws when webDavUrl does not match the expected pattern for internal links', async () => {
const { executeFunctions, getCredentials, requestWithAuthentication } = buildExecuteFunctions(
{
parameters: {
resource,
operation: 'share',
path,
shareType: 200,
options: {},
},
},
);
// Override credential to a non-standard WebDAV URL
getCredentials.mockResolvedValue({ webDavUrl: 'https://nc.example.com/dav' });
requestWithAuthentication.mockResolvedValue(webDavFilePropfindResponse);
const promise = executeNode(executeFunctions);
await expect(promise).rejects.toThrow(NodeOperationError);
await expect(promise).rejects.toThrow('must end with /remote.php/webdav');
});
});
describe('errors', () => {
it('throws NodeApiError on an OCS error response', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'user',
operation: 'get',
userId: 'missing-user',
},
});
requestWithAuthentication.mockResolvedValue(ocsErrorResponse);
await expect(executeNode(executeFunctions)).rejects.toThrow(NodeApiError);
});
it('propagates WebDAV request errors', async () => {
const webDavError = new Error('404 Not Found');
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'delete',
path: '/missing.txt',
},
});
requestWithAuthentication.mockRejectedValue(webDavError);
await expect(executeNode(executeFunctions)).rejects.toThrow(webDavError);
});
it('throws NodeOperationError when an internal link PROPFIND response has no file id', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'share',
path: '/test.txt',
shareType: 200,
options: {},
},
});
requestWithAuthentication.mockResolvedValue(webDavMissingFileIdResponse);
const promise = executeNode(executeFunctions);
await expect(promise).rejects.toThrow(NodeOperationError);
await expect(promise).rejects.toThrow('oc:fileid not found');
});
it('throws NodeOperationError when an internal link PROPFIND response is not a string', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
parameters: {
resource: 'file',
operation: 'share',
path: '/test.txt',
shareType: 200,
options: {},
},
});
requestWithAuthentication.mockResolvedValue({});
const promise = executeNode(executeFunctions);
await expect(promise).rejects.toThrow(NodeOperationError);
await expect(promise).rejects.toThrow('unexpected response type');
});
it('wraps request errors when continueOnFail is true', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
continueOnFail: true,
parameters: {
resource: 'folder',
operation: 'delete',
path: '/missing',
},
});
requestWithAuthentication.mockRejectedValue(new Error('404 Not Found'));
const result = await executeNode(executeFunctions);
expect(result).toEqual([[{ json: { error: '404 Not Found' }, pairedItem: { item: 0 } }]]);
});
it('returns original items when a file download fails with continueOnFail', async () => {
const inputItem = { json: {} };
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
continueOnFail: true,
inputData: [inputItem],
parameters: {
resource: 'file',
operation: 'download',
path: '/large-file.mp4',
binaryPropertyName: 'data',
},
});
requestWithAuthentication.mockRejectedValue(new Error('Network timeout'));
const result = await executeNode(executeFunctions);
// Should return the original items (with error attached), not returnData
expect(result).toEqual([
[
{
json: { error: 'Network timeout' },
},
],
]);
});
});
describe('multi item execution', () => {
it('keeps WebDAV endpoint usage scoped per item', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
inputData: [{ json: {} }, { json: {} }],
parameters: [
{
resource: 'file',
operation: 'share',
path: '/test.txt',
shareType: 200,
options: {},
binaryPropertyName: 'data',
},
{
resource: 'file',
operation: 'share',
path: '/second.txt',
shareType: 200,
options: {},
},
],
});
requestWithAuthentication.mockResolvedValue(webDavFilePropfindResponse);
await executeNode(executeFunctions);
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUri('/test.txt')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication).uri);
expect(requestOptions(requestWithAuthentication, 1)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUri('/second.txt')}`,
});
expectWebDavUri(requestOptions(requestWithAuthentication, 1).uri);
});
it('does not leak OCS headers from a regular share into a subsequent Internal Link request', async () => {
const { executeFunctions, requestWithAuthentication } = buildExecuteFunctions({
inputData: [{ json: {} }, { json: {} }],
parameters: [
{
resource: 'file',
operation: 'share',
path: '/first.txt',
shareType: 3, // public link share
options: {},
},
{
resource: 'file',
operation: 'share',
path: '/second.txt',
shareType: 200, // internal link
options: {},
},
],
});
requestWithAuthentication
.mockResolvedValueOnce(ocsSuccessResponse)
.mockResolvedValueOnce(webDavFilePropfindResponse);
const result = await executeNode(executeFunctions);
// First request: OCS endpoint with OCS-APIRequest header
expect(requestOptions(requestWithAuthentication)).toMatchObject({
method: 'POST',
uri: expect.stringContaining('/ocs/v2.php/apps/files_sharing/api/v1/shares'), // fixed slash
headers: { 'OCS-APIRequest': true },
});
expectOcsUri(requestOptions(requestWithAuthentication).uri);
// Second request: WebDAV PROPFIND, must NOT have OCS headers
expect(requestOptions(requestWithAuthentication, 1)).toMatchObject({
method: 'PROPFIND',
uri: `${webDavUri('/second.txt')}`,
headers: {
'Content-Type': 'application/xml',
Depth: '0',
},
});
expectWebDavUri(requestOptions(requestWithAuthentication, 1).uri);
expect(requestOptions(requestWithAuthentication, 1).headers).not.toHaveProperty(
'OCS-APIRequest',
);
// Results: first item gets OCS share data, second gets internal link
expect(result[0][0].json).toHaveProperty('id');
expect(result[0][1].json).toEqual({ link: `${baseUrl}/f/55555` });
});
});
});