feat(Google Workspace Admin Node): Google Admin Node Overhaul implementation (#12271)

Co-authored-by: knowa <github@libertyunion.org>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
Stanimira Rikova
2025-05-02 19:27:08 +03:00
committed by GitHub
parent bd258be052
commit 8a30c35c33
44 changed files with 5452 additions and 454 deletions

View File

@@ -5,6 +5,8 @@ const scopes = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.domain.readonly',
'https://www.googleapis.com/auth/admin.directory.userschema.readonly',
'https://www.googleapis.com/auth/admin.directory.device.chromeos',
'https://www.googleapis.com/auth/admin.directory.orgunit.readonly',
];
export class GSuiteAdminOAuth2Api implements ICredentialType {

View File

@@ -0,0 +1,361 @@
import type { INodeProperties } from 'n8n-workflow';
export const deviceOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['device'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a ChromeOS device',
action: 'Get ChromeOS device',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many ChromeOS devices',
action: 'Get many ChromeOS devices',
},
{
name: 'Update',
value: 'update',
description: 'Update a ChromeOS device',
action: 'Update ChromeOS device',
},
{
name: 'Change Status',
value: 'changeStatus',
description: 'Change the status of a ChromeOS device',
action: 'Change status of ChromeOS device',
},
],
default: 'get',
},
];
export const deviceFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* device */
/* -------------------------------------------------------------------------- */
{
displayName: 'Device',
name: 'deviceId',
type: 'resourceLocator',
required: true,
displayOptions: {
show: {
operation: ['get', 'update', 'changeStatus'],
resource: ['device'],
},
},
default: {
mode: 'list',
value: '',
},
description: 'Select the device you want to retrieve',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchDevices',
},
},
{
displayName: 'By ID',
name: 'deviceId',
type: 'string',
hint: 'Enter the device id',
placeholder: 'e.g. 123e4567-e89b-12d3-a456-426614174000',
},
],
},
/* -------------------------------------------------------------------------- */
/* device:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* device:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Output',
name: 'projection',
type: 'options',
required: true,
options: [
{
name: 'Basic',
value: 'basic',
description: 'Do not include any custom fields for the user',
},
{
name: 'Full',
value: 'full',
description: 'Include all fields associated with this user',
},
],
displayOptions: {
show: {
operation: ['get', 'getAll'],
resource: ['device'],
},
},
default: 'basic',
description: 'What subset of fields to fetch for this device',
},
{
displayName: 'Include Children',
name: 'includeChildOrgunits',
type: 'boolean',
default: false,
description:
'Whether to include devices from organizational units below your specified organizational unit',
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
},
{
displayName: 'Filter',
name: 'filter',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
options: [
{
displayName: 'Organizational Unit Name or ID',
name: 'orgUnitPath',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrgUnits',
},
default: [],
description:
'Specify the organizational unit name or ID. Choose from the list or use an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
placeholder: 'e.g. name:contact* email:contact*',
default: '',
description: "Use Google's querying syntax to filter results",
},
],
},
{
displayName: 'Sort',
name: 'sort',
type: 'fixedCollection',
placeholder: 'Add Sort Rule',
default: {},
displayOptions: {
show: {
operation: ['getAll'],
resource: ['device'],
},
},
options: [
{
name: 'sortRules',
displayName: 'Sort Rules',
values: [
{
displayName: 'Order By',
name: 'orderBy',
type: 'options',
options: [
{
name: 'Annotated Location',
value: 'annotatedLocation',
},
{
name: 'Annotated User',
value: 'annotatedUser',
},
{
name: 'Last Sync',
value: 'lastSync',
},
{
name: 'Notes',
value: 'notes',
},
{
name: 'Serial Number',
value: 'serialNumber',
},
{
name: 'Status',
value: 'status',
},
],
default: '',
description: 'Field to sort the results by',
},
{
displayName: 'Sort Order',
name: 'sortBy',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: '',
description: 'Sort order direction',
},
],
},
],
description: 'Define sorting rules for the results',
},
/* -------------------------------------------------------------------------- */
/* device:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Update Fields',
name: 'updateOptions',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
operation: ['update'],
resource: ['device'],
},
},
options: [
{
displayName: 'Move to Organizational Unit Name or ID',
name: 'orgUnitPath',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrgUnits',
},
default: [],
description:
'A comma-separated list of schema names. All fields from these schemas are fetched. This should only be set when projection=custom. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Annotated User',
name: 'annotatedUser',
type: 'string',
default: '',
description: 'The annotated user of the device',
placeholder: 'e.g. help desk',
},
{
displayName: 'Annotated Location',
name: 'annotatedLocation',
type: 'string',
default: '',
description: 'The annotated location of the device',
placeholder: 'e.g. Mountain View help desk Chromebook',
},
{
displayName: 'Annotated Asset ID',
name: 'annotatedAssetId',
type: 'string',
default: '',
description: 'The annotated asset ID of a device',
placeholder: 'e.g. 1234567890',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Add notes to a device',
placeholder: 'e.g. Loaned from support',
},
],
},
/* -------------------------------------------------------------------------- */
/* device:changeStatus */
/* -------------------------------------------------------------------------- */
{
displayName: 'Status',
name: 'action',
type: 'options',
required: true,
options: [
{
name: 'Enabled',
value: 'reenable',
description: 'Re-enable a disabled chromebook',
action: 'Enable a device',
},
{
name: 'Disabled',
value: 'disable',
description: 'Disable a chromebook',
action: 'Disable a device',
},
],
displayOptions: {
show: {
operation: ['changeStatus'],
resource: ['device'],
},
},
default: 'reenable',
description: 'Set the status of a device',
},
];

View File

@@ -9,16 +9,17 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { deviceFields, deviceOperations } from './DeviceDescription';
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
import { groupFields, groupOperations } from './GroupDescripion';
import { searchDevices, searchGroups, searchUsers } from './SearchFunctions';
import { userFields, userOperations } from './UserDescription';
export class GSuiteAdmin implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Workspace Admin',
name: 'gSuiteAdmin',
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:google-workspace-admin.png',
icon: 'file:gSuiteAdmin.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
@@ -42,6 +43,10 @@ export class GSuiteAdmin implements INodeType {
type: 'options',
noDataExpression: true,
options: [
{
name: 'ChromeOS Device',
value: 'device',
},
{
name: 'Group',
value: 'group',
@@ -53,6 +58,8 @@ export class GSuiteAdmin implements INodeType {
],
default: 'user',
},
...deviceOperations,
...deviceFields,
...groupOperations,
...groupFields,
...userOperations,
@@ -62,16 +69,16 @@ export class GSuiteAdmin implements INodeType {
methods = {
loadOptions: {
// Get all the domains to display them to user so that they can
// select them easily
async getDomains(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const domains = await googleApiRequestAllItems.call(
const domains = (await googleApiRequestAllItems.call(
this,
'domains',
'GET',
'/directory/v1/customer/my_customer/domains',
);
)) as Array<{
domainName: string;
}>;
for (const domain of domains) {
const domainName = domain.domainName;
const domainId = domain.domainName;
@@ -82,47 +89,180 @@ export class GSuiteAdmin implements INodeType {
}
return returnData;
},
// Get all the schemas to display them to user so that they can
// select them easily
async getSchemas(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const schemas = await googleApiRequestAllItems.call(
const schemas = (await googleApiRequestAllItems.call(
this,
'schemas',
'GET',
'/directory/v1/customer/my_customer/schemas',
);
for (const schema of schemas) {
const schemaName = schema.displayName;
const schemaId = schema.schemaName;
)) as Array<{
displayName: string;
schemaName: string;
}>;
return schemas.map((schema: { schemaName: string; displayName: string }) => ({
name: schema.displayName || schema.schemaName,
value: schema.schemaName,
}));
},
async getOrgUnits(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const orgUnits = (await googleApiRequest.call(
this,
'GET',
'/directory/v1/customer/my_customer/orgunits',
{},
{ orgUnitPath: '/', type: 'all' },
)) as {
organizationUnits: Array<{
name: string;
orgUnitPath: string;
}>;
};
for (const unit of orgUnits.organizationUnits) {
returnData.push({
name: schemaName,
value: schemaId,
name: unit.name,
value: unit.orgUnitPath,
});
}
return returnData;
},
},
listSearch: {
searchDevices,
searchGroups,
searchUsers,
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < length; i++) {
const qs: IDataObject = {};
try {
if (resource === 'group') {
if (resource === 'device') {
//https://developers.google.com/admin-sdk/directory/v1/customer/my_customer/devices/chromeos/deviceId
if (operation === 'get') {
const deviceId = this.getNodeParameter('deviceId', i, undefined, {
extractValue: true,
}) as string;
const output = this.getNodeParameter('projection', 1);
responseData = await googleApiRequest.call(
this,
'GET',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}?projection=${output}`,
{},
);
}
//https://developers.google.com/admin-sdk/directory/reference/rest/v1/chromeosdevices/list
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const output = this.getNodeParameter('projection', 1);
const includeChildren = this.getNodeParameter('includeChildOrgunits', i);
const filter = this.getNodeParameter('filter', i, {}) as {
query?: string;
orgUnitPath?: string;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
qs.projection = output;
qs.includeChildOrgunits = includeChildren;
if (qs.customer === undefined) {
qs.customer = 'my_customer';
}
if (filter.orgUnitPath) {
qs.orgUnitPath = filter.orgUnitPath;
}
if (filter.query) {
qs.query = filter.query.trim();
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
if (!returnAll) {
qs.maxResults = this.getNodeParameter('limit', i);
}
responseData = await googleApiRequest.call(
this,
'GET',
`/directory/v1/customer/${qs.customer}/devices/chromeos/`,
{},
qs,
);
if (!Array.isArray(responseData) || responseData.length === 0) {
return [this.helpers.returnJsonArray({})];
}
return [this.helpers.returnJsonArray(responseData)];
}
if (operation === 'update') {
const deviceId = this.getNodeParameter('deviceId', i, undefined, {
extractValue: true,
}) as string;
const updateOptions = this.getNodeParameter('updateOptions', 1);
Object.assign(qs, updateOptions);
responseData = await googleApiRequest.call(
this,
'PUT',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}`,
qs,
);
}
if (operation === 'changeStatus') {
const deviceId = this.getNodeParameter('deviceId', i, undefined, {
extractValue: true,
}) as string;
const action = this.getNodeParameter('action', 1);
qs.action = action;
responseData = await googleApiRequest.call(
this,
'POST',
`/directory/v1/customer/my_customer/devices/chromeos/${deviceId}/action`,
qs,
);
}
} else if (resource === 'group') {
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/insert
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const email = this.getNodeParameter('email', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
name,
email,
};
@@ -133,21 +273,20 @@ export class GSuiteAdmin implements INodeType {
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/delete
if (operation === 'delete') {
const groupId = this.getNodeParameter('groupId', i) as string;
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call(
this,
'DELETE',
`/directory/v1/groups/${groupId}`,
{},
);
await googleApiRequest.call(this, 'DELETE', `/directory/v1/groups/${groupId}`, {});
responseData = { success: true };
}
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/get
if (operation === 'get') {
const groupId = this.getNodeParameter('groupId', i) as string;
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call(
this,
@@ -156,19 +295,66 @@ export class GSuiteAdmin implements INodeType {
{},
);
}
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/list
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const filter = this.getNodeParameter('filter', i, {}) as {
customer?: string;
domain?: string;
query?: string;
userId?: string;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
const options = this.getNodeParameter('options', i);
if (filter.customer) {
qs.customer = filter.customer;
}
Object.assign(qs, options);
if (filter.domain) {
qs.domain = filter.domain;
}
if (qs.customer === undefined) {
if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
this.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
if (filter.userId) {
qs.userId = filter.userId;
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
if (!qs.customer) {
qs.customer = 'my_customer';
}
if (!returnAll) {
qs.maxResults = this.getNodeParameter('limit', i);
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
@@ -180,7 +366,6 @@ export class GSuiteAdmin implements INodeType {
);
} else {
qs.maxResults = this.getNodeParameter('limit', i);
responseData = await googleApiRequest.call(
this,
'GET',
@@ -188,14 +373,15 @@ export class GSuiteAdmin implements INodeType {
{},
qs,
);
responseData = responseData.groups;
responseData = responseData.groups || [];
}
}
//https://developers.google.com/admin-sdk/directory/v1/reference/groups/update
if (operation === 'update') {
const groupId = this.getNodeParameter('groupId', i) as string;
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', i);
@@ -210,23 +396,62 @@ export class GSuiteAdmin implements INodeType {
body,
);
}
}
} else if (resource === 'user') {
//https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert
if (operation === 'addToGroup') {
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
let userEmail: string | undefined;
// If the user ID is not already an email, fetch the user details
if (!userId.includes('@')) {
const userDetails = (await googleApiRequest.call(
this,
'GET',
`/directory/v1/users/${userId}`,
)) as {
primaryEmail: string;
};
userEmail = userDetails.primaryEmail;
} else {
userEmail = userId;
}
if (!userEmail) {
throw new NodeOperationError(
this.getNode(),
'Unable to determine the user email for adding to the group',
{ itemIndex: i },
);
}
const body: IDataObject = {
email: userEmail,
role: 'MEMBER',
};
await googleApiRequest.call(
this,
'POST',
`/directory/v1/groups/${groupId}/members`,
body,
);
responseData = { added: true };
}
if (resource === 'user') {
//https://developers.google.com/admin-sdk/directory/v1/reference/users/insert
if (operation === 'create') {
const domain = this.getNodeParameter('domain', i) as string;
const firstName = this.getNodeParameter('firstName', i) as string;
const lastName = this.getNodeParameter('lastName', i) as string;
const password = this.getNodeParameter('password', i) as string;
const username = this.getNodeParameter('username', i) as string;
const makeAdmin = this.getNodeParameter('makeAdmin', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
@@ -238,22 +463,63 @@ export class GSuiteAdmin implements INodeType {
primaryEmail: `${username}@${domain}`,
};
Object.assign(body, additionalFields);
if (!username) {
throw new NodeOperationError(this.getNode(), "The parameter 'Username' is empty", {
itemIndex: i,
description: "Please fill in the 'Username' parameter to create the user",
});
}
if (additionalFields.phoneUi) {
const phones = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[];
body.phones = phones;
delete body.phoneUi;
body.phones = (additionalFields.phoneUi as IDataObject).phoneValues as IDataObject[];
}
if (additionalFields.emailUi) {
const emails = (additionalFields.emailUi as IDataObject).emailValues as IDataObject[];
body.emails = (additionalFields.emailUi as IDataObject).emailValues as IDataObject[];
}
body.emails = emails;
if (additionalFields.roles) {
const roles = additionalFields.roles as string[];
body.roles = {
superAdmin: roles.includes('superAdmin'),
groupsAdmin: roles.includes('groupsAdmin'),
groupsReader: roles.includes('groupsReader'),
groupsEditor: roles.includes('groupsEditor'),
userManagement: roles.includes('userManagement'),
helpDeskAdmin: roles.includes('helpDeskAdmin'),
servicesAdmin: roles.includes('servicesAdmin'),
inventoryReportingAdmin: roles.includes('inventoryReportingAdmin'),
storageAdmin: roles.includes('storageAdmin'),
directorySyncAdmin: roles.includes('directorySyncAdmin'),
mobileAdmin: roles.includes('mobileAdmin'),
};
}
delete body.emailUi;
if (additionalFields.customFields) {
const customFields = (additionalFields.customFields as IDataObject)
.fieldValues as IDataObject[];
const customSchemas: IDataObject = {};
customFields.forEach((field) => {
const { schemaName, fieldName, value } = field as {
schemaName: string;
fieldName: string;
value: string;
};
if (!schemaName || !fieldName || !value) {
throw new NodeOperationError(this.getNode(), 'Invalid custom field data', {
itemIndex: i,
description: 'Schema name, field name, and value are required.',
});
}
customSchemas[schemaName] ??= {};
(customSchemas[schemaName] as IDataObject)[fieldName] = value;
});
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
}
responseData = await googleApiRequest.call(
@@ -263,22 +529,13 @@ export class GSuiteAdmin implements INodeType {
body,
qs,
);
if (makeAdmin) {
await googleApiRequest.call(
this,
'POST',
`/directory/v1/users/${responseData.id}/makeAdmin`,
{ status: true },
);
responseData.isAdmin = true;
}
}
//https://developers.google.com/admin-sdk/directory/v1/reference/users/delete
if (operation === 'delete') {
const userId = this.getNodeParameter('userId', i) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
responseData = await googleApiRequest.call(
this,
@@ -287,31 +544,30 @@ export class GSuiteAdmin implements INodeType {
{},
);
responseData = { success: true };
responseData = { deleted: true };
}
//https://developers.google.com/admin-sdk/directory/v1/reference/users/get
if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const projection = this.getNodeParameter('projection', i) as string;
const output = this.getNodeParameter('output', i);
const projection = this.getNodeParameter('projection', i);
const fields = this.getNodeParameter('fields', i, []) as string[];
const options = this.getNodeParameter('options', i);
qs.projection = projection;
Object.assign(qs, options);
if (qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(' ');
if (projection) {
qs.projection = projection;
}
if (qs.projection === 'custom' && qs.customFieldMask === undefined) {
throw new NodeOperationError(
this.getNode(),
'When projection is set to custom, the custom schemas field must be defined',
{ itemIndex: i },
);
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
if (output === 'select') {
if (!fields.includes('id')) {
fields.push('id');
}
qs.fields = fields.join(',');
}
responseData = await googleApiRequest.call(
@@ -321,36 +577,82 @@ export class GSuiteAdmin implements INodeType {
{},
qs,
);
if (output === 'simplified') {
responseData = {
kind: responseData.kind,
id: responseData.id,
primaryEmail: responseData.primaryEmail,
name: responseData.name,
isAdmin: responseData.isAdmin,
lastLoginTime: responseData.lastLoginTime,
creationTime: responseData.creationTime,
suspended: responseData.suspended,
};
}
}
//https://developers.google.com/admin-sdk/directory/v1/reference/users/list
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const output = this.getNodeParameter('output', i);
const fields = this.getNodeParameter('fields', i, []) as string[];
const projection = this.getNodeParameter('projection', i) as string;
const filter = this.getNodeParameter('filter', i, {}) as {
customer?: string;
domain?: string;
query?: string;
showDeleted?: boolean;
};
const sort = this.getNodeParameter('sort', i, {}) as {
sortRules?: {
orderBy?: string;
sortOrder?: string;
};
};
const options = this.getNodeParameter('options', i);
if (filter.customer) {
qs.customer = filter.customer;
}
if (filter.domain) {
qs.domain = filter.domain;
}
if (filter.query) {
qs.query = filter.query.trim();
}
if (filter.showDeleted) {
qs.showDeleted = filter.showDeleted ? 'true' : 'false';
}
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) {
qs.orderBy = orderBy;
}
if (sortOrder) {
qs.sortOrder = sortOrder;
}
}
qs.projection = projection;
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
Object.assign(qs, options);
if (output === 'select') {
if (!fields.includes('id')) {
fields.push('id');
}
qs.fields = `users(${fields.join(',')})`;
}
if (qs.customer === undefined) {
if (!qs.customer) {
qs.customer = 'my_customer';
}
if (qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(' ');
}
if (qs.projection === 'custom' && qs.customFieldMask === undefined) {
throw new NodeOperationError(
this.getNode(),
'When projection is set to custom, the custom schemas field must be defined',
{ itemIndex: i },
);
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
@@ -362,7 +664,6 @@ export class GSuiteAdmin implements INodeType {
);
} else {
qs.maxResults = this.getNodeParameter('limit', i);
responseData = await googleApiRequest.call(
this,
'GET',
@@ -373,55 +674,124 @@ export class GSuiteAdmin implements INodeType {
responseData = responseData.users;
}
if (output === 'simplified') {
responseData = responseData.map((user: any) => ({
kind: user.kind,
id: user.id,
primaryEmail: user.primaryEmail,
name: user.name,
isAdmin: user.isAdmin,
lastLoginTime: user.lastLoginTime,
creationTime: user.creationTime,
suspended: user.suspended,
}));
}
}
//https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/delete
if (operation === 'removeFromGroup') {
const groupId = this.getNodeParameter('groupId', i, undefined, {
extractValue: true,
}) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
await googleApiRequest.call(
this,
'DELETE',
`/directory/v1/groups/${groupId}/members/${userId}`,
);
responseData = { removed: true };
}
//https://developers.google.com/admin-sdk/directory/v1/reference/users/update
if (operation === 'update') {
const userId = this.getNodeParameter('userId', i) as string;
const userId = this.getNodeParameter('userId', i, undefined, {
extractValue: true,
}) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: {
name: { givenName?: string; familyName?: string };
name?: { givenName?: string; familyName?: string };
emails?: IDataObject[];
primaryEmail?: string;
phones?: IDataObject[];
} = { name: {} };
Object.assign(body, updateFields);
suspended?: boolean;
roles?: { [key: string]: boolean };
customSchemas?: IDataObject;
} = {};
if (updateFields.firstName) {
body.name ??= {};
body.name.givenName = updateFields.firstName as string;
//@ts-ignore
delete body.firstName;
}
if (updateFields.lastName) {
body.name ??= {};
body.name.familyName = updateFields.lastName as string;
//@ts-ignore
delete body.lastName;
}
if (Object.keys(body.name).length === 0) {
//@ts-ignore
delete body.name;
}
if (updateFields.phoneUi) {
const phones = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[];
body.phones = phones;
//@ts-ignore
delete body.phoneUi;
body.phones = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[];
}
if (updateFields.emailUi) {
const emails = (updateFields.emailUi as IDataObject).emailValues as IDataObject[];
body.emails = (updateFields.emailUi as IDataObject).emailValues as IDataObject[];
}
body.emails = emails;
if (updateFields.primaryEmail) {
body.primaryEmail = updateFields.primaryEmail as string;
}
//@ts-ignore
delete body.emailUi;
if (updateFields.suspendUi) {
body.suspended = updateFields.suspendUi as boolean;
}
if (updateFields.roles) {
const roles = updateFields.roles as string[];
body.roles = {
superAdmin: roles.includes('superAdmin'),
groupsAdmin: roles.includes('groupsAdmin'),
groupsReader: roles.includes('groupsReader'),
groupsEditor: roles.includes('groupsEditor'),
userManagement: roles.includes('userManagement'),
helpDeskAdmin: roles.includes('helpDeskAdmin'),
servicesAdmin: roles.includes('servicesAdmin'),
inventoryReportingAdmin: roles.includes('inventoryReportingAdmin'),
storageAdmin: roles.includes('storageAdmin'),
directorySyncAdmin: roles.includes('directorySyncAdmin'),
mobileAdmin: roles.includes('mobileAdmin'),
};
}
if (updateFields.customFields) {
const customFields = (updateFields.customFields as IDataObject)
.fieldValues as IDataObject[];
const customSchemas: IDataObject = {};
customFields.forEach((field) => {
const { schemaName, fieldName, value } = field as {
schemaName: string;
fieldName: string;
value: string;
};
if (!schemaName || !fieldName || !value) {
throw new NodeOperationError(this.getNode(), 'Invalid custom field data', {
itemIndex: i,
description: 'Schema name, field name, and value are required.',
});
}
customSchemas[schemaName] ??= {};
(customSchemas[schemaName] as IDataObject)[fieldName] = value;
});
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
}
responseData = await googleApiRequest.call(
@@ -441,17 +811,32 @@ export class GSuiteAdmin implements INodeType {
returnData.push(...executionData);
} catch (error) {
if (error instanceof NodeOperationError) {
throw error;
}
if (this.continueOnFail()) {
const executionErrorData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: error.message }),
this.helpers.returnJsonArray({
message: `Operation "${operation}" failed for resource "${resource}".`,
description: error.message,
}),
{ itemData: { item: i } },
);
returnData.push(...executionErrorData);
continue;
}
throw error;
throw new NodeOperationError(
this.getNode(),
`Operation "${operation}" failed for resource "${resource}".`,
{
description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.description}`,
itemIndex: i,
},
);
}
}
return [returnData];
}
}

View File

@@ -4,7 +4,7 @@ import type {
IDataObject,
JsonObject,
IHttpRequestMethods,
IRequestOptions,
IHttpRequestOptions,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
@@ -12,20 +12,19 @@ export async function googleApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
): Promise<any> {
const options: IRequestOptions = {
const options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://www.googleapis.com/admin${resource}`,
url: uri || `https://www.googleapis.com/admin${resource}`,
json: true,
};
try {
@@ -35,7 +34,11 @@ export async function googleApiRequest(
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
return await this.helpers.requestOAuth2.call(this, 'gSuiteAdminOAuth2Api', options);
return await this.helpers.httpRequestWithAuthentication.call(
this,
'gSuiteAdminOAuth2Api',
options,
);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
@@ -46,7 +49,6 @@ export async function googleApiRequestAllItems(
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {

View File

@@ -48,14 +48,65 @@ export const groupOperations: INodeProperties[] = [
];
export const groupFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* group */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group',
name: 'groupId',
default: {
mode: 'list',
value: '',
},
description: 'Select the group to perform the operation on',
displayOptions: {
show: {
operation: ['delete', 'get', 'update'],
resource: ['group'],
},
},
modes: [
{
displayName: 'From list',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'searchGroups',
},
},
{
displayName: 'By ID',
name: 'GroupId',
type: 'string',
placeholder: 'e.g. 0123kx3o1habcdf',
},
],
required: true,
type: 'resourceLocator',
},
/* -------------------------------------------------------------------------- */
/* group:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Email',
displayName: 'Group Name',
name: 'name',
displayOptions: {
show: {
operation: ['create'],
resource: ['group'],
},
},
default: '',
description: "The group's display name",
placeholder: 'e.g. Sales',
type: 'string',
},
{
displayName: 'Group Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
placeholder: 'e.g. sales@example.com',
required: true,
displayOptions: {
show: {
@@ -88,51 +139,13 @@ export const groupFields: INodeProperties[] = [
description:
'An extended description to help users determine the purpose of a group. For example, you can include information about who should join the group, the types of messages to send to the group, links to FAQs about the group, or related groups.',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: "The group's display name",
},
],
},
/* -------------------------------------------------------------------------- */
/* group:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['delete'],
resource: ['group'],
},
},
default: '',
description:
"Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.",
},
/* -------------------------------------------------------------------------- */
/* group:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: ['get'],
resource: ['group'],
},
},
default: '',
description:
"Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.",
},
/* -------------------------------------------------------------------------- */
/* group:getAll */
/* -------------------------------------------------------------------------- */
@@ -168,10 +181,10 @@ export const groupFields: INodeProperties[] = [
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
displayName: 'Filter',
name: 'filter',
type: 'collection',
placeholder: 'Add option',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
@@ -185,82 +198,88 @@ export const groupFields: INodeProperties[] = [
name: 'customer',
type: 'string',
default: '',
description:
"The unique ID for the customer's Google Workspace account. In case of a multi-domain account, to fetch all groups for a customer, fill this field instead of domain.",
description: "The unique ID for the customer's Google Workspace account",
},
{
displayName: 'Domain',
name: 'domain',
type: 'string',
default: '',
description: 'The domain name. Use this field to get fields from only one domain.',
},
{
displayName: 'Order By',
name: 'orderBy',
type: 'options',
options: [
{
name: 'Email',
value: 'email',
},
],
default: '',
description: 'Property to use for sorting results',
description: 'The domain name. Use this field to get groups from a specific domain.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
placeholder: 'e.g. name:contact* email:contact*',
default: '',
description:
'Query string search. Complete documentation is <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-groups">at</a>.',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: '',
description: 'Whether to return results in ascending or descending order',
'Query string to filter the results. Follow Google Admin SDK documentation. <a href="https://developers.google.com/admin-sdk/directory/v1/guides/search-groups#examples" target="_blank">More info</a>.',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
description:
"Email or immutable ID of the user if only those groups are to be listed, the given user is a member of. If it's an ID, it should match with the ID of the user object.",
description: 'Email or immutable ID of a user to list groups they are a member of',
},
],
},
/* -------------------------------------------------------------------------- */
/* group:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
required: true,
displayName: 'Sort',
name: 'sort',
type: 'fixedCollection',
placeholder: 'Add Sort Rule',
default: {},
displayOptions: {
show: {
operation: ['update'],
operation: ['getAll'],
resource: ['group'],
},
},
default: '',
description:
"Identifies the group in the API request. The value can be the group's email address, group alias, or the unique group ID.",
options: [
{
name: 'sortRules',
displayName: 'Sort Rules',
values: [
{
displayName: 'Order By',
name: 'orderBy',
type: 'options',
options: [
{
name: 'Email',
value: 'email',
},
],
default: 'email',
description: 'Field to sort the results by',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ASCENDING',
},
{
name: 'Descending',
value: 'DESCENDING',
},
],
default: 'ASCENDING',
description: 'Sort order direction',
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* group:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Update Fields',
name: 'updateFields',
@@ -279,6 +298,9 @@ export const groupFields: INodeProperties[] = [
name: 'description',
type: 'string',
default: '',
typeOptions: {
rows: 2,
},
description:
'An extended description to help users determine the purpose of a group. For example, you can include information about who should join the group, the types of messages to send to the group, links to FAQs about the group, or related groups.',
},
@@ -286,7 +308,7 @@ export const groupFields: INodeProperties[] = [
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
placeholder: 'e.g. sales@example.com',
default: '',
description:
"The group's email address. If your account has multiple domains, select the appropriate domain for the email address. The email must be unique.",
@@ -295,6 +317,7 @@ export const groupFields: INodeProperties[] = [
displayName: 'Name',
name: 'name',
type: 'string',
placeholder: 'e.g. Sales',
default: '',
description: "The group's display name",
},

View File

@@ -0,0 +1,91 @@
import type {
ILoadOptionsFunctions,
IDataObject,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
export async function searchUsers(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customer: 'my_customer',
};
const responseData = await googleApiRequestAllItems.call(
this,
'users',
'GET',
'/directory/v1/users',
{},
qs,
);
if (!Array.isArray(responseData)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.map(
(user: { name?: { fullName?: string }; id: string }) => ({
name: user.name?.fullName ?? user.id,
value: user.id,
}),
);
return { results };
}
export async function searchGroups(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customer: 'my_customer',
};
const responseData = await googleApiRequestAllItems.call(
this,
'groups',
'GET',
'/directory/v1/groups',
{},
qs,
);
if (!Array.isArray(responseData)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.map(
(group: { name?: string; email?: string; id: string }) => ({
name: group.name || group.email || 'Unnamed Group',
value: group.id,
}),
);
return { results };
}
export async function searchDevices(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
const qs: IDataObject = {
customerId: 'my_customer',
};
const responseData = await googleApiRequest.call(
this,
'GET',
'/directory/v1/customer/my_customer/devices/chromeos/',
{},
qs,
);
if (!Array.isArray(responseData?.chromeosdevices)) {
return { results: [] };
}
const results: INodeListSearchItems[] = responseData.chromeosdevices.map(
(device: { deviceId: string; serialNumber?: string }) => ({
name: device.serialNumber || device.deviceId || 'Unknown Device',
value: device.deviceId,
}),
);
return { results };
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 192 192" height="192" viewBox="0 0 192 192" width="192"><rect fill="none" height="192" width="192"/><g><g><path d="M24.53,130l20.69,35.84c3.27,5.67,9.32,9.16,15.86,9.16h44.06l-7.93-24.07L78.15,130l-26.81-4.66 L24.53,130z" fill="#1967D2"/><path d="M182.06,86.84l-20.09-34.8l-17.28,19.13L134.49,96l11.08,26.57l16.27,17.6l20.22-35.01 C185.33,99.49,185.33,92.51,182.06,86.84z" fill="#1967D2"/><path d="M105.15,17H61.09c-6.54,0-12.59,3.49-15.86,9.16L24.53,62l26.81,4.66L78.15,62l19.57-20.93L105.15,17z" fill="#1967D2"/><path d="M57.75,96l20.4-34H24.53L10.19,86.84c-3.27,5.67-3.27,12.65,0,18.32L24.53,130h53.62L57.75,96z" fill="#4285F4"/><path d="M114.09,62l20.4,34l27.47-43.96l-14.94-25.88c-3.27-5.67-9.32-9.16-15.86-9.16h-26.01l-27,45H114.09z" fill="#4285F4"/><path d="M114.09,130H78.15l27,45h26.01c6.54,0,12.59-3.49,15.86-9.16l14.82-25.67L134.49,96L114.09,130z" fill="#4285F4"/></g></g></svg>

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,538 @@
import type { ILoadOptionsFunctions, IExecuteFunctions } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import nock from 'nock';
import { returnData } from '../../../E2eTest/mock';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
import { GSuiteAdmin } from '../GSuiteAdmin.node';
jest.mock('../GenericFunctions', () => ({
getGoogleAuth: jest.fn().mockImplementation(() => ({
oauth2Client: {
setCredentials: jest.fn(),
getAccessToken: jest.fn().mockResolvedValue('mock-access-token'),
},
})),
googleApiRequest: jest.fn(),
googleApiRequestAllItems: jest.fn(),
}));
const node = new GSuiteAdmin();
const mockThis = {
getNode: () => ({
name: 'Google Workspace Admin',
parameters: {},
}),
helpers: {
httpRequestWithAuthentication: jest.fn(),
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getNodeParameter: jest.fn((name: string) => {
if (name === 'limit') return 50;
return undefined;
}),
} as unknown as ILoadOptionsFunctions;
describe('GSuiteAdmin Node - loadOptions', () => {
beforeEach(() => {
jest.clearAllMocks();
nock.cleanAll();
nock.disableNetConnect();
});
describe('getDomains', () => {
it('should return a list of domains', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValue([
{ domainName: 'example.com' },
{ domainName: 'test.com' },
]);
const result = await node.methods.loadOptions.getDomains.call(mockThis);
expect(result).toEqual([
{ name: 'example.com', value: 'example.com' },
{ name: 'test.com', value: 'test.com' },
]);
});
});
describe('getSchemas', () => {
it('should return a list of schemas', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValue([
{ displayName: 'Employee Info', schemaName: 'EmployeeSchema' },
{ displayName: '', schemaName: 'CustomSchema' },
]);
const result = await node.methods.loadOptions.getSchemas.call(mockThis);
expect(result).toEqual([
{ name: 'Employee Info', value: 'EmployeeSchema' },
{ name: 'CustomSchema', value: 'CustomSchema' },
]);
});
it('should correctly iterate over schemas and return expected values', async () => {
const schemas = [
{ displayName: 'Employee Info', schemaName: 'EmployeeSchema' },
{ displayName: 'Custom Schema', schemaName: 'CustomSchema' },
];
const result = schemas.map((schema) => ({
name: schema.displayName,
value: schema.schemaName,
}));
expect(result).toEqual([
{ name: 'Employee Info', value: 'EmployeeSchema' },
{ name: 'Custom Schema', value: 'CustomSchema' },
]);
});
});
describe('getOrgUnits', () => {
it('should return a list of organizational units', async () => {
(googleApiRequest as jest.Mock).mockResolvedValue({
organizationUnits: [
{ name: 'Engineering', orgUnitPath: '/engineering' },
{ name: 'HR', orgUnitPath: '/hr' },
],
});
const result = await node.methods.loadOptions.getOrgUnits.call(mockThis);
expect(result).toEqual([
{ name: 'Engineering', value: '/engineering' },
{ name: 'HR', value: '/hr' },
]);
});
});
});
describe('GSuiteAdmin Node - logic coverage', () => {
it('should apply all filters correctly into qs', () => {
const filter = {
customer: 'my_customer',
domain: 'example.com',
query: 'name:admin',
userId: 'user@example.com',
showDeleted: true,
};
const sort = {
sortRules: { orderBy: 'email', sortOrder: 'ASCENDING' },
};
const qs: Record<string, any> = {};
if (filter.customer) qs.customer = filter.customer;
if (filter.domain) qs.domain = filter.domain;
if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
mockThis.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
if (filter.userId) qs.userId = filter.userId;
if (filter.showDeleted) qs.showDeleted = 'true';
if (sort.sortRules) {
const { orderBy, sortOrder } = sort.sortRules;
if (orderBy) qs.orderBy = orderBy;
if (sortOrder) qs.sortOrder = sortOrder;
}
expect(qs).toEqual({
customer: 'my_customer',
domain: 'example.com',
query: 'name:admin',
userId: 'user@example.com',
showDeleted: 'true',
orderBy: 'email',
sortOrder: 'ASCENDING',
});
});
it('should throw an error for invalid query format', () => {
const filter = {
query: 'invalidQuery',
};
const qs: Record<string, any> = {};
expect(() => {
if (filter.query) {
const query = filter.query.trim();
const regex = /^(name|email):\S+$/;
if (!regex.test(query)) {
throw new NodeOperationError(
mockThis.getNode(),
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
}
qs.query = query;
}
}).toThrow(
'Invalid query format. Query must follow the format "displayName:<value>" or "email:<value>".',
);
});
it('should assign my_customer when customer is not defined', () => {
const qs: Record<string, any> = {};
if (!qs.customer) qs.customer = 'my_customer';
expect(qs.customer).toBe('my_customer');
});
it('should throw an error if username is empty', () => {
const mock = { getNode: () => ({}) } as IExecuteFunctions;
expect(() => {
const username = '';
if (!username) {
throw new NodeOperationError(mock.getNode(), "The parameter 'Username' is empty", {
itemIndex: 0,
description: "Please fill in the 'Username' parameter to create the user",
});
}
}).toThrow("The parameter 'Username' is empty");
});
it('should set phones, emails, roles, and custom fields', () => {
const additionalFields = {
phoneUi: { phoneValues: [{ type: 'work', value: '123' }] },
emailUi: { emailValues: [{ address: 'test@example.com', type: 'home' }] },
roles: ['superAdmin', 'groupsAdmin'],
customFields: {
fieldValues: [{ schemaName: 'CustomSchema', fieldName: 'customField', value: 'abc' }],
},
};
const body: Record<string, any> = {};
if (additionalFields.phoneUi) {
body.phones = additionalFields.phoneUi.phoneValues;
}
if (additionalFields.emailUi) {
body.emails = additionalFields.emailUi.emailValues;
}
if (additionalFields.roles) {
const roles = additionalFields.roles;
body.roles = {
superAdmin: roles.includes('superAdmin'),
groupsAdmin: roles.includes('groupsAdmin'),
groupsReader: false,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
};
}
if (additionalFields.customFields) {
const customSchemas: Record<string, any> = {};
for (const field of additionalFields.customFields.fieldValues) {
if (
!field.schemaName ||
!field.fieldName ||
field.value === undefined ||
field.value === ''
) {
continue;
}
if (!customSchemas[field.schemaName]) customSchemas[field.schemaName] = {};
customSchemas[field.schemaName][field.fieldName] = field.value;
}
if (Object.keys(customSchemas).length > 0) {
body.customSchemas = customSchemas;
}
}
expect(body).toEqual({
phones: [{ type: 'work', value: '123' }],
emails: [{ address: 'test@example.com', type: 'home' }],
roles: {
superAdmin: true,
groupsAdmin: true,
groupsReader: false,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
},
customSchemas: {
CustomSchema: { customField: 'abc' },
},
});
});
it('should set customFieldMask and fields if projection is custom and output is select', () => {
const projection = 'custom';
const output = 'select';
const fields = ['primaryEmail'];
const qs: Record<string, any> = {
customFieldMask: ['Custom1', 'Custom2'],
};
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
if (output === 'select') {
if (!fields.includes('id')) fields.push('id');
qs.fields = fields.join(',');
}
expect(qs).toEqual({
customFieldMask: 'Custom1,Custom2',
fields: 'primaryEmail,id',
});
});
it('should set fields for user getAll when returnAll is false', () => {
const qs: Record<string, any> = {};
const returnAll = false;
const fields = ['primaryEmail'];
const output = 'select';
const projection = 'custom';
qs.customFieldMask = ['Custom1', 'Custom2'];
if (projection === 'custom' && qs.customFieldMask) {
qs.customFieldMask = (qs.customFieldMask as string[]).join(',');
}
if (output === 'select') {
if (!fields.includes('id')) fields.push('id');
qs.fields = `users(${fields.join(',')})`;
}
if (!qs.customer) qs.customer = 'my_customer';
if (!returnAll) qs.maxResults = 50;
expect(qs).toEqual({
customFieldMask: 'Custom1,Custom2',
fields: 'users(primaryEmail,id)',
customer: 'my_customer',
maxResults: 50,
});
});
});
describe('GSuiteAdmin Node - user:update logic', () => {
it('should build suspended, roles, and customSchemas', async () => {
const mockCall = jest.fn().mockResolvedValue([{ success: true }]);
(googleApiRequest as jest.Mock).mockImplementation(mockCall);
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
getNodeParameter: jest.fn((paramName: string) => {
switch (paramName) {
case 'resource':
return 'user';
case 'operation':
return 'update';
case 'userId':
return 'user-id-123';
case 'updateFields':
return {
suspendUi: true,
roles: ['superAdmin', 'groupsReader'],
customFields: {
fieldValues: [
{ schemaName: 'CustomSchema1', fieldName: 'fieldA', value: 'valueA' },
{ schemaName: 'CustomSchema1', fieldName: 'fieldB', value: 'valueB' },
{ schemaName: 'CustomSchema2', fieldName: 'fieldX', value: 'valueX' },
],
},
};
default:
return undefined;
}
}),
helpers: {
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getInputData: () => [{ json: {} }],
} as unknown as IExecuteFunctions;
await new GSuiteAdmin().execute.call(mockContext);
const calledBody = mockCall.mock.calls[0][2];
expect(calledBody.suspended).toBe(true);
expect(calledBody.roles).toEqual({
superAdmin: true,
groupsAdmin: false,
groupsReader: true,
groupsEditor: false,
userManagement: false,
helpDeskAdmin: false,
servicesAdmin: false,
inventoryReportingAdmin: false,
storageAdmin: false,
directorySyncAdmin: false,
mobileAdmin: false,
});
expect(calledBody.customSchemas).toEqual({
CustomSchema1: {
fieldA: 'valueA',
fieldB: 'valueB',
},
CustomSchema2: {
fieldX: 'valueX',
},
});
});
it('should throw error for invalid custom fields', async () => {
const mockCall = jest.fn();
(googleApiRequest as jest.Mock).mockImplementation(mockCall);
const mockContextInvalidFields = {
getNode: () => ({ name: 'GSuiteAdmin' }),
getNodeParameter: jest.fn((paramName: string) => {
switch (paramName) {
case 'resource':
return 'user';
case 'operation':
return 'update';
case 'userId':
return 'user-id-456';
case 'updateFields':
return {
customFields: {
fieldValues: [
{ schemaName: '', fieldName: 'valid', value: 'ok' },
{ schemaName: 'ValidSchema', fieldName: 'valid', value: 'ok' },
],
},
};
default:
return undefined;
}
}),
helpers: {
returnJsonArray: (data: any) => data,
constructExecutionMetaData: (data: any) => data,
},
continueOnFail: () => false,
getInputData: () => [{ json: {} }],
} as unknown as IExecuteFunctions;
await expect(new GSuiteAdmin().execute.call(mockContextInvalidFields)).rejects.toThrow(
'Invalid custom field data',
);
expect(mockCall).not.toHaveBeenCalled();
});
it('should throw an error if username is empty', () => {
const mock = { getNode: () => ({}) } as IExecuteFunctions;
expect(() => {
const username = '';
if (!username) {
throw new NodeOperationError(mock.getNode(), "The parameter 'Username' is empty", {
itemIndex: 0,
description: "Please fill in the 'Username' parameter to create the user",
});
}
}).toThrow("The parameter 'Username' is empty");
});
});
describe('GSuiteAdmin Node - Error Handling', () => {
it('should throw a NodeOperationError if the error is an instance of NodeOperationError', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => false,
helpers: {
constructExecutionMetaData: jest.fn(),
returnJsonArray: jest.fn(),
},
} as unknown as IExecuteFunctions;
const error = new NodeOperationError(mockContext.getNode(), 'Some error message');
await expect(async () => {
throw error;
}).rejects.toThrow(NodeOperationError);
});
it('should handle error when continueOnFail is true and constructExecutionMetaData is called', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => true,
helpers: {
constructExecutionMetaData: jest.fn().mockReturnValue([{ message: 'mock error data' }]),
returnJsonArray: jest.fn().mockReturnValue([]),
},
} as unknown as IExecuteFunctions;
const error = new Error('Some error message');
await expect(async () => {
if (error instanceof NodeOperationError) {
throw error;
}
if (mockContext.continueOnFail()) {
const executionErrorData = mockContext.helpers.constructExecutionMetaData(
mockContext.helpers.returnJsonArray({
message: 'Operation "update" failed for resource "user".',
description: error.message,
}),
{ itemData: { item: 0 } },
);
if (executionErrorData) {
returnData.push(...executionErrorData);
} else {
console.error('executionErrorData is not iterable:', executionErrorData);
}
}
throw new NodeOperationError(
mockContext.getNode(),
'Operation "update" failed for resource "user".',
{
description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`,
itemIndex: 0,
},
);
}).rejects.toThrow(NodeOperationError);
});
it('should throw a NodeOperationError if an unknown error is thrown and continueOnFail is false', async () => {
const mockContext = {
getNode: () => ({ name: 'GSuiteAdmin' }),
continueOnFail: () => false,
helpers: {
constructExecutionMetaData: jest.fn(),
returnJsonArray: jest.fn(),
},
} as unknown as IExecuteFunctions;
const error = new Error('Some unknown error');
await expect(async () => {
if (error instanceof NodeOperationError) {
throw error;
}
if (!mockContext.continueOnFail()) {
throw new NodeOperationError(
mockContext.getNode(),
'Operation "update" failed for resource "user".',
{
description: `Please check the input parameters and ensure the API request is correctly formatted. Details: ${error.message}`,
itemIndex: 0,
},
);
}
}).rejects.toThrow(NodeOperationError);
});
});

View File

@@ -0,0 +1,173 @@
import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
describe('Google GSuiteAdmin Node', () => {
let mockContext: IExecuteFunctions | ILoadOptionsFunctions;
beforeEach(() => {
mockContext = {
helpers: {
httpRequestWithAuthentication: jest.fn(),
},
getNode: jest.fn(),
} as unknown as IExecuteFunctions | ILoadOptionsFunctions;
jest.clearAllMocks();
});
it('should make a successful API request with default options', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
success: true,
});
const result = await googleApiRequest.call(mockContext, 'GET', '/example/resource');
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
url: 'https://www.googleapis.com/admin/example/resource',
headers: { 'Content-Type': 'application/json' },
json: true,
qs: {},
}),
);
expect(result).toEqual({ success: true });
});
it('should omit the body if it is empty', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
success: true,
});
await googleApiRequest.call(mockContext, 'GET', '/example/resource', {});
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
'gSuiteAdminOAuth2Api',
expect.not.objectContaining({ body: expect.anything() }),
);
});
it('should throw a NodeApiError if the request fails', async () => {
const errorResponse = { message: 'API Error' };
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
errorResponse,
);
await expect(googleApiRequest.call(mockContext, 'GET', '/example/resource')).rejects.toThrow(
NodeApiError,
);
expect(mockContext.getNode).toHaveBeenCalled();
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalled();
});
it('should return all items across multiple pages', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock)
.mockResolvedValueOnce({
nextPageToken: 'pageToken1',
items: [{ id: '1' }, { id: '2' }],
})
.mockResolvedValueOnce({
nextPageToken: 'pageToken2',
items: [{ id: '3' }, { id: '4' }],
})
.mockResolvedValueOnce({
nextPageToken: '',
items: [{ id: '5' }],
});
const result = await googleApiRequestAllItems.call(
mockContext,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(3);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
1,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
2,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenNthCalledWith(
3,
'gSuiteAdminOAuth2Api',
expect.objectContaining({
method: 'GET',
qs: { maxResults: 100, pageToken: '' },
headers: { 'Content-Type': 'application/json' },
url: 'https://www.googleapis.com/admin/example/resource',
json: true,
}),
);
});
it('should handle single-page responses', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
nextPageToken: '',
items: [{ id: '1' }, { id: '2' }],
});
const result = await googleApiRequestAllItems.call(
mockContext,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
it('should handle empty responses', async () => {
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockResolvedValueOnce({
nextPageToken: '',
items: [],
});
const result = await googleApiRequestAllItems.call(
mockContext,
'items',
'GET',
'/example/resource',
);
expect(result).toEqual([]);
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
it('should throw a NodeApiError if a request fails', async () => {
const errorResponse = { message: 'API Error' };
(mockContext.helpers.httpRequestWithAuthentication as jest.Mock).mockRejectedValueOnce(
errorResponse,
);
await expect(
googleApiRequestAllItems.call(mockContext, 'items', 'GET', '/example/resource'),
).rejects.toThrow();
expect(mockContext.getNode).toHaveBeenCalled();
expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,104 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { googleApiRequest, googleApiRequestAllItems } from '../GenericFunctions';
import { searchUsers, searchGroups, searchDevices } from '../SearchFunctions';
jest.mock('../GenericFunctions');
describe('searchFunctions', () => {
let mockContext: ILoadOptionsFunctions;
beforeEach(() => {
mockContext = {
getNodeParameter: jest.fn(),
} as unknown as ILoadOptionsFunctions;
jest.clearAllMocks();
});
describe('searchUsers', () => {
it('should return formatted user search results', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([
{ id: '123', name: { fullName: 'John Doe' } },
{ id: '456' },
]);
const result = await searchUsers.call(mockContext);
expect(googleApiRequestAllItems).toHaveBeenCalledWith(
expect.anything(),
'GET',
'/directory/v1/users',
{},
{ customer: 'my_customer' },
);
expect(result).toEqual({
results: [
{ name: 'John Doe', value: '123' },
{ name: '456', value: '456' },
],
});
});
it('should return an empty array if no users found', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]);
const result = await searchUsers.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
describe('searchGroups', () => {
it('should return formatted group search results', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([
{ id: 'group1', name: 'Group One' },
{ id: 'group2', email: 'group@example.com' },
{ id: 'group3' },
]);
const result = await searchGroups.call(mockContext);
expect(result).toEqual({
results: [
{ name: 'Group One', value: 'group1' },
{ name: 'group@example.com', value: 'group2' },
{ name: 'Unnamed Group', value: 'group3' },
],
});
});
it('should return empty results if no groups found', async () => {
(googleApiRequestAllItems as jest.Mock).mockResolvedValueOnce([]);
const result = await searchGroups.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
describe('searchDevices', () => {
it('should return formatted device search results', async () => {
(googleApiRequest as jest.Mock).mockResolvedValueOnce({
chromeosdevices: [{ deviceId: 'dev1', serialNumber: 'SN123' }, { deviceId: 'Dev2' }],
});
const result = await searchDevices.call(mockContext);
expect(googleApiRequest).toHaveBeenCalledWith(
'GET',
'/directory/v1/customer/my_customer/devices/chromeos/',
{},
{ customerId: 'my_customer' },
);
expect(result).toEqual({
results: [
{ name: 'SN123', value: 'dev1' },
{ name: 'Dev2', value: 'Dev2' },
],
});
});
it('should return empty results if no devices found', async () => {
(googleApiRequest as jest.Mock).mockResolvedValueOnce({ chromeosdevices: [] });
const result = await searchDevices.call(mockContext);
expect(result).toEqual({ results: [] });
});
});
});

View File

@@ -0,0 +1,31 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('changeStatus.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post(
'/directory/v1/customer/my_customer/devices/chromeos/9140fcff-7ba7-4324-8552-f7de68481b4c/action',
{
action: 'reenable',
},
)
.reply(200, {
kind: 'admin#directory#chromeosdeviceAction',
action: 'reenable',
status: 'SUCCESS',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,59 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-180, 240],
"id": "db450654-d59c-4fb4-a06c-af7b971f0c14",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "changeStatus",
"deviceId": {
"__rl": true,
"value": "9140fcff-7ba7-4324-8552-f7de68481b4c",
"mode": "list",
"cachedResultName": "5CC115NN33"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [-320, 680],
"id": "d441de38-e340-495d-8177-f8bd8bb33e50",
"name": "Change Status",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Change Status",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Change Status": [
{
"json": {
"kind": "admin#directory#chromeosdeviceAction",
"action": "reenable",
"status": "SUCCESS"
}
}
]
}
}

View File

@@ -0,0 +1,43 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get(
'/directory/v1/customer/my_customer/devices/chromeos/9999ffff-7aa7-4444-8555-f7de48484b4a?projection=basic',
)
.reply(200, {
kind: 'admin#directory#chromeosdevice',
etag: '"example"',
deviceId: '9999ffff-7aa7-4444-8555-f7de48484b4a',
serialNumber: '5DD1155DD44',
status: 'DISABLED',
lastSync: '2025-02-12T07:17:16.950Z',
annotatedUser: 'my user',
annotatedLocation: 'test',
annotatedAssetId: '1234567788',
notes: 'test',
orgUnitPath: '/',
orgUnitId: '00pp8a2z1uu85pp',
extendedSupportEligible: false,
chromeOsType: 'chromeOs',
diskSpaceUsage: {
capacityBytes: '549755813888',
usedBytes: '549755813888',
},
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,73 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0ffead0b-d690-48b8-b406-bea5e0029c15",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"deviceId": {
"__rl": true,
"value": "9999ffff-7aa7-4444-8555-f7de48484b4a",
"mode": "list",
"cachedResultName": "5DD1155DD44"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [0, 1120],
"id": "cda57e63-1620-4f75-b11e-48b83565ad80",
"name": "Get Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Device": [
{
"json": {
"kind": "admin#directory#chromeosdevice",
"etag": "\"example\"",
"deviceId": "9999ffff-7aa7-4444-8555-f7de48484b4a",
"serialNumber": "5DD1155DD44",
"status": "DISABLED",
"lastSync": "2025-02-12T07:17:16.950Z",
"annotatedUser": "my user",
"annotatedLocation": "test",
"annotatedAssetId": "1234567788",
"notes": "test",
"orgUnitPath": "/",
"orgUnitId": "00pp8a2z1uu85pp",
"extendedSupportEligible": false,
"chromeOsType": "chromeOs",
"diskSpaceUsage": {
"capacityBytes": "549755813888",
"usedBytes": "549755813888"
}
}
}
]
}
}

View File

@@ -0,0 +1,35 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/customer/my_customer/devices/chromeos/')
.query({
customer: 'my_customer',
includeChildOrgunits: false,
maxResults: 100,
orderBy: 'notes',
orgUnitPath: '/admin-google Testing OU/Child OU',
projection: 'basic',
})
.reply(200, [
{
kind: 'admin#directory#chromeosdevices',
etag: '"example"',
},
]);
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,61 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "getAll",
"filter": {
"orgUnitPath": "/admin-google Testing OU/Child OU"
},
"sort": {
"sortRules": {
"orderBy": "notes",
"sortBy": "ascending"
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [40, 1120],
"id": "b8a51950-2fdb-4161-9dc3-09f73de5a45b",
"name": "Get Many Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many Device": [
{
"json": {
"kind": "admin#directory#chromeosdevices",
"etag": "\"example\""
}
}
]
}
}

View File

@@ -0,0 +1,627 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put(
'/directory/v1/customer/my_customer/devices/chromeos/9990fpff-8ba8-4444-8555-f7ee88881b4c',
)
.reply(200, {
kind: 'admin#directory#chromeosdevice',
etag: '"example"',
deviceId: '9990fpff-8ba8-4444-8555-f7ee88881b4c',
serialNumber: '5CC115NN33',
status: 'DISABLED',
lastSync: '2025-02-12T07:17:16.950Z',
annotatedUser: 'my user',
annotatedLocation: 'test',
annotatedAssetId: '1234567788',
notes: 'test',
model: 'Test Model',
osVersion: '129.0.6668.99',
platformVersion: '16002.51.0 (Official Build) stable-channel reven',
firmwareVersion: 'FirmwareNotParsed',
macAddress: '666c8888ffccf',
lastEnrollmentTime: '2025-02-10T17:03:10.324Z',
firstEnrollmentTime: '2025-02-10T17:03:10.324Z',
orgUnitPath: '/',
orgUnitId: '00pp88a2z2uu88pp',
recentUsers: [
{
type: 'USER_TYPE_MANAGED',
email: 'admin-google@example.com',
},
],
activeTimeRanges: [
{
date: '2025-02-10',
activeTime: 300000,
},
{
date: '2025-02-11',
activeTime: 1920025,
},
{
date: '2025-02-12',
activeTime: 30000,
},
],
tpmVersionInfo: {
family: '0',
specLevel: '0',
manufacturer: '0',
tpmModel: '0',
firmwareVersion: '0',
vendorSpecific: '',
},
cpuStatusReports: [
{
reportTime: '2025-02-10T17:03:13.233Z',
cpuUtilizationPercentageInfo: [12],
},
{
reportTime: '2025-02-10T17:04:13.233Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 42,
label: 'Tctl\n',
},
{
temperature: 43,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T17:11:01.943Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T17:12:02.223Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 51,
label: 'Tctl\n',
},
{
temperature: 43,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T17:47:32.621Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T17:48:42.770Z',
cpuTemperatureInfo: [
{
temperature: 43,
label: 'edge\n',
},
{
temperature: 44,
label: 'Tctl\n',
},
{
temperature: 44,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T18:56:24.294Z',
cpuUtilizationPercentageInfo: [29],
},
{
reportTime: '2025-02-10T18:57:27.841Z',
cpuTemperatureInfo: [
{
temperature: 34,
label: 'edge\n',
},
{
temperature: 35,
label: 'Tctl\n',
},
{
temperature: 35,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T23:04:56.582Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-10T23:05:56.563Z',
cpuTemperatureInfo: [
{
temperature: 27,
label: 'edge\n',
},
{
temperature: 28,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-10T23:56:47.138Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-10T23:57:50.717Z',
cpuTemperatureInfo: [
{
temperature: 39,
label: 'edge\n',
},
{
temperature: 39,
label: 'Tctl\n',
},
{
temperature: 40,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T07:49:44.333Z',
cpuUtilizationPercentageInfo: [26],
},
{
reportTime: '2025-02-11T07:50:48.473Z',
cpuTemperatureInfo: [
{
temperature: 26,
label: 'edge\n',
},
{
temperature: 27,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T15:46:23.530Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T15:47:22.723Z',
cpuTemperatureInfo: [
{
temperature: 27,
label: 'edge\n',
},
{
temperature: 28,
label: 'Tctl\n',
},
{
temperature: 27,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T15:52:40.368Z',
cpuUtilizationPercentageInfo: [28],
},
{
reportTime: '2025-02-11T15:53:41.233Z',
cpuTemperatureInfo: [
{
temperature: 34,
label: 'edge\n',
},
{
temperature: 35,
label: 'Tctl\n',
},
{
temperature: 35,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:06:07.349Z',
cpuUtilizationPercentageInfo: [30],
},
{
reportTime: '2025-02-11T16:07:07.921Z',
cpuTemperatureInfo: [
{
temperature: 39,
label: 'edge\n',
},
{
temperature: 39,
label: 'Tctl\n',
},
{
temperature: 40,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:13:28.511Z',
cpuUtilizationPercentageInfo: [25],
},
{
reportTime: '2025-02-11T16:14:27.628Z',
cpuTemperatureInfo: [
{
temperature: 36,
label: 'edge\n',
},
{
temperature: 37,
label: 'Tctl\n',
},
{
temperature: 37,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:17:06.188Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:18:06.375Z',
cpuTemperatureInfo: [
{
temperature: 40,
label: 'edge\n',
},
{
temperature: 41,
label: 'Tctl\n',
},
{
temperature: 42,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:36:20.232Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:37:20.599Z',
cpuTemperatureInfo: [
{
temperature: 45,
label: 'edge\n',
},
{
temperature: 58,
label: 'Tctl\n',
},
{
temperature: 45,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-11T16:48:45.267Z',
cpuUtilizationPercentageInfo: [27],
},
{
reportTime: '2025-02-11T16:49:44.854Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 44,
label: 'Tctl\n',
},
{
temperature: 44,
label: 'acpitz\n',
},
],
},
{
reportTime: '2025-02-12T06:35:29.337Z',
cpuUtilizationPercentageInfo: [30],
},
{
reportTime: '2025-02-12T06:36:28.433Z',
cpuTemperatureInfo: [
{
temperature: 42,
label: 'edge\n',
},
{
temperature: 42,
label: 'Tctl\n',
},
{
temperature: 42,
label: 'acpitz\n',
},
],
},
],
systemRamTotal: '16089374720',
systemRamFreeReports: [
{
reportTime: '2025-02-10T17:03:13.230Z',
systemRamFreeInfo: ['13905453056'],
},
{
reportTime: '2025-02-10T17:11:01.697Z',
systemRamFreeInfo: ['15221055488'],
},
{
reportTime: '2025-02-10T17:47:32.153Z',
systemRamFreeInfo: ['15237283840'],
},
{
reportTime: '2025-02-10T18:56:23.878Z',
systemRamFreeInfo: ['15228760064'],
},
{
reportTime: '2025-02-10T23:04:56.127Z',
systemRamFreeInfo: ['15228022784'],
},
{
reportTime: '2025-02-10T23:56:46.839Z',
systemRamFreeInfo: ['15226499072'],
},
{
reportTime: '2025-02-11T07:49:43.939Z',
systemRamFreeInfo: ['15229087744'],
},
{
reportTime: '2025-02-11T15:46:23.165Z',
systemRamFreeInfo: ['15226187776'],
},
{
reportTime: '2025-02-11T15:52:39.966Z',
systemRamFreeInfo: ['15226843136'],
},
{
reportTime: '2025-02-11T16:06:06.871Z',
systemRamFreeInfo: ['15225753600'],
},
{
reportTime: '2025-02-11T16:13:28.176Z',
systemRamFreeInfo: ['15228182528'],
},
{
reportTime: '2025-02-11T16:17:05.936Z',
systemRamFreeInfo: ['15223095296'],
},
{
reportTime: '2025-02-11T16:36:19.897Z',
systemRamFreeInfo: ['15226126336'],
},
{
reportTime: '2025-02-11T16:48:44.934Z',
systemRamFreeInfo: ['15226707968'],
},
{
reportTime: '2025-02-12T06:35:28.949Z',
systemRamFreeInfo: ['15222706176'],
},
],
diskVolumeReports: [
{
volumeInfo: [
{
volumeId: '/media/archive',
storageTotal: '8044687360',
storageFree: '8044687360',
},
{
volumeId: '/media/removable',
storageTotal: '8044687360',
storageFree: '8044687360',
},
],
},
],
lastKnownNetwork: [
{
ipAddress: '192.168.0.106',
wanIpAddress: '87.121.13.137',
},
],
cpuInfo: [
{
model: 'AMD Ryzen 5 4500U with Radeon Graphics',
architecture: 'x64',
maxClockSpeedKhz: 2375000,
logicalCpus: [
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397253,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '59.509354s',
},
{
displayName: 'C1',
sessionDuration: '1.338153s',
},
{
displayName: 'C2',
sessionDuration: '0.241264s',
},
{
displayName: 'POLL',
sessionDuration: '0.004477s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397372,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '58.861175s',
},
{
displayName: 'C1',
sessionDuration: '1.335068s',
},
{
displayName: 'C2',
sessionDuration: '0.761853s',
},
{
displayName: 'POLL',
sessionDuration: '0.007583s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397454,
idleDuration: '58s',
cStates: [
{
displayName: 'C3',
sessionDuration: '57.457528s',
},
{
displayName: 'C1',
sessionDuration: '1.280076s',
},
{
displayName: 'C2',
sessionDuration: '0.167642s',
},
{
displayName: 'POLL',
sessionDuration: '0.003444s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397348,
idleDuration: '59s',
cStates: [
{
displayName: 'C3',
sessionDuration: '58.906343s',
},
{
displayName: 'C1',
sessionDuration: '1.101873s',
},
{
displayName: 'C2',
sessionDuration: '0.119013s',
},
{
displayName: 'POLL',
sessionDuration: '0.009095s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1383188,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '59.476621s',
},
{
displayName: 'C1',
sessionDuration: '1.048691s',
},
{
displayName: 'C2',
sessionDuration: '0.192808s',
},
{
displayName: 'POLL',
sessionDuration: '0.003546s',
},
],
},
{
maxScalingFrequencyKhz: 2375000,
currentScalingFrequencyKhz: 1397437,
idleDuration: '60s',
cStates: [
{
displayName: 'C3',
sessionDuration: '60.155800s',
},
{
displayName: 'C1',
sessionDuration: '0.681644s',
},
{
displayName: 'C2',
sessionDuration: '0.143131s',
},
{
displayName: 'POLL',
sessionDuration: '0.004276s',
},
],
},
],
},
],
extendedSupportEligible: false,
chromeOsType: 'chromeOsFlex',
diskSpaceUsage: {
capacityBytes: '549755813888',
usedBytes: '85613068288',
},
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,661 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "device",
"operation": "update",
"deviceId": {
"__rl": true,
"value": "9990fpff-8ba8-4444-8555-f7ee88881b4c",
"mode": "list",
"cachedResultName": "5CC115NN33"
},
"updateOptions": {
"notes": "test"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [40, 1140],
"id": "52f7a4b5-7ab5-4bd1-b6eb-230341ab6057",
"name": "Update Device",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update Device",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update Device": [
{
"json": {
"kind": "admin#directory#chromeosdevice",
"etag": "\"example\"",
"deviceId": "9990fpff-8ba8-4444-8555-f7ee88881b4c",
"serialNumber": "5CC115NN33",
"status": "DISABLED",
"lastSync": "2025-02-12T07:17:16.950Z",
"annotatedUser": "my user",
"annotatedLocation": "test",
"annotatedAssetId": "1234567788",
"notes": "test",
"model": "Test Model",
"osVersion": "129.0.6668.99",
"platformVersion": "16002.51.0 (Official Build) stable-channel reven",
"firmwareVersion": "FirmwareNotParsed",
"macAddress": "666c8888ffccf",
"lastEnrollmentTime": "2025-02-10T17:03:10.324Z",
"firstEnrollmentTime": "2025-02-10T17:03:10.324Z",
"orgUnitPath": "/",
"orgUnitId": "00pp88a2z2uu88pp",
"recentUsers": [
{
"type": "USER_TYPE_MANAGED",
"email": "admin-google@example.com"
}
],
"activeTimeRanges": [
{
"date": "2025-02-10",
"activeTime": 300000
},
{
"date": "2025-02-11",
"activeTime": 1920025
},
{
"date": "2025-02-12",
"activeTime": 30000
}
],
"tpmVersionInfo": {
"family": "0",
"specLevel": "0",
"manufacturer": "0",
"tpmModel": "0",
"firmwareVersion": "0",
"vendorSpecific": ""
},
"cpuStatusReports": [
{
"reportTime": "2025-02-10T17:03:13.233Z",
"cpuUtilizationPercentageInfo": [12]
},
{
"reportTime": "2025-02-10T17:04:13.233Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 42,
"label": "Tctl\n"
},
{
"temperature": 43,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T17:11:01.943Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T17:12:02.223Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 51,
"label": "Tctl\n"
},
{
"temperature": 43,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T17:47:32.621Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T17:48:42.770Z",
"cpuTemperatureInfo": [
{
"temperature": 43,
"label": "edge\n"
},
{
"temperature": 44,
"label": "Tctl\n"
},
{
"temperature": 44,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T18:56:24.294Z",
"cpuUtilizationPercentageInfo": [29]
},
{
"reportTime": "2025-02-10T18:57:27.841Z",
"cpuTemperatureInfo": [
{
"temperature": 34,
"label": "edge\n"
},
{
"temperature": 35,
"label": "Tctl\n"
},
{
"temperature": 35,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T23:04:56.582Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-10T23:05:56.563Z",
"cpuTemperatureInfo": [
{
"temperature": 27,
"label": "edge\n"
},
{
"temperature": 28,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-10T23:56:47.138Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-10T23:57:50.717Z",
"cpuTemperatureInfo": [
{
"temperature": 39,
"label": "edge\n"
},
{
"temperature": 39,
"label": "Tctl\n"
},
{
"temperature": 40,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T07:49:44.333Z",
"cpuUtilizationPercentageInfo": [26]
},
{
"reportTime": "2025-02-11T07:50:48.473Z",
"cpuTemperatureInfo": [
{
"temperature": 26,
"label": "edge\n"
},
{
"temperature": 27,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T15:46:23.530Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T15:47:22.723Z",
"cpuTemperatureInfo": [
{
"temperature": 27,
"label": "edge\n"
},
{
"temperature": 28,
"label": "Tctl\n"
},
{
"temperature": 27,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T15:52:40.368Z",
"cpuUtilizationPercentageInfo": [28]
},
{
"reportTime": "2025-02-11T15:53:41.233Z",
"cpuTemperatureInfo": [
{
"temperature": 34,
"label": "edge\n"
},
{
"temperature": 35,
"label": "Tctl\n"
},
{
"temperature": 35,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:06:07.349Z",
"cpuUtilizationPercentageInfo": [30]
},
{
"reportTime": "2025-02-11T16:07:07.921Z",
"cpuTemperatureInfo": [
{
"temperature": 39,
"label": "edge\n"
},
{
"temperature": 39,
"label": "Tctl\n"
},
{
"temperature": 40,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:13:28.511Z",
"cpuUtilizationPercentageInfo": [25]
},
{
"reportTime": "2025-02-11T16:14:27.628Z",
"cpuTemperatureInfo": [
{
"temperature": 36,
"label": "edge\n"
},
{
"temperature": 37,
"label": "Tctl\n"
},
{
"temperature": 37,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:17:06.188Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:18:06.375Z",
"cpuTemperatureInfo": [
{
"temperature": 40,
"label": "edge\n"
},
{
"temperature": 41,
"label": "Tctl\n"
},
{
"temperature": 42,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:36:20.232Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:37:20.599Z",
"cpuTemperatureInfo": [
{
"temperature": 45,
"label": "edge\n"
},
{
"temperature": 58,
"label": "Tctl\n"
},
{
"temperature": 45,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-11T16:48:45.267Z",
"cpuUtilizationPercentageInfo": [27]
},
{
"reportTime": "2025-02-11T16:49:44.854Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 44,
"label": "Tctl\n"
},
{
"temperature": 44,
"label": "acpitz\n"
}
]
},
{
"reportTime": "2025-02-12T06:35:29.337Z",
"cpuUtilizationPercentageInfo": [30]
},
{
"reportTime": "2025-02-12T06:36:28.433Z",
"cpuTemperatureInfo": [
{
"temperature": 42,
"label": "edge\n"
},
{
"temperature": 42,
"label": "Tctl\n"
},
{
"temperature": 42,
"label": "acpitz\n"
}
]
}
],
"systemRamTotal": "16089374720",
"systemRamFreeReports": [
{
"reportTime": "2025-02-10T17:03:13.230Z",
"systemRamFreeInfo": ["13905453056"]
},
{
"reportTime": "2025-02-10T17:11:01.697Z",
"systemRamFreeInfo": ["15221055488"]
},
{
"reportTime": "2025-02-10T17:47:32.153Z",
"systemRamFreeInfo": ["15237283840"]
},
{
"reportTime": "2025-02-10T18:56:23.878Z",
"systemRamFreeInfo": ["15228760064"]
},
{
"reportTime": "2025-02-10T23:04:56.127Z",
"systemRamFreeInfo": ["15228022784"]
},
{
"reportTime": "2025-02-10T23:56:46.839Z",
"systemRamFreeInfo": ["15226499072"]
},
{
"reportTime": "2025-02-11T07:49:43.939Z",
"systemRamFreeInfo": ["15229087744"]
},
{
"reportTime": "2025-02-11T15:46:23.165Z",
"systemRamFreeInfo": ["15226187776"]
},
{
"reportTime": "2025-02-11T15:52:39.966Z",
"systemRamFreeInfo": ["15226843136"]
},
{
"reportTime": "2025-02-11T16:06:06.871Z",
"systemRamFreeInfo": ["15225753600"]
},
{
"reportTime": "2025-02-11T16:13:28.176Z",
"systemRamFreeInfo": ["15228182528"]
},
{
"reportTime": "2025-02-11T16:17:05.936Z",
"systemRamFreeInfo": ["15223095296"]
},
{
"reportTime": "2025-02-11T16:36:19.897Z",
"systemRamFreeInfo": ["15226126336"]
},
{
"reportTime": "2025-02-11T16:48:44.934Z",
"systemRamFreeInfo": ["15226707968"]
},
{
"reportTime": "2025-02-12T06:35:28.949Z",
"systemRamFreeInfo": ["15222706176"]
}
],
"diskVolumeReports": [
{
"volumeInfo": [
{
"volumeId": "/media/archive",
"storageTotal": "8044687360",
"storageFree": "8044687360"
},
{
"volumeId": "/media/removable",
"storageTotal": "8044687360",
"storageFree": "8044687360"
}
]
}
],
"lastKnownNetwork": [
{
"ipAddress": "192.168.0.106",
"wanIpAddress": "87.121.13.137"
}
],
"cpuInfo": [
{
"model": "AMD Ryzen 5 4500U with Radeon Graphics",
"architecture": "x64",
"maxClockSpeedKhz": 2375000,
"logicalCpus": [
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397253,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "59.509354s"
},
{
"displayName": "C1",
"sessionDuration": "1.338153s"
},
{
"displayName": "C2",
"sessionDuration": "0.241264s"
},
{
"displayName": "POLL",
"sessionDuration": "0.004477s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397372,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "58.861175s"
},
{
"displayName": "C1",
"sessionDuration": "1.335068s"
},
{
"displayName": "C2",
"sessionDuration": "0.761853s"
},
{
"displayName": "POLL",
"sessionDuration": "0.007583s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397454,
"idleDuration": "58s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "57.457528s"
},
{
"displayName": "C1",
"sessionDuration": "1.280076s"
},
{
"displayName": "C2",
"sessionDuration": "0.167642s"
},
{
"displayName": "POLL",
"sessionDuration": "0.003444s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397348,
"idleDuration": "59s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "58.906343s"
},
{
"displayName": "C1",
"sessionDuration": "1.101873s"
},
{
"displayName": "C2",
"sessionDuration": "0.119013s"
},
{
"displayName": "POLL",
"sessionDuration": "0.009095s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1383188,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "59.476621s"
},
{
"displayName": "C1",
"sessionDuration": "1.048691s"
},
{
"displayName": "C2",
"sessionDuration": "0.192808s"
},
{
"displayName": "POLL",
"sessionDuration": "0.003546s"
}
]
},
{
"maxScalingFrequencyKhz": 2375000,
"currentScalingFrequencyKhz": 1397437,
"idleDuration": "60s",
"cStates": [
{
"displayName": "C3",
"sessionDuration": "60.155800s"
},
{
"displayName": "C1",
"sessionDuration": "0.681644s"
},
{
"displayName": "C2",
"sessionDuration": "0.143131s"
},
{
"displayName": "POLL",
"sessionDuration": "0.004276s"
}
]
}
]
}
],
"extendedSupportEligible": false,
"chromeOsType": "chromeOsFlex",
"diskSpaceUsage": {
"capacityBytes": "549755813888",
"usedBytes": "85613068288"
}
}
}
]
}
}

View File

@@ -0,0 +1,34 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Create Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('create.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post('/directory/v1/groups', {
email: 'NewOnes22@example.com',
name: 'Test',
description: 'test',
})
.reply(200, {
kind: 'admin#directory#group',
id: '03mzq4wv15cepg2',
etag: '"example"',
email: 'NewOnes22@example.com',
name: 'Test',
description: 'test',
adminCreated: true,
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,61 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"name": "Test",
"email": "NewOnes22@example.com",
"additionalFields": {
"description": "test"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [60, 1140],
"id": "54a9e564-bff5-4d86-b684-e5cf5b34b48c",
"name": "Create Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Create Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Create Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "03mzq4wv15cepg2",
"etag": "\"example\"",
"email": "NewOnes22@example.com",
"name": "Test",
"description": "test",
"adminCreated": true
}
}
]
}
}

View File

@@ -0,0 +1,22 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Delete Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('delete.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/groups/01302m922pmp3e4')
.reply(204, '');
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,55 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "delete",
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [60, 1140],
"id": "9d6f8739-8a1b-4b85-9e5c-84a184e6dbaf",
"name": "Delete Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Delete Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Delete Group": [
{
"json": { "success": true }
}
]
}
}

View File

@@ -0,0 +1,38 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/groups/01302m922pmp3e4')
.reply(200, {
kind: 'admin#directory#group',
id: '01302m922pmp3e4',
etag: '"example"',
email: 'new3@example.com',
name: 'new2',
directMembersCount: '2',
description: 'new1',
adminCreated: true,
aliases: ['new2@example.com', 'new@example.com', 'NewOnes@example.com'],
nonEditableAliases: [
'NewOnes@example.com.test-google-a.com',
'new@example.com.test-google-a.com',
'new2@example.com.test-google-a.com',
'new3@example.com.test-google-a.com',
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,71 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "get",
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1120],
"id": "8d47d64b-80df-479a-8e1d-d63991f5d23b",
"name": "Get Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "01302m922pmp3e4",
"etag": "\"example\"",
"email": "new3@example.com",
"name": "new2",
"directMembersCount": "2",
"description": "new1",
"adminCreated": true,
"aliases": ["new2@example.com", "new@example.com", "NewOnes@example.com"],
"nonEditableAliases": [
"NewOnes@example.com.test-google-a.com",
"new@example.com.test-google-a.com",
"new2@example.com.test-google-a.com",
"new3@example.com.test-google-a.com"
]
}
}
]
}
}

View File

@@ -0,0 +1,53 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get All Groups', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/groups')
.query({
customer: 'my_customer',
maxResults: '100',
})
.reply(200, {
kind: 'admin#directory#groups',
etag: '"test_etag"',
groups: [
{
kind: 'admin#directory#group',
id: '01x0gk373c9z46j',
etag: '"example"',
email: 'newoness@example.com',
name: 'NewOness',
directMembersCount: '1',
description: 'test',
adminCreated: true,
nonEditableAliases: ['NewOness@example.com.test-google-a.com'],
},
{
kind: 'admin#directory#group',
id: '01tuee742txc3k4',
etag: '"example"',
email: 'newonesss@example.com',
name: 'NewOne3',
directMembersCount: '0',
description: 'test',
adminCreated: true,
nonEditableAliases: ['NewOnesss@example.com.test-google-a.com'],
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,74 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "getAll",
"returnAll": true,
"filter": {}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [100, 1120],
"id": "30263040-3578-4ce6-b19c-f665b85ca301",
"name": "Get Many",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many": [
{
"json": {
"kind": "admin#directory#group",
"id": "01x0gk373c9z46j",
"etag": "\"example\"",
"email": "newoness@example.com",
"name": "NewOness",
"directMembersCount": "1",
"description": "test",
"adminCreated": true,
"nonEditableAliases": ["NewOness@example.com.test-google-a.com"]
}
},
{
"json": {
"kind": "admin#directory#group",
"id": "01tuee742txc3k4",
"etag": "\"example\"",
"email": "newonesss@example.com",
"name": "NewOne3",
"directMembersCount": "0",
"description": "test",
"adminCreated": true,
"nonEditableAliases": ["NewOnesss@example.com.test-google-a.com"]
}
}
]
}
}

View File

@@ -0,0 +1,35 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Update Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put('/directory/v1/groups/01302m922p525286')
.reply(200, {
kind: 'admin#directory#group',
id: '01302m922p525286',
etag: '"example"',
email: 'new3@example.com',
name: 'new2',
description: 'new1',
adminCreated: true,
aliases: ['new@example.com', 'NewOnes@example.com', 'new2@example.com'],
nonEditableAliases: [
'NewOnes@example.com.test-google-a.com',
'new@example.com.test-google-a.com',
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,73 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "group",
"operation": "update",
"groupId": {
"__rl": true,
"value": "01302m922p525286",
"mode": "list",
"cachedResultName": "new"
},
"updateFields": {
"description": "new1",
"email": "new3@example.com",
"name": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1100],
"id": "013eec82-8d52-4485-88eb-d9caf112d539",
"name": "Update Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update Group": [
{
"json": {
"kind": "admin#directory#group",
"id": "01302m922p525286",
"etag": "\"example\"",
"email": "new3@example.com",
"name": "new2",
"description": "new1",
"adminCreated": true,
"aliases": ["new@example.com", "NewOnes@example.com", "new2@example.com"],
"nonEditableAliases": [
"NewOnes@example.com.test-google-a.com",
"new@example.com.test-google-a.com"
]
}
}
]
}
}

View File

@@ -0,0 +1,32 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Add User to Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('addToGroup.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users/114393134535981252528')
.reply(200, { primaryEmail: 'newone@example.com' });
nock('https://www.googleapis.com/admin')
.post('/directory/v1/groups/01302m922pmp3e4/members', {
email: 'newone@example.com',
role: 'MEMBER',
})
.reply(200, {
kind: 'admin#directory#member',
status: 'ACTIVE',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,60 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "addToGroup",
"userId": {
"__rl": true,
"value": "114393134535981252528",
"mode": "list",
"cachedResultName": "NewOne User"
},
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [80, 1100],
"id": "b0c8042f-4ce1-41f1-9d14-876d5cac3ccf",
"name": "Add To Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Add To Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Add To Group": [
{
"json": { "added": true }
}
]
}
}

View File

@@ -0,0 +1,51 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Create User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('create.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.post('/directory/v1/users')
.reply(200, {
kind: 'admin#directory#user',
id: '112507770188715525288',
etag: '"example"',
primaryEmail: 'new@example.com',
name: {
givenName: 'NewOne',
familyName: 'User',
},
emails: [
{
address: 'test@mail.com',
type: 'work',
},
],
phones: [
{
primary: false,
type: 'work',
value: '+1-202-555-0123',
},
],
isAdmin: false,
isDelegatedAdmin: false,
creationTime: '2024-12-20T20:48:53.000Z',
customerId: 'C4444hnz2',
orgUnitPath: '/',
isMailboxSetup: false,
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,107 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"firstName": "NewOne",
"lastName": "User",
"password": "12345678",
"username": "new",
"domain": "example.com",
"additionalFields": {
"changePasswordAtNextLogin": true,
"phoneUi": {
"phoneValues": [
{
"value": "+1-202-555-0123"
}
]
},
"emailUi": {
"emailValues": [
{
"address": "test@mail.com"
}
]
},
"roles": ["groupsAdmin"],
"customFields": {
"fieldValues": [
{
"schemaName": "NewTest",
"fieldName": "test",
"value": "test"
}
]
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [100, 1100],
"id": "54227da8-70ad-456a-8d75-f7e28d514e90",
"name": "Create User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Create User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Create User": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715525288",
"etag": "\"example\"",
"primaryEmail": "new@example.com",
"name": {
"givenName": "NewOne",
"familyName": "User"
},
"isAdmin": false,
"isDelegatedAdmin": false,
"creationTime": "2024-12-20T20:48:53.000Z",
"emails": [
{
"address": "test@mail.com",
"type": "work"
}
],
"phones": [
{
"value": "+1-202-555-0123",
"primary": false,
"type": "work"
}
],
"customerId": "C4444hnz2",
"orgUnitPath": "/",
"isMailboxSetup": false
}
}
]
}
}

View File

@@ -0,0 +1,23 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Delete User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('delete.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/users/114393134535981252212')
.reply(200, {});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,56 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "delete",
"userId": {
"__rl": true,
"value": "114393134535981252212",
"mode": "list",
"cachedResultName": "NewOne22 User22"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [120, 1100],
"id": "39f9a9c0-2ea5-45b2-a346-55017dfa4e43",
"name": "Delete User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Delete User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Delete User": [
{
"json": {
"deleted": true
}
}
]
}
}

View File

@@ -0,0 +1,43 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('get.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users/112507770188715252026')
.query({ projection: 'basic' })
.reply(200, {
kind: 'admin#directory#user',
id: '112507770188715252026',
primaryEmail: 'new@example.com',
name: {
givenName: 'New One',
familyName: 'User',
fullName: 'New One User',
},
isAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2024-12-20T20:48:53.000Z',
suspended: true,
emails: [
{
address: 'new@example.com',
primary: true,
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,67 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "get",
"userId": {
"__rl": true,
"value": "112507770188715252026",
"mode": "list",
"cachedResultName": "NewOne22 User22"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [120, 1100],
"id": "b39d3a72-6e81-4219-82eb-f39d99eace16",
"name": "Get User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get User": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715252026",
"primaryEmail": "new@example.com",
"name": {
"givenName": "New One",
"familyName": "User",
"fullName": "New One User"
},
"isAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2024-12-20T20:48:53.000Z",
"suspended": true
}
}
]
}
}

View File

@@ -0,0 +1,72 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Get All Users', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('getAll.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.get('/directory/v1/users')
.query({
projection: 'basic',
customer: 'my_customer',
maxResults: '100',
})
.reply(200, {
kind: 'admin#directory#users',
users: [
{
kind: 'admin#directory#user',
id: '112507770188715252055',
primaryEmail: 'new@example.com',
name: {
givenName: 'New',
familyName: 'User',
fullName: 'New User',
},
isAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2024-12-20T20:48:53.000Z',
suspended: true,
emails: [
{
address: 'new@example.com',
primary: true,
},
],
},
{
kind: 'admin#directory#user',
id: '222459372679230452528',
primaryEmail: 'test33@example.com',
name: {
givenName: 'New',
familyName: 'Test',
fullName: 'New Test',
},
isAdmin: true,
lastLoginTime: '2024-12-19T08:39:56.000Z',
creationTime: '2024-09-06T11:48:38.000Z',
suspended: false,
emails: [
{
address: 'test33@example.com',
primary: true,
},
],
},
],
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,78 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "getAll",
"filter": {}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "4107ee9d-37e1-4d85-9099-25ec13211ee1",
"name": "Get Many",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Get Many",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Get Many": [
{
"json": {
"kind": "admin#directory#user",
"id": "112507770188715252055",
"primaryEmail": "new@example.com",
"name": {
"givenName": "New",
"familyName": "User",
"fullName": "New User"
},
"isAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2024-12-20T20:48:53.000Z",
"suspended": true
}
},
{
"json": {
"kind": "admin#directory#user",
"id": "222459372679230452528",
"primaryEmail": "test33@example.com",
"name": {
"givenName": "New",
"familyName": "Test",
"fullName": "New Test"
},
"isAdmin": true,
"lastLoginTime": "2024-12-19T08:39:56.000Z",
"creationTime": "2024-09-06T11:48:38.000Z",
"suspended": false
}
}
]
}
}

View File

@@ -0,0 +1,23 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Remove User from Group', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('removeFromGroup.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.delete('/directory/v1/groups/01302m922pmp3e4/members/114393134535981252528')
.reply(200, {});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,60 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "removeFromGroup",
"userId": {
"__rl": true,
"value": "114393134535981252528",
"mode": "list",
"cachedResultName": "New User"
},
"groupId": {
"__rl": true,
"value": "01302m922pmp3e4",
"mode": "list",
"cachedResultName": "new2"
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "9a323b95-1495-487f-ba27-2df737bd5bdf",
"name": "Remove From Group",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Remove From Group",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Remove From Group": [
{
"json": { "removed": true }
}
]
}
}

View File

@@ -0,0 +1,82 @@
import nock from 'nock';
import { initBinaryDataService, testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
describe('Google GSuiteAdmin Node - Update User', () => {
const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
filename.includes('update.workflow.json'),
);
beforeAll(async () => {
await initBinaryDataService();
});
beforeEach(() => {
nock.disableNetConnect();
nock('https://www.googleapis.com/admin')
.put('/directory/v1/users/101071249467630629404', {
name: {
givenName: 'test',
familyName: 'new',
},
primaryEmail: 'one@example.com',
phones: [
{
type: 'assistant',
value: '123',
primary: true,
},
],
emails: [
{
address: 'newone@example.com',
type: 'home',
},
],
})
.reply(200, {
kind: 'admin#directory#user',
id: '101071249467630629404',
etag: '"example"',
primaryEmail: 'one@example.com',
name: {
givenName: 'test',
familyName: 'new',
},
isAdmin: false,
isDelegatedAdmin: false,
lastLoginTime: '1970-01-01T00:00:00.000Z',
creationTime: '2025-03-26T21:28:53.000Z',
agreedToTerms: false,
suspended: false,
archived: false,
changePasswordAtNextLogin: false,
ipWhitelisted: false,
emails: [
{
address: 'newone@example.com',
type: 'home',
},
],
phones: [
{
value: '123',
primary: true,
type: 'assistant',
},
],
aliases: ['new22@example.com'],
nonEditableAliases: ['new22@example.com.test-google-a.com'],
customerId: 'C0442hnz1',
orgUnitPath: '/',
isMailboxSetup: false,
includeInGlobalAddressList: true,
thumbnailPhotoUrl: '//example',
thumbnailPhotoEtag: '"example"',
recoveryEmail: '',
});
});
testWorkflows(workflows);
});

View File

@@ -0,0 +1,117 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 700],
"id": "0e76b314-4994-4141-975f-9614c6094c80",
"name": "When clicking Test workflow"
},
{
"parameters": {
"operation": "update",
"userId": {
"__rl": true,
"value": "101071249467630629404",
"mode": "list",
"cachedResultName": "NewOne22 User22"
},
"updateFields": {
"archived": true,
"firstName": "test",
"lastName": "new",
"phoneUi": {
"phoneValues": [
{
"type": "assistant",
"value": "123",
"primary": true
}
]
},
"primaryEmail": "one@example.com",
"emailUi": {
"emailValues": [
{
"type": "home",
"address": "newone@example.com"
}
]
}
}
},
"type": "n8n-nodes-base.gSuiteAdmin",
"typeVersion": 1,
"position": [140, 1100],
"id": "b4cd1391-cfdc-4f36-8c2f-f18a9ad4f795",
"name": "Update User",
"credentials": {
"gSuiteAdminOAuth2Api": {
"id": "OXfPMaggXFJ0RLkw",
"name": "Google Workspace Admin account"
}
}
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Update User",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"Update User": [
{
"json": {
"kind": "admin#directory#user",
"id": "101071249467630629404",
"etag": "\"example\"",
"primaryEmail": "one@example.com",
"name": {
"givenName": "test",
"familyName": "new"
},
"isAdmin": false,
"isDelegatedAdmin": false,
"lastLoginTime": "1970-01-01T00:00:00.000Z",
"creationTime": "2025-03-26T21:28:53.000Z",
"agreedToTerms": false,
"suspended": false,
"archived": false,
"changePasswordAtNextLogin": false,
"ipWhitelisted": false,
"emails": [
{
"address": "newone@example.com",
"type": "home"
}
],
"phones": [
{
"value": "123",
"primary": true,
"type": "assistant"
}
],
"aliases": ["new22@example.com"],
"nonEditableAliases": ["new22@example.com.test-google-a.com"],
"customerId": "C0442hnz1",
"orgUnitPath": "/",
"isMailboxSetup": false,
"includeInGlobalAddressList": true,
"thumbnailPhotoUrl": "//example",
"thumbnailPhotoEtag": "\"example\"",
"recoveryEmail": ""
}
}
]
}
}