1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-10-06 09:37:36 +03:00

feat(Okta Node): Add Okta Node (#10278)

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Shireen Missi 2024-08-12 16:49:06 +01:00 committed by GitHub
parent 71b6c67179
commit 5cac0f339d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1414 additions and 4 deletions

View File

@ -33,8 +33,11 @@ function findReferencedMethods(obj, refs = {}, latestName = '') {
const knownCredentials = loader.known.credentials; const knownCredentials = loader.known.credentials;
const credentialTypes = Object.values(loader.credentialTypes).map((data) => { const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
const credentialType = data.type; const credentialType = data.type;
if (knownCredentials[credentialType.name].supportedNodes?.length > 0) { if (
delete credentialType.httpRequestNode; knownCredentials[credentialType.name].supportedNodes?.length > 0 &&
credentialType.httpRequestNode
) {
credentialType.httpRequestNode.hidden = true;
} }
return credentialType; return credentialType;
}); });

View File

@ -206,7 +206,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
}); });
const httpOnlyCredentialTypes = computed(() => { const httpOnlyCredentialTypes = computed(() => {
return allCredentialTypes.value.filter((credentialType) => credentialType.httpRequestNode); return allCredentialTypes.value.filter(
(credentialType) => credentialType.httpRequestNode && !credentialType.httpRequestNode.hidden,
);
}); });
// #endregion // #endregion

View File

@ -30,12 +30,13 @@ export class OktaApi implements ICredentialType {
placeholder: 'https://dev-123456.okta.com', placeholder: 'https://dev-123456.okta.com',
}, },
{ {
displayName: 'SSWS Access Token', displayName: 'Access Token',
name: 'accessToken', name: 'accessToken',
type: 'string', type: 'string',
typeOptions: { password: true }, typeOptions: { password: true },
required: true, required: true,
default: '', default: '',
description: 'Secure Session Web Service Access Token',
}, },
]; ];

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,56 @@
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { userFields, userOperations } from './UserDescription';
import { getUsers } from './UserFunctions';
export class Okta implements INodeType {
description: INodeTypeDescription = {
displayName: 'Okta',
name: 'okta',
icon: { light: 'file:Okta.svg', dark: 'file:Okta.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Use the Okta API',
defaults: {
name: 'Okta',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'oktaApi',
required: true,
},
],
requestDefaults: {
returnFullResponse: true,
baseURL: '={{$credentials.url.replace(new RegExp("/$"), "")}}',
headers: {},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'User',
value: 'user',
},
],
default: 'user',
},
// USER
...userOperations,
...userFields,
],
};
methods = {
listSearch: {
getUsers,
},
};
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,795 @@
import type { INodeProperties } from 'n8n-workflow';
import { getCursorPaginator, simplifyGetAllResponse, simplifyGetResponse } from './UserFunctions';
const BASE_API_URL = '/api/v1/users/';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
// Create Operation
{
name: 'Create',
value: 'create',
description: 'Create a new user',
routing: {
request: {
method: 'POST',
url: BASE_API_URL,
qs: { activate: '={{$parameter["activate"]}}' },
returnFullResponse: true,
},
},
action: 'Create a new user',
},
// Delete Operation
{
name: 'Delete',
value: 'delete',
description: 'Delete an existing user',
routing: {
request: {
method: 'DELETE',
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
returnFullResponse: true,
},
output: {
postReceive: [
{
type: 'set',
properties: {
value: '={{ { "success": true } }}',
},
},
],
},
},
action: 'Delete a user',
},
// Get Operation
{
name: 'Get',
value: 'get',
description: 'Get details of a user',
routing: {
request: {
method: 'GET',
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
returnFullResponse: true,
qs: {},
},
output: {
postReceive: [simplifyGetResponse],
},
},
action: 'Get a user',
},
// Get All Operation
{
name: 'Get Many',
value: 'getAll',
description: 'Get many users',
routing: {
request: {
method: 'GET',
url: BASE_API_URL,
qs: { search: '={{$parameter["searchQuery"]}}' },
returnFullResponse: true,
},
output: {
postReceive: [simplifyGetAllResponse],
},
send: {
paginate: true,
},
operations: {
pagination: getCursorPaginator(),
},
},
action: 'Get many users',
},
// Update Operation
{
name: 'Update',
value: 'update',
description: 'Update an existing user',
routing: {
request: {
method: 'POST',
url: '={{"/api/v1/users/" + $parameter["userId"]}}',
returnFullResponse: true,
},
},
action: 'Update a user',
},
],
default: 'getAll',
},
];
const mainProfileFields: INodeProperties[] = [
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
placeholder: 'e.g. Nathan',
default: '',
routing: {
send: {
property: 'profile.firstName',
type: 'body',
},
},
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
placeholder: 'e.g. Smith',
default: '',
routing: {
send: {
property: 'profile.lastName',
type: 'body',
},
},
},
{
displayName: 'Username',
name: 'login',
type: 'string',
placeholder: 'e.g. nathan@example.com',
hint: 'Unique identifier for the user, must be an email',
default: '',
routing: {
send: {
property: 'profile.login',
type: 'body',
},
},
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'e.g. nathan@example.com',
default: '',
routing: {
send: {
property: 'profile.email',
type: 'body',
},
},
},
];
const createFields: INodeProperties[] = [
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.city',
type: 'body',
},
},
},
{
displayName: 'Cost Center',
name: 'costCenter',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.costCenter',
type: 'body',
},
},
},
{
displayName: 'Country Code',
name: 'countryCode',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.countryCode',
type: 'body',
},
},
},
{
displayName: 'Department',
name: 'department',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.department',
type: 'body',
},
},
},
{
displayName: 'Display Name',
name: 'displayName',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.displayName',
type: 'body',
},
},
},
{
displayName: 'Division',
name: 'division',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.division',
type: 'body',
},
},
},
{
displayName: 'Employee Number',
name: 'employeeNumber',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.employeeNumber',
type: 'body',
},
},
},
{
displayName: 'Honorific Prefix',
name: 'honorificPrefix',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.honorificPrefix',
type: 'body',
},
},
},
{
displayName: 'Honorific Suffix',
name: 'honorificSuffix',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.honorificSuffix',
type: 'body',
},
},
},
{
displayName: 'Locale',
name: 'locale',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.locale',
type: 'body',
},
},
},
{
displayName: 'Manager',
name: 'manager',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.manager',
type: 'body',
},
},
},
{
displayName: 'ManagerId',
name: 'managerId',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.managerId',
type: 'body',
},
},
},
{
displayName: 'Middle Name',
name: 'middleName',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.middleName',
type: 'body',
},
},
},
{
displayName: 'Mobile Phone',
name: 'mobilePhone',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.mobilePhone',
type: 'body',
},
},
},
{
displayName: 'Nick Name',
name: 'nickName',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.nickName',
type: 'body',
},
},
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: { password: true },
default: '',
routing: {
send: {
property: 'credentials.password.value',
type: 'body',
},
},
},
{
displayName: 'Organization',
name: 'organization',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.organization',
type: 'body',
},
},
},
{
displayName: 'Postal Address',
name: 'postalAddress',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.postalAddress',
type: 'body',
},
},
},
{
displayName: 'Preferred Language',
name: 'preferredLanguage',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.preferredLanguage',
type: 'body',
},
},
},
{
displayName: 'Primary Phone',
name: 'primaryPhone',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.primaryPhone',
type: 'body',
},
},
},
{
displayName: 'Profile Url',
name: 'profileUrl',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.profileUrl',
type: 'body',
},
},
},
{
displayName: 'Recovery Question Answer',
name: 'recoveryQuestionAnswer',
type: 'string',
default: '',
routing: {
send: {
property: 'credentials.recovery_question.answer',
type: 'body',
},
},
},
{
displayName: 'Recovery Question Question',
name: 'recoveryQuestionQuestion',
type: 'string',
default: '',
routing: {
send: {
property: 'credentials.recovery_question.question',
type: 'body',
},
},
},
{
displayName: 'Second Email',
name: 'secondEmail',
type: 'string',
typeOptions: { email: true },
default: '',
routing: {
send: {
property: 'profile.secondEmail',
type: 'body',
},
},
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.state',
type: 'body',
},
},
},
{
displayName: 'Street Address',
name: 'streetAddress',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.streetAddress',
type: 'body',
},
},
},
{
displayName: 'Timezone',
name: 'timezone',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.timezone',
type: 'body',
},
},
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.title',
type: 'body',
},
},
},
{
displayName: 'User Type',
name: 'userType',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.userType',
type: 'body',
},
},
},
{
displayName: 'Zip Code',
name: 'zipCode',
type: 'string',
default: '',
routing: {
send: {
property: 'profile.zipCode',
type: 'body',
},
},
},
];
const updateFields: INodeProperties[] = createFields
.concat(mainProfileFields)
.sort((a, b) => a.displayName.localeCompare(b.displayName));
export const userFields: INodeProperties[] = [
// Fields for 'get', 'update', and 'delete' operations
{
displayName: 'User',
name: 'userId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a user...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
},
},
{
displayName: 'By username',
name: 'login',
type: 'string',
placeholder: '',
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 00u1abcd2345EfGHIjk6',
},
],
displayOptions: {
show: {
resource: ['user'],
operation: ['get', 'update', 'delete'],
},
},
description: 'The user you want to operate on. Choose from the list, or specify an ID.',
},
// Fields specific to 'create' operation
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
required: true,
placeholder: 'e.g. Nathan',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
property: 'profile.firstName',
type: 'body',
},
},
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
required: true,
placeholder: 'e.g. Smith',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
property: 'profile.lastName',
type: 'body',
},
},
},
{
displayName: 'Username',
name: 'login',
type: 'string',
required: true,
placeholder: 'e.g. nathan@example.com',
hint: 'Unique identifier for the user, must be an email',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
property: 'profile.login',
type: 'body',
},
},
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
placeholder: 'e.g. nathan@example.com',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: '',
routing: {
send: {
property: 'profile.email',
type: 'body',
},
},
},
{
displayName: 'Activate',
name: 'activate',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: true,
description: 'Whether to activate the user and allow access to all assigned applications',
},
{
displayName: 'Fields',
name: 'getCreateFields',
type: 'collection',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
default: {},
placeholder: 'Add field',
options: createFields,
},
// Fields for 'update' operations
{
displayName: 'Fields',
name: 'getUpdateFields',
type: 'collection',
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
default: {},
placeholder: 'Add field',
options: updateFields,
},
// Fields specific to 'getAll' operation
{
displayName: 'Search Query',
name: 'searchQuery',
type: 'string',
placeholder: 'e.g. profile.lastName sw "Smi"',
hint: 'Filter users by using the allowed syntax. <a href="https://developer.okta.com/docs/reference/core-okta-api/#filter" target="_blank">More info</a>.',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: '',
routing: {
request: {
qs: {
prefix: '={{$value}}',
},
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 200,
},
default: 20,
routing: {
send: {
type: 'query',
property: 'limit',
},
output: {
maxResults: '={{$value}}', // Set maxResults to the value of current parameter
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
// Fields for 'get' and 'getAll' operations
{
displayName: 'Simplify',
name: 'simplify',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['get', 'getAll'],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
// Fields specific to 'delete' operation
{
displayName: 'Send Email',
name: 'sendEmail',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['delete'],
},
},
default: false,
description: 'Whether to send a deactivation email to the administrator',
},
];

View File

@ -0,0 +1,170 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHookFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
INodeListSearchResult,
INodePropertyOptions,
} from 'n8n-workflow';
type OktaUser = {
status: string;
created: string;
activated: string;
lastLogin: string;
lastUpdated: string;
passwordChanged: string;
profile: {
login: string;
email: string;
firstName: string;
lastName: string;
};
id: string;
};
export async function oktaApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject = {},
qs: IDataObject = {},
url?: string,
option: IDataObject = {},
): Promise<OktaUser[]> {
const credentials = await this.getCredentials('oktaApi');
const baseUrl = `${credentials.url as string}/api/v1/${resource}`;
const options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body: Object.keys(body).length ? body : undefined,
qs: Object.keys(qs).length ? qs : undefined,
url: url ?? baseUrl,
json: true,
...option,
};
return await (this.helpers.httpRequestWithAuthentication.call(
this,
'oktaApi',
options,
) as Promise<OktaUser[]>);
}
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const responseData: OktaUser[] = await oktaApiRequest.call(this, 'GET', '/users/');
const filteredUsers = responseData.filter((user) => {
if (!filter) return true;
const username = `${user.profile.login}`.toLowerCase();
return username.includes(filter.toLowerCase());
});
const users: INodePropertyOptions[] = filteredUsers.map((user) => ({
name: `${user.profile.login}`,
value: user.id,
}));
return {
results: users,
};
}
function simplifyOktaUser(item: OktaUser): IDataObject {
return {
id: item.id,
status: item.status,
created: item.created,
activated: item.activated,
lastLogin: item.lastLogin,
lastUpdated: item.lastUpdated,
passwordChanged: item.passwordChanged,
profile: {
firstName: item.profile.firstName,
lastName: item.profile.lastName,
login: item.profile.login,
email: item.profile.email,
},
};
}
export async function simplifyGetAllResponse(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
if (items.length === 0) return items;
const simplify = this.getNodeParameter('simplify');
if (!simplify)
return ((items[0].json as unknown as IDataObject[]) ?? []).map((item: IDataObject) => ({
json: item,
headers: _response.headers,
})) as INodeExecutionData[];
let simplifiedItems: INodeExecutionData[] = [];
if (items[0].json) {
const jsonArray = items[0].json as unknown;
simplifiedItems = (jsonArray as OktaUser[]).map((item: OktaUser) => {
const simplifiedItem = simplifyOktaUser(item);
return {
json: simplifiedItem,
headers: _response.headers,
};
});
}
return simplifiedItems;
}
export async function simplifyGetResponse(
this: IExecuteSingleFunctions,
items: INodeExecutionData[],
_response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
const simplify = this.getNodeParameter('simplify');
if (!simplify) return items;
const item = items[0].json as OktaUser;
const simplifiedItem = simplifyOktaUser(item);
return [
{
json: simplifiedItem,
},
] as INodeExecutionData[];
}
export const getCursorPaginator = () => {
return async function cursorPagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
if (!requestOptions.options.qs) {
requestOptions.options.qs = {};
}
let items: INodeExecutionData[] = [];
let responseData: INodeExecutionData[];
let nextCursor: string | undefined = undefined;
const returnAll = this.getNodeParameter('returnAll', true) as boolean;
do {
requestOptions.options.qs.limit = 200;
requestOptions.options.qs.after = nextCursor;
responseData = await this.makeRoutingRequest(requestOptions);
if (responseData.length > 0) {
const headers = responseData[responseData.length - 1].headers;
const headersLink = (headers as IDataObject)?.link as string | undefined;
nextCursor = headersLink?.split('after=')[1]?.split('&')[0]?.split('>')[0];
}
items = items.concat(responseData);
} while (returnAll && nextCursor);
return items;
};
};

View File

@ -0,0 +1,375 @@
import type {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IN8nHttpFullResponse,
INodeExecutionData,
} from 'n8n-workflow';
import {
getCursorPaginator,
getUsers,
oktaApiRequest,
simplifyGetAllResponse,
simplifyGetResponse,
} from '../UserFunctions';
describe('oktaApiRequest', () => {
const mockGetCredentials = jest.fn();
const mockHttpRequestWithAuthentication = jest.fn();
const mockContext = {
getCredentials: mockGetCredentials,
helpers: {
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
},
} as unknown as IExecuteFunctions;
beforeEach(() => {
mockGetCredentials.mockClear();
mockHttpRequestWithAuthentication.mockClear();
});
it('should make a GET request and return data', async () => {
mockGetCredentials.mockResolvedValue({ url: 'https://okta.example.com' });
mockHttpRequestWithAuthentication.mockResolvedValue([
{ profile: { firstName: 'John', lastName: 'Doe' }, id: '1' },
]);
const response = await oktaApiRequest.call(mockContext, 'GET', 'users');
expect(mockGetCredentials).toHaveBeenCalledWith('oktaApi');
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('oktaApi', {
headers: { 'Content-Type': 'application/json' },
method: 'GET',
body: undefined,
qs: undefined,
url: 'https://okta.example.com/api/v1/users',
json: true,
});
expect(response).toEqual([{ profile: { firstName: 'John', lastName: 'Doe' }, id: '1' }]);
});
// Tests for error handling
it('should handle errors from oktaApiRequest', async () => {
mockHttpRequestWithAuthentication.mockRejectedValue(new Error('Network error'));
await expect(oktaApiRequest.call(mockContext, 'GET', 'users')).rejects.toThrow('Network error');
});
});
describe('getUsers', () => {
const mockOktaApiRequest = jest.fn();
const mockContext = {
getCredentials: jest.fn().mockResolvedValue({ url: 'https://okta.example.com' }),
helpers: {
httpRequestWithAuthentication: mockOktaApiRequest,
},
} as unknown as ILoadOptionsFunctions;
beforeEach(() => {
mockOktaApiRequest.mockClear();
});
it('should return users with filtering', async () => {
mockOktaApiRequest.mockResolvedValue([
{ profile: { login: 'John@example.com' }, id: '1' },
{ profile: { login: 'Jane@example.com' }, id: '2' },
]);
const response = await getUsers.call(mockContext, 'john');
expect(response).toEqual({
results: [{ name: 'John@example.com', value: '1' }],
});
});
it('should return all users when no filter is applied', async () => {
mockOktaApiRequest.mockResolvedValue([
{ profile: { login: 'John@example.com' }, id: '1' },
{ profile: { login: 'Jane@example.com' }, id: '2' },
]);
const response = await getUsers.call(mockContext);
expect(response).toEqual({
results: [
{ name: 'John@example.com', value: '1' },
{ name: 'Jane@example.com', value: '2' },
],
});
});
// Tests for empty results
it('should handle empty results from oktaApiRequest', async () => {
mockOktaApiRequest.mockResolvedValue([]);
const response = await getUsers.call(mockContext);
expect(response).toEqual({
results: [],
});
});
});
describe('simplifyGetAllResponse', () => {
const mockGetNodeParameter = jest.fn();
const mockContext = {
getNodeParameter: mockGetNodeParameter,
} as unknown as IExecuteSingleFunctions;
const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
const items: INodeExecutionData[] = [
{
json: [
{
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
some_item: 'some_value',
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
some_profile_item: 'some_profile_value',
},
},
] as unknown as IDataObject,
},
];
beforeEach(() => {
mockGetNodeParameter.mockClear();
});
it('should return items unchanged when simplify parameter is not set', async () => {
mockGetNodeParameter.mockReturnValueOnce(false);
const expectedResult: INodeExecutionData[] = [
{
json: {
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
some_item: 'some_value',
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
some_profile_item: 'some_profile_value',
},
},
},
];
const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
expect(result).toEqual(expectedResult);
});
it('should simplify items correctly', async () => {
mockGetNodeParameter.mockReturnValueOnce(true);
const expectedResult: INodeExecutionData[] = [
{
json: {
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
},
},
},
];
const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
expect(result).toEqual(expectedResult);
});
it('should return an empty array when items is an empty array', async () => {
mockGetNodeParameter.mockReturnValueOnce(false);
const emptyArrayItems: INodeExecutionData[] = [];
const result = await simplifyGetAllResponse.call(mockContext, emptyArrayItems, mockResponse);
expect(result).toEqual([]);
});
});
describe('simplifyGetResponse', () => {
const mockGetNodeParameter = jest.fn();
const mockContext = {
getNodeParameter: mockGetNodeParameter,
} as unknown as IExecuteSingleFunctions;
const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
const items: INodeExecutionData[] = [
{
json: {
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
some_item: 'some_value',
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
some_profile_item: 'some_profile_value',
},
} as unknown as IDataObject,
},
];
beforeEach(() => {
mockGetNodeParameter.mockClear();
});
it('should return the item unchanged when simplify parameter is not set', async () => {
mockGetNodeParameter.mockReturnValueOnce(false);
const expectedResult: INodeExecutionData[] = [
{
json: {
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
some_item: 'some_value',
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
some_profile_item: 'some_profile_value',
},
},
},
];
const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
expect(result).toEqual(expectedResult);
});
it('should simplify the item correctly', async () => {
mockGetNodeParameter.mockReturnValueOnce(true);
const expectedResult: INodeExecutionData[] = [
{
json: {
id: '01',
status: 'ACTIVE',
created: '2023-01-01T00:00:00.000Z',
activated: '2023-01-01T00:00:01.000Z',
lastLogin: null,
lastUpdated: '2023-01-01T00:00:01.000Z',
passwordChanged: null,
profile: {
firstName: 'John',
lastName: 'Doe',
login: 'john.doe@example.com',
email: 'john.doe@example.com',
},
},
},
];
const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
expect(result).toEqual(expectedResult);
});
});
describe('getCursorPaginator', () => {
let mockContext: IExecutePaginationFunctions;
let mockRequestOptions: DeclarativeRestApiSettings.ResultOptions;
const baseUrl = 'https://api.example.com';
beforeEach(() => {
mockContext = {
getNodeParameter: jest.fn(),
makeRoutingRequest: jest.fn(),
} as unknown as IExecutePaginationFunctions;
mockRequestOptions = {
options: {
qs: {},
},
} as DeclarativeRestApiSettings.ResultOptions;
});
it('should return all items when returnAll is true', async () => {
const mockResponseData: INodeExecutionData[] = [
{ json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
{ json: { id: 2 }, headers: { link: `<${baseUrl}?after=cursor2>` } },
{ json: { id: 3 }, headers: { link: `<${baseUrl}>` } },
];
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
(mockContext.makeRoutingRequest as jest.Mock)
.mockResolvedValueOnce([mockResponseData[0]])
.mockResolvedValueOnce([mockResponseData[1]])
.mockResolvedValueOnce([mockResponseData[2]]);
const paginator = getCursorPaginator().bind(mockContext);
const result = await paginator(mockRequestOptions);
expect(result).toEqual(mockResponseData);
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(3);
});
it('should return items until nextCursor is undefined', async () => {
const mockResponseData: INodeExecutionData[] = [
{ json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
{ json: { id: 2 }, headers: { link: `<${baseUrl}>` } },
];
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
(mockContext.makeRoutingRequest as jest.Mock)
.mockResolvedValueOnce([mockResponseData[0]])
.mockResolvedValueOnce([mockResponseData[1]]);
const paginator = getCursorPaginator().bind(mockContext);
const result = await paginator(mockRequestOptions);
expect(result).toEqual(mockResponseData);
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(2);
});
it('should handle empty response data', async () => {
(mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
(mockContext.makeRoutingRequest as jest.Mock).mockResolvedValue([]);
const paginator = getCursorPaginator().bind(mockContext);
const result = await paginator(mockRequestOptions);
expect(result).toEqual([]);
expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(1);
});
});

View File

@ -648,6 +648,7 @@
"dist/nodes/Notion/NotionTrigger.node.js", "dist/nodes/Notion/NotionTrigger.node.js",
"dist/nodes/Npm/Npm.node.js", "dist/nodes/Npm/Npm.node.js",
"dist/nodes/Odoo/Odoo.node.js", "dist/nodes/Odoo/Odoo.node.js",
"dist/nodes/Okta/Okta.node.js",
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js", "dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
"dist/nodes/OpenAi/OpenAi.node.js", "dist/nodes/OpenAi/OpenAi.node.js",
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js", "dist/nodes/OpenThesaurus/OpenThesaurus.node.js",

View File

@ -308,6 +308,7 @@ export interface ICredentialTestRequestData {
type ICredentialHttpRequestNode = { type ICredentialHttpRequestNode = {
name: string; name: string;
docsUrl: string; docsUrl: string;
hidden?: boolean;
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string }); } & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
export interface ICredentialType { export interface ICredentialType {