feat(Microsoft Entra ID Node): New node (#11779)

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
feelgood-interface
2025-01-08 11:49:26 +01:00
committed by GitHub
parent 52ae02abaa
commit 3006ccf41b
16 changed files with 7331 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
import type { ICredentialType, INodeProperties, Icon } from 'n8n-workflow';
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class MicrosoftEntraOAuth2Api implements ICredentialType {
name = 'microsoftEntraOAuth2Api';
@@ -7,8 +7,6 @@ export class MicrosoftEntraOAuth2Api implements ICredentialType {
extends = ['microsoftOAuth2Api'];
icon: Icon = 'file:icons/Azure.svg';
documentationUrl = 'microsoftentra';
properties: INodeProperties[] = [
@@ -16,8 +14,9 @@ export class MicrosoftEntraOAuth2Api implements ICredentialType {
displayName: 'Scope',
name: 'scope',
type: 'hidden',
// Sites.FullControl.All required to update user specific properties https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1316
default:
'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All',
'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All Sites.FullControl.All GroupMember.ReadWrite.All',
},
];
}

View File

@@ -0,0 +1,412 @@
import type {
JsonObject,
IDataObject,
IExecuteFunctions,
IExecuteSingleFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IRequestOptions,
INodeExecutionData,
IN8nHttpFullResponse,
INodePropertyOptions,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { parseStringPromise } from 'xml2js';
export async function microsoftApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs?: IDataObject,
headers?: IDataObject,
url?: string,
): Promise<any> {
const options: IHttpRequestOptions = {
method,
url: url ?? `https://graph.microsoft.com/v1.0${endpoint}`,
json: true,
headers,
body,
qs,
};
return await this.helpers.requestWithAuthentication.call(
this,
'microsoftEntraOAuth2Api',
options,
);
}
export async function microsoftApiPaginateRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
qs?: IDataObject,
headers?: IDataObject,
url?: string,
itemIndex: number = 0,
): Promise<IDataObject[]> {
// Todo: IHttpRequestOptions doesn't have uri property which is required for requestWithAuthenticationPaginated
const options: IRequestOptions = {
method,
uri: url ?? `https://graph.microsoft.com/v1.0${endpoint}`,
json: true,
headers,
body,
qs,
};
const pages = await this.helpers.requestWithAuthenticationPaginated.call(
this,
options,
itemIndex,
{
continue: '={{ !!$response.body?.["@odata.nextLink"] }}',
request: {
url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}',
},
requestInterval: 0,
},
'microsoftEntraOAuth2Api',
);
let results: IDataObject[] = [];
for (const page of pages) {
const items = page.body.value as IDataObject[];
if (items) {
results = results.concat(items);
}
}
return results;
}
export async function handleErrorPostReceive(
this: IExecuteSingleFunctions,
data: INodeExecutionData[],
response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) {
const resource = this.getNodeParameter('resource') as string;
const operation = this.getNodeParameter('operation') as string;
const {
code: errorCode,
message: errorMessage,
details: errorDetails,
} = (response.body as IDataObject)?.error as {
code: string;
message: string;
innerError?: {
code: string;
'request-id'?: string;
date?: string;
};
details?: Array<{
code: string;
message: string;
}>;
};
// Operation specific errors
if (resource === 'group') {
if (operation === 'create') {
} else if (operation === 'delete') {
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group to Delete' and try again",
});
}
} else if (operation === 'get') {
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group to Get' and try again",
});
}
} else if (operation === 'getAll') {
} else if (operation === 'update') {
if (
errorCode === 'BadRequest' &&
errorMessage === 'Empty Payload. JSON content expected.'
) {
// Ignore empty payload error. Currently n8n deletes the empty body object from the request.
return data;
}
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group to Update' and try again",
});
}
}
} else if (resource === 'user') {
if (operation === 'addGroup') {
if (
errorCode === 'Request_BadRequest' &&
errorMessage ===
"One or more added object references already exist for the following modified properties: 'members'."
) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'The user is already in the group',
description:
'The specified user cannot be added to the group because they are already a member',
});
} else if (errorCode === 'Request_ResourceNotFound') {
const group = this.getNodeParameter('group.value') as string;
if (errorMessage.includes(group)) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required group doesn't match any existing one",
description: "Double-check the value in the parameter 'Group' and try again",
});
} else {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User to Add' and try again",
});
}
}
} else if (operation === 'create') {
} else if (operation === 'delete') {
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User to Delete' and try again",
});
}
} else if (operation === 'get') {
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User to Get' and try again",
});
}
} else if (operation === 'getAll') {
} else if (operation === 'removeGroup') {
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'The user is not in the group',
description:
'The specified user cannot be removed from the group because they are not a member of the group',
});
} else if (
errorCode === 'Request_UnsupportedQuery' &&
errorMessage ===
"Unsupported referenced-object resource identifier for link property 'members'."
) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: 'The user ID is invalid',
description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
});
}
} else if (operation === 'update') {
if (
errorCode === 'BadRequest' &&
errorMessage === 'Empty Payload. JSON content expected.'
) {
// Ignore empty payload error. Currently n8n deletes the empty body object from the request.
return data;
}
if (errorCode === 'Request_ResourceNotFound') {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: "The required user doesn't match any existing one",
description: "Double-check the value in the parameter 'User to Update' and try again",
});
}
}
}
// Generic errors
if (
errorCode === 'Request_BadRequest' &&
errorMessage.startsWith('Invalid object identifier')
) {
const group = this.getNodeParameter('group.value', '') as string;
const parameterResource =
resource === 'group' || errorMessage.includes(group) ? 'group' : 'user';
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: `The ${parameterResource} ID is invalid`,
description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315',
});
}
if (errorDetails?.some((x) => x.code === 'ObjectConflict' || x.code === 'ConflictingObjects')) {
throw new NodeApiError(this.getNode(), response as unknown as JsonObject, {
message: `The ${resource} already exists`,
description: errorMessage,
});
}
throw new NodeApiError(this.getNode(), response as unknown as JsonObject);
}
return data;
}
export async function getGroupProperties(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#groups');
const metadata = await parseStringPromise(response as string, {
explicitArray: false,
});
/* eslint-disable */
const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema']
.find((x: any) => x['$']['Namespace'] === 'microsoft.graph')
['EntityType'].filter((x: any) =>
['entity', 'directoryObject', 'group'].includes(x['$']['Name']),
);
let properties = entities
.flatMap((x: any) => x['Property'])
.map((x: any) => x['$']['Name']) as string[];
/* eslint-enable */
properties = properties.filter(
(x) => !['id', 'isArchived', 'hasMembersWithLicenseErrors'].includes(x),
);
properties = properties.sort();
for (const property of properties) {
returnData.push({
name: property,
value: property,
});
}
return returnData;
}
export async function getUserProperties(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#users');
const metadata = await parseStringPromise(response as string, {
explicitArray: false,
});
/* eslint-disable */
const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema']
.find((x: any) => x['$']['Namespace'] === 'microsoft.graph')
['EntityType'].filter((x: any) =>
['entity', 'directoryObject', 'user'].includes(x['$']['Name']),
);
let properties = entities
.flatMap((x: any) => x['Property'])
.map((x: any) => x['$']['Name']) as string[];
/* eslint-enable */
// signInActivity requires AuditLog.Read.All
// mailboxSettings MailboxSettings.Read
properties = properties.filter(
(x) =>
!['id', 'deviceEnrollmentLimit', 'mailboxSettings', 'print', 'signInActivity'].includes(x),
);
properties = properties.sort();
for (const property of properties) {
returnData.push({
name: property,
value: property,
});
}
return returnData;
}
export async function getGroups(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: any;
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'/groups',
{},
undefined,
undefined,
paginationToken,
);
} else {
const qs: IDataObject = {
$select: 'id,displayName',
};
const headers: IDataObject = {};
if (filter) {
headers.ConsistencyLevel = 'eventual';
qs.$search = `"displayName:${filter}"`;
}
response = await microsoftApiRequest.call(this, 'GET', '/groups', {}, qs, headers);
}
const groups: Array<{
id: string;
displayName: string;
}> = response.value;
const results: INodeListSearchItems[] = groups
.map((g) => ({
name: g.displayName,
value: g.id,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return { results, paginationToken: response['@odata.nextLink'] };
}
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let response: any;
if (paginationToken) {
response = await microsoftApiRequest.call(
this,
'GET',
'/users',
{},
undefined,
undefined,
paginationToken,
);
} else {
const qs: IDataObject = {
$select: 'id,displayName',
};
const headers: IDataObject = {};
if (filter) {
qs.$filter = `startsWith(displayName, '${filter}') OR startsWith(userPrincipalName, '${filter}')`;
}
response = await microsoftApiRequest.call(this, 'GET', '/users', {}, qs, headers);
}
const users: Array<{
id: string;
displayName: string;
}> = response.value;
const results: INodeListSearchItems[] = users
.map((u) => ({
name: u.displayName,
value: u.id,
}))
.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
);
return { results, paginationToken: response['@odata.nextLink'] };
}

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.microsoftEntra",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftentra/"
}
]
}
}

View File

@@ -0,0 +1,115 @@
import type {
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { groupFields, groupOperations, userFields, userOperations } from './descriptions';
import { getGroupProperties, getGroups, getUserProperties, getUsers } from './GenericFunctions';
export class MicrosoftEntra implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft Entra ID',
name: 'microsoftEntra',
icon: {
light: 'file:microsoftEntra.svg',
dark: 'file:microsoftEntra.dark.svg',
},
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with Micosoft Entra ID API',
defaults: {
name: 'Micosoft Entra ID',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'microsoftEntraOAuth2Api',
required: true,
},
],
requestDefaults: {
baseURL: 'https://graph.microsoft.com/v1.0',
headers: {
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Group',
value: 'group',
},
{
name: 'User',
value: 'user',
},
],
default: 'user',
},
...groupOperations,
...groupFields,
...userOperations,
...userFields,
],
};
methods = {
loadOptions: {
getGroupProperties,
async getGroupPropertiesGetAll(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
// Filter items not supported for list endpoint
return (await getGroupProperties.call(this)).filter(
(x) =>
![
'allowExternalSenders',
'autoSubscribeNewMembers',
'hideFromAddressLists',
'hideFromOutlookClients',
'isSubscribedByMail',
'unseenCount',
].includes(x.value as string),
);
},
getUserProperties,
async getUserPropertiesGetAll(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
// Filter items not supported for list endpoint
return (await getUserProperties.call(this)).filter(
(x) =>
![
'aboutMe',
'birthday',
'hireDate',
'interests',
'mySite',
'pastProjects',
'preferredName',
'responsibilities',
'schools',
'skills',
'mailboxSettings',
].includes(x.value as string),
);
},
},
listSearch: {
getGroups,
getUsers,
},
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from './GroupDescription';
export * from './UserDescription';

View File

@@ -0,0 +1,4 @@
<svg width="42" height="40" viewBox="0 0 42 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M40.2158 22.1487C41.5024 23.6021 41.1669 25.7976 39.498 26.8398V26.8376L22.8313 37.2554C22.33 37.5682 21.7635 37.7399 21.189 37.7704L21.1882 37.7906C20.5527 37.7906 19.7243 37.6044 19.1687 37.2577L9.44873 31.1821C10.311 31.7199 11.7443 32.3177 13.2598 32.3177C14.6398 32.3177 15.9221 31.9177 16.9843 31.2355C16.9843 31.2355 16.9865 31.2355 16.9887 31.2332L21.5727 28.3589L21.5723 28.3681L30.7024 22.6621L21.0047 11.7198L21.2469 11.4465C22.4691 10.0532 24.3069 9.1665 26.3624 9.1665C27.4091 9.1665 28.398 9.40206 29.2802 9.81317L40.2158 22.1487Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M21.0022 2.22439V2.22217L21 2.22439L21.0022 2.22439ZM21.0022 2.22439C21.9415 2.22498 22.8827 2.60054 23.5467 3.35106L29.2778 9.8155C28.3956 9.40661 27.4089 9.16883 26.36 9.16883C24.3044 9.16883 22.4667 10.0555 21.2444 11.4488L21.0703 11.6453L21.0703 11.6484L11.3022 22.6644L21 28.7266L16.9889 31.2333C16.9867 31.2355 16.9844 31.2355 16.9844 31.2355C15.9222 31.9177 14.64 32.3177 13.26 32.3177C11.7444 32.3177 10.3111 31.7199 9.44888 31.1822C8.6711 30.6955 2.50221 26.8399 2.50221 26.8399C0.833325 25.7977 0.49777 23.6022 1.78444 22.1488L18.4511 3.34883C19.1091 2.60751 20.0714 2.2363 21.0022 2.22675V2.22439Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,8 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.44824 30.9595C9.31047 31.4972 10.7438 32.095 12.2594 32.095C13.6394 32.095 14.9216 31.695 15.9838 31.0128C15.9838 31.0128 15.986 31.0128 15.9883 31.0106L19.9994 28.5039V37.555C19.3638 37.555 18.7238 37.3817 18.1683 37.035L8.44824 30.9595Z" fill="#225086"/>
<path d="M17.4511 3.12667L0.784438 21.9267C-0.502231 23.38 -0.166675 25.5756 1.50222 26.6178C1.50222 26.6178 7.67112 30.4734 8.44889 30.96C9.31112 31.4978 10.7445 32.0956 12.26 32.0956C13.64 32.0956 14.9222 31.6956 15.9845 31.0134C15.9845 31.0134 15.9867 31.0134 15.9889 31.0112L20 28.5045L10.3022 22.4423L20.0022 11.5V2C19.06 2 18.1178 2.37556 17.4511 3.12667Z" fill="#66DDFF"/>
<path d="M10.3027 22.4422L10.4183 22.5133L20.0005 28.5045H20.0027V11.5022L20.0005 11.5L10.3027 22.4422Z" fill="#CBF8FF"/>
<path d="M38.498 26.6177C40.1669 25.5755 40.5025 23.3799 39.2158 21.9266L28.2802 9.591C27.398 9.17989 26.4091 8.94434 25.3624 8.94434C23.3069 8.94434 21.4691 9.831 20.2469 11.2243L20.0047 11.4977L29.7025 22.4399L20.0024 28.5021V37.5533C20.6402 37.5533 21.2758 37.3799 21.8313 37.0333L38.498 26.6155V26.6177Z" fill="#074793"/>
<path d="M20.0022 2V11.5L20.2444 11.2267C21.4667 9.83334 23.3044 8.94668 25.36 8.94668C26.4089 8.94668 27.3956 9.18445 28.2778 9.59334L22.5467 3.12889C21.8822 2.37778 20.94 2.00222 20 2.00222L20.0022 2Z" fill="#0294E4"/>
<path d="M29.7002 22.442L20.0024 11.502V28.502L29.7002 22.442Z" fill="#96BCC2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,775 @@
import { NodeConnectionType } from 'n8n-workflow';
import nock from 'nock';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
import * as Helpers from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
describe('Gong Node', () => {
const baseUrl = 'https://graph.microsoft.com/v1.0';
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('Group description', () => {
const tests: WorkflowTestData[] = [
{
description: 'should create group',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'create',
displayName: 'Group Display Name',
groupType: 'Unified',
mailEnabled: true,
mailNickname: 'MailNickname',
membershipType: 'DynamicMembership',
securityEnabled: true,
additionalFields: {
isAssignableToRole: true,
description: 'Group Description',
membershipRule: 'department -eq "Marketing"',
membershipRuleProcessingState: 'On',
preferredDataLocation: 'Preferred Data Location',
uniqueName: 'UniqueName',
visibility: 'Public',
},
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.createGroup],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'post',
path: '/groups',
statusCode: 201,
requestBody: {
displayName: 'Group Display Name',
mailNickname: 'MailNickname',
mailEnabled: true,
membershipRule: 'department -eq "Marketing"',
membershipRuleProcessingState: 'On',
securityEnabled: true,
groupTypes: ['Unified', 'DynamicMembership'],
},
responseBody: microsoftEntraApiResponse.postGroup,
},
{
method: 'patch',
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
statusCode: 204,
requestBody: {
description: 'Group Description',
preferredDataLocation: 'Preferred Data Location',
uniqueName: 'UniqueName',
visibility: 'Public',
},
responseBody: {},
},
],
},
},
{
description: 'should delete group',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'delete',
group: {
__rl: true,
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
mode: 'id',
},
options: {},
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.deleteGroup],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'delete',
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b',
statusCode: 204,
responseBody: {},
},
],
},
},
{
description: 'should get group',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'get',
group: {
__rl: true,
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
mode: 'id',
},
output: 'raw',
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroup],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'get',
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b',
statusCode: 200,
responseBody: microsoftEntraApiResponse.getGroup,
},
],
},
},
{
description: 'should get group with fields output and members',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'get',
group: {
__rl: true,
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
mode: 'id',
},
output: 'fields',
fields: [
'assignedLabels',
'assignedLicenses',
'createdDateTime',
'classification',
'deletedDateTime',
'description',
'displayName',
'expirationDateTime',
'groupTypes',
'visibility',
'unseenCount',
'theme',
'uniqueName',
'serviceProvisioningErrors',
'securityIdentifier',
'renewedDateTime',
'securityEnabled',
'autoSubscribeNewMembers',
'allowExternalSenders',
'licenseProcessingState',
'isManagementRestricted',
'isSubscribedByMail',
'isAssignableToRole',
'id',
'hideFromOutlookClients',
'hideFromAddressLists',
'onPremisesProvisioningErrors',
'onPremisesSecurityIdentifier',
'onPremisesSamAccountName',
'onPremisesNetBiosName',
'onPremisesSyncEnabled',
'preferredDataLocation',
'preferredLanguage',
'proxyAddresses',
'onPremisesLastSyncDateTime',
'onPremisesDomainName',
'membershipRuleProcessingState',
'membershipRule',
'mailNickname',
'mailEnabled',
'mail',
],
options: {
includeMembers: true,
},
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroupWithProperties],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'get',
path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b?$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,unseenCount,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,autoSubscribeNewMembers,allowExternalSenders,licenseProcessingState,isManagementRestricted,isSubscribedByMail,isAssignableToRole,id,hideFromOutlookClients,hideFromAddressLists,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$expand=members($select=id,accountEnabled,createdDateTime,displayName,employeeId,mail,securityIdentifier,userPrincipalName,userType)',
statusCode: 200,
responseBody: microsoftEntraApiResponse.getGroupWithProperties,
},
],
},
},
{
description: 'should get all groups with simple output',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'getAll',
returnAll: true,
filter: '',
output: 'simple',
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [new Array(102).fill(microsoftEntraNodeResponse.getGroup[0])],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'get',
path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility',
statusCode: 200,
responseBody: {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
'@odata.nextLink':
'https://graph.microsoft.com/v1.0/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
value: new Array(100).fill(microsoftEntraApiResponse.getGroup),
},
},
{
method: 'get',
path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
statusCode: 200,
responseBody: {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
value: new Array(2).fill(microsoftEntraApiResponse.getGroup),
},
},
],
},
},
{
description: 'should get limit 10 groups with raw output',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'getAll',
limit: 10,
filter: '',
output: 'raw',
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [new Array(10).fill(microsoftEntraNodeResponse.getGroup[0])],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'get',
path: '/groups?$top=10',
statusCode: 200,
responseBody: {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
'@odata.nextLink':
'https://graph.microsoft.com/v1.0/groups?$top=10&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA',
value: new Array(10).fill(microsoftEntraApiResponse.getGroup),
},
},
],
},
},
{
description: 'should get all groups with options and filter',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'getAll',
returnAll: true,
filter: "startswith(displayName,'group')",
output: 'fields',
fields: [
'assignedLabels',
'assignedLicenses',
'createdDateTime',
'classification',
'deletedDateTime',
'description',
'displayName',
'expirationDateTime',
'groupTypes',
'visibility',
'theme',
'uniqueName',
'serviceProvisioningErrors',
'securityIdentifier',
'renewedDateTime',
'securityEnabled',
'licenseProcessingState',
'isManagementRestricted',
'isAssignableToRole',
'onPremisesProvisioningErrors',
'onPremisesSecurityIdentifier',
'onPremisesSamAccountName',
'onPremisesNetBiosName',
'onPremisesSyncEnabled',
'preferredDataLocation',
'preferredLanguage',
'proxyAddresses',
'onPremisesLastSyncDateTime',
'onPremisesDomainName',
'membershipRuleProcessingState',
'membershipRule',
'mailNickname',
'mailEnabled',
'mail',
],
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [
new Array(102).fill(microsoftEntraNodeResponse.getGroupWithProperties[0]),
],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'get',
path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id",
statusCode: 200,
responseBody: {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
'@odata.nextLink':
"https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA",
value: new Array(100).fill(microsoftEntraApiResponse.getGroupWithProperties),
},
},
{
method: 'get',
path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA",
statusCode: 200,
responseBody: {
'@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups',
value: new Array(2).fill(microsoftEntraApiResponse.getGroupWithProperties),
},
},
],
},
},
{
description: 'should update group',
input: {
workflowData: {
nodes: [
{
parameters: {},
id: '416e4fc1-5055-4e61-854e-a6265256ac26',
name: "When clicking 'Test workflow'",
type: 'n8n-nodes-base.manualTrigger',
position: [820, 380],
typeVersion: 1,
},
{
parameters: {
resource: 'group',
operation: 'update',
group: {
__rl: true,
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
mode: 'id',
},
updateFields: {
allowExternalSenders: true,
autoSubscribeNewMembers: true,
description: 'Group Description',
displayName: 'Group Display Name',
mailNickname: 'MailNickname',
membershipRule: 'department -eq "Marketing"',
membershipRuleProcessingState: 'On',
preferredDataLocation: 'Preferred Data Location',
securityEnabled: true,
uniqueName: 'UniqueName',
visibility: 'Public',
},
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.updateGroup],
},
},
nock: {
baseUrl,
mocks: [
{
method: 'patch',
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
statusCode: 204,
requestBody: {
description: 'Group Description',
displayName: 'Group Display Name',
mailNickname: 'MailNickname',
membershipRule: 'department -eq "Marketing"',
membershipRuleProcessingState: 'On',
preferredDataLocation: 'Preferred Data Location',
securityEnabled: true,
uniqueName: 'UniqueName',
visibility: 'Public',
},
responseBody: {},
},
{
method: 'patch',
path: `/groups/${microsoftEntraApiResponse.postGroup.id}`,
statusCode: 204,
requestBody: {
allowExternalSenders: true,
autoSubscribeNewMembers: true,
},
responseBody: {},
},
],
},
},
];
const nodeTypes = Helpers.setup(tests);
test.each(tests)('$description', async (testData) => {
const { result } = await executeWorkflow(testData, nodeTypes);
const resultNodeData = Helpers.getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) =>
expect(resultData).toEqual(testData.output.nodeData[nodeName]),
);
expect(result.status).toEqual('success');
});
});
});

View File

@@ -0,0 +1,249 @@
import type {
ICredentialDataDecryptedObject,
IDataObject,
IHttpRequestOptions,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import nock from 'nock';
import { executeWorkflow } from '@test/nodes/ExecuteWorkflow';
import * as Helpers from '@test/nodes/Helpers';
import type { WorkflowTestData } from '@test/nodes/types';
import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks';
import { FAKE_CREDENTIALS_DATA } from '../../../../test/nodes/FakeCredentialsMap';
import { MicrosoftEntra } from '../MicrosoftEntra.node';
describe('Gong Node', () => {
const baseUrl = 'https://graph.microsoft.com/v1.0';
beforeEach(() => {
// https://github.com/nock/nock/issues/2057#issuecomment-663665683
if (!nock.isActive()) {
nock.activate();
}
});
describe('Credentials', () => {
const tests: WorkflowTestData[] = [
{
description: 'should use correct credentials',
input: {
workflowData: {
nodes: [
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [0, 0],
id: '1307e408-a8a5-464e-b858-494953e2f43b',
name: "When clicking 'Test workflow'",
},
{
parameters: {
resource: 'group',
operation: 'get',
group: {
__rl: true,
value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b',
mode: 'id',
},
filter: '',
output: 'raw',
requestOptions: {},
},
type: 'n8n-nodes-base.microsoftEntra',
typeVersion: 1,
position: [220, 0],
id: '3429f7f2-dfca-4b72-8913-43a582e96e66',
name: 'Micosoft Entra ID',
credentials: {
microsoftEntraOAuth2Api: {
id: 'Hot2KwSMSoSmMVqd',
name: 'Microsoft Entra ID (Azure Active Directory) account',
},
},
},
],
connections: {
"When clicking 'Test workflow'": {
main: [
[
{
node: 'Micosoft Entra ID',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
},
},
},
output: {
nodeExecutionOrder: ['Start'],
nodeData: {
'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroup],
},
},
},
];
beforeAll(() => {
nock.disableNetConnect();
jest
.spyOn(Helpers.CredentialsHelper.prototype, 'authenticate')
.mockImplementation(
async (
credentials: ICredentialDataDecryptedObject,
typeName: string,
requestParams: IHttpRequestOptions,
): Promise<IHttpRequestOptions> => {
if (typeName === 'microsoftEntraOAuth2Api') {
return {
...requestParams,
headers: {
authorization:
'bearer ' + (credentials.oauthTokenData as IDataObject).access_token,
},
};
} else {
return requestParams;
}
},
);
});
afterAll(() => {
nock.restore();
jest.restoreAllMocks();
});
nock(baseUrl)
.get(`/groups/${microsoftEntraApiResponse.getGroup.id}`)
.matchHeader(
'authorization',
'bearer ' + FAKE_CREDENTIALS_DATA.microsoftEntraOAuth2Api.oauthTokenData.access_token,
)
.reply(200, {
...microsoftEntraApiResponse.getGroup,
});
const nodeTypes = Helpers.setup(tests);
test.each(tests)('$description', async (testData) => {
const { result } = await executeWorkflow(testData, nodeTypes);
const resultNodeData = Helpers.getResultNodeData(result, testData);
resultNodeData.forEach(({ nodeName, resultData }) =>
expect(resultData).toEqual(testData.output.nodeData[nodeName]),
);
expect(result.status).toEqual('success');
});
});
describe('Load options', () => {
it('should load group properties', async () => {
const mockContext = {
helpers: {
requestWithAuthentication: jest
.fn()
.mockReturnValue(microsoftEntraApiResponse.metadata.groups),
},
getCurrentNodeParameter: jest.fn(),
} as unknown as ILoadOptionsFunctions;
const node = new MicrosoftEntra();
const properties = await node.methods.loadOptions.getGroupProperties.call(mockContext);
expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getGroupProperties);
});
it('should load user properties', async () => {
const mockContext = {
helpers: {
requestWithAuthentication: jest
.fn()
.mockReturnValue(microsoftEntraApiResponse.metadata.users),
},
getCurrentNodeParameter: jest.fn(),
} as unknown as ILoadOptionsFunctions;
const node = new MicrosoftEntra();
const properties = await node.methods.loadOptions.getUserProperties.call(mockContext);
expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getUserProperties);
});
});
describe('List search', () => {
it('should list search groups', async () => {
const mockResponse = {
value: Array.from({ length: 2 }, (_, i) => ({
id: (i + 1).toString(),
displayName: `Group ${i + 1}`,
})),
'@odata.nextLink': '',
};
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
const mockContext = {
helpers: {
requestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
const node = new MicrosoftEntra();
const listSearchResult = await node.methods.listSearch.getGroups.call(mockContext);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', {
method: 'GET',
url: 'https://graph.microsoft.com/v1.0/groups',
json: true,
headers: {},
body: {},
qs: {
$select: 'id,displayName',
},
});
expect(listSearchResult).toEqual({
results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })),
paginationToken: mockResponse['@odata.nextLink'],
});
});
it('should list search users', async () => {
const mockResponse = {
value: Array.from({ length: 2 }, (_, i) => ({
id: (i + 1).toString(),
displayName: `User ${i + 1}`,
})),
'@odata.nextLink': '',
};
const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse);
const mockContext = {
helpers: {
requestWithAuthentication: mockRequestWithAuthentication,
},
} as unknown as ILoadOptionsFunctions;
const node = new MicrosoftEntra();
const listSearchResult = await node.methods.listSearch.getUsers.call(mockContext);
expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', {
method: 'GET',
url: 'https://graph.microsoft.com/v1.0/users',
json: true,
headers: {},
body: {},
qs: {
$select: 'id,displayName',
},
});
expect(listSearchResult).toEqual({
results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })),
paginationToken: mockResponse['@odata.nextLink'],
});
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -626,6 +626,7 @@
"dist/nodes/MessageBird/MessageBird.node.js",
"dist/nodes/Metabase/Metabase.node.js",
"dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js",
"dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js",
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
"dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js",
"dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js",

View File

@@ -80,6 +80,30 @@ BQIDAQAB
},
baseUrl: 'https://api.gong.io',
},
microsoftEntraOAuth2Api: {
grantType: 'authorizationCode',
authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
clientId: 'CLIENTID',
clientSecret: 'CLIENTSECRET',
scope:
'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All Sites.FullControl.All',
authQueryParameters: 'response_mode=query',
authentication: 'body',
oauthTokenData: {
token_type: 'Bearer',
scope:
'AccessReview.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All EntitlementManagement.ReadWrite.All Group.ReadWrite.All NetworkAccessPolicy.ReadWrite.All openid Sites.FullControl.All User.DeleteRestore.All User.EnableDisableAccount.All User.Export.All User.Invite.All User.ManageIdentities.All User.Read User.Read.All User.ReadBasic.All User.ReadWrite User.ReadWrite.All User.RevokeSessions.All profile email',
expires_in: 4822,
ext_expires_in: 4822,
access_token: 'ACCESSTOKEN',
refresh_token: 'REFRESHTOKEN',
id_token: 'IDTOKEN',
callbackQueryString: {
session_state: 'SESSIONSTATE',
},
},
},
n8nApi: {
apiKey: 'key123',
baseUrl: 'https://test.app.n8n.cloud/api/v1',

View File

@@ -2451,7 +2451,7 @@ export interface WorkflowTestData {
nock?: {
baseUrl: string;
mocks: Array<{
method: 'delete' | 'get' | 'post' | 'put';
method: 'delete' | 'get' | 'patch' | 'post' | 'put';
path: string;
requestBody?: RequestBodyMatcher;
statusCode: number;