n8n/packages/nodes-base/nodes/Google/Gmail/test/v2/GmailV2.node.test.ts

397 lines
16 KiB
TypeScript

/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */
import { mock, mockDeep } from 'jest-mock-extended';
import { jsonParse, type ILoadOptionsFunctions, type INode } from 'n8n-workflow';
import nock from 'nock';
import { testWorkflows } from '@test/nodes/Helpers';
import labels from './fixtures/labels.json';
import messages from './fixtures/messages.json';
import { getGmailAliases, getLabels, getThreadMessages } from '../../v2/loadOptions';
describe('Test Gmail Node v2', () => {
beforeAll(() => {
jest
.useFakeTimers({ doNotFake: ['setImmediate', 'nextTick'] })
.setSystemTime(new Date('2024-12-16 12:34:56.789Z'));
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.resetAllMocks();
});
describe('Messages', () => {
const gmailNock = nock('https://www.googleapis.com/gmail');
beforeAll(() => {
gmailNock.get('/v1/users/me/messages').query({ maxResults: 2 }).reply(200, {
messages,
});
gmailNock
.get('/v1/users/me/messages')
.query({
includeSpamTrash: 'true',
labelIds: 'CHAT',
q: 'test from:Test Sender after:1734393600 before:1735171200',
readStatus: 'both',
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: 'true',
maxResults: '2',
})
.reply(200, { messages });
gmailNock
.get('/v1/users/me/messages/a1b2c3d4e5f6g7h8')
.query({
maxResults: '2',
format: 'metadata',
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
})
.reply(200, messages[0]);
gmailNock
.get('/v1/users/me/messages/a1b2c3d4e5f6g7h8')
.query({
includeSpamTrash: 'true',
labelIds: 'CHAT',
q: 'test from:Test Sender after:1734393600 before:1735171200',
readStatus: 'both',
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: 'true',
maxResults: '2',
format: 'raw',
})
.reply(200, {
...messages[0],
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
});
gmailNock
.get('/v1/users/me/messages/z9y8x7w6v5u4t3s2')
.query({
includeSpamTrash: 'true',
labelIds: 'CHAT',
q: 'test from:Test Sender after:1734393600 before:1735171200',
readStatus: 'both',
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: 'true',
maxResults: '2',
format: 'raw',
})
.reply(200, {
...messages[1],
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
});
gmailNock
.get('/v1/users/me/messages/z9y8x7w6v5u4t3s2')
.query({
maxResults: '2',
format: 'metadata',
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
})
.reply(200, messages[0]);
gmailNock.get('/v1/users/me/labels').reply(200, {
labels,
});
gmailNock.get('/v1/users/me/profile').times(2).reply(200, { emailAddress: 'test@n8n.io' });
gmailNock
.post('/v1/users/me/messages/send')
.query({ format: 'metadata' })
.reply(200, messages[0]);
gmailNock.post('/v1/users/me/messages/send').reply(200, messages[0]);
gmailNock
.post('/v1/users/me/messages/send')
.query({ userId: 'me', uploadType: 'media' })
.reply(200, messages[0]);
gmailNock
.post('/v1/users/me/messages/test/modify', (body) => 'addLabelIds' in body)
.reply(200, messages[0]);
gmailNock
.post('/v1/users/me/messages/test/modify', (body) => 'removeLabelIds' in body)
.reply(200, messages[0]);
gmailNock.delete('/v1/users/me/messages/test').reply(200, messages[0]);
gmailNock
.get('/v1/users/me/messages/test')
.query({
format: 'metadata',
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
})
.reply(200, messages[0]);
gmailNock.get('/v1/users/me/labels').reply(200, { labels });
gmailNock
.get('/v1/users/me/messages/test')
.query({ format: 'raw' })
.reply(200, { raw: 'test email content' });
gmailNock
.post('/v1/users/me/messages/test/modify', { removeLabelIds: ['UNREAD'] })
.reply(200, messages[0]);
gmailNock
.post('/v1/users/me/messages/test/modify', { addLabelIds: ['UNREAD'] })
.reply(200, messages[0]);
gmailNock
.get('/v1/users/me/messages/test')
.query({
format: 'metadata',
})
.reply(200, messages[0]);
});
testWorkflows(['nodes/Google/Gmail/test/v2/messages.workflow.json']);
it('should make the correct network calls', () => {
gmailNock.done();
});
});
describe('Labels', () => {
const gmailNock = nock('https://www.googleapis.com/gmail');
beforeAll(() => {
gmailNock
.post('/v1/users/me/labels', {
labelListVisibility: 'labelShow',
messageListVisibility: 'show',
name: 'Test Label Name',
})
.reply(200, labels[0]);
gmailNock.delete('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
gmailNock.get('/v1/users/me/labels/test-label-id').reply(200, labels[0]);
gmailNock.get('/v1/users/me/labels').reply(200, {
labels,
});
});
testWorkflows(['nodes/Google/Gmail/test/v2/labels.workflow.json']);
it('should make the correct network calls', () => {
gmailNock.done();
});
});
describe('Drafts', () => {
const gmailNock = nock('https://www.googleapis.com/gmail');
beforeAll(() => {
gmailNock
.filteringRequestBody((body) => {
try {
const parsedBody = jsonParse<{ message: { raw: string; threadId: string } }>(body);
const mail = Buffer.from(parsedBody.message.raw, 'base64').toString('utf-8');
// Remove dynamic fields from mail
parsedBody.message.raw = Buffer.from(
mail
.replace(/boundary=".*"/g, 'boundary="--test-boundary"')
.replace(/----.*/g, '----test-boundary')
.replace(/Message-ID:.*/g, 'Message-ID: test-message-id'),
'utf-8',
).toString('base64');
return JSON.stringify(parsedBody);
} catch (error) {
return body;
}
})
.post('/v1/users/me/drafts', {
message: {
raw: 'Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7IGJvdW5kYXJ5PSItLXRlc3QtYm91bmRhcnkiDQpGcm9tOiB0ZXN0LWFsaWFzQG44bi5pbw0KVG86IHRlc3QtdG9AbjhuLmlvDQpDYzogdGVzdC1jY0BuOG4uaW8NCkJjYzogdGVzdC1iY2NAbjhuLmlvDQpSZXBseS1UbzogdGVzdC1yZXBseUBuOG4uaW8NClN1YmplY3Q6IFRlc3QgRHJhZnQgU3ViamVjdA0KTWVzc2FnZS1JRDogdGVzdC1tZXNzYWdlLWlkDQpEYXRlOiBNb24sIDE2IERlYyAyMDI0IDEyOjM0OjU2ICswMDAwDQpNSU1FLVZlcnNpb246IDEuMA0KDQotLS0tdGVzdC1ib3VuZGFyeQ0KQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PXV0Zi04DQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0DQoNClRlc3QgRHJhZnQgTWVzc2FnZQ0KLS0tLXRlc3QtYm91bmRhcnkNCkNvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbjsgbmFtZT1maWxlLmpzb24NCkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IGJhc2U2NA0KQ29udGVudC1EaXNwb3NpdGlvbjogYXR0YWNobWVudDsgZmlsZW5hbWU9ZmlsZS5qc29uDQoNClczc2lZbWx1WVhKNUlqcDBjblZsZlYwPQ0KLS0tLXRlc3QtYm91bmRhcnkNCg==',
threadId: 'test-thread-id',
},
})
.query({ userId: 'me', uploadType: 'media' })
.reply(200, messages[0]);
gmailNock.delete('/v1/users/me/drafts/test-draft-id').reply(200, messages[0]);
gmailNock
.get('/v1/users/me/drafts/test-draft-id')
.query({ format: 'raw' })
.reply(200, {
message: {
...messages[0],
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
},
});
gmailNock
.get('/v1/users/me/drafts')
.query({
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: true,
includeSpamTrash: true,
maxResults: 100,
})
.reply(200, { drafts: messages });
gmailNock
.get('/v1/users/me/drafts/a1b2c3d4e5f6g7h8')
.query({
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: true,
includeSpamTrash: true,
maxResults: 100,
format: 'raw',
})
.reply(200, {
message: {
...messages[0],
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
},
});
gmailNock
.get('/v1/users/me/drafts/z9y8x7w6v5u4t3s2')
.query({
dataPropertyAttachmentsPrefixName: 'attachment_',
downloadAttachments: true,
includeSpamTrash: true,
maxResults: 100,
format: 'raw',
})
.reply(200, {
message: {
...messages[1],
raw: 'TUlNRS1WZXJzaW9uOiAxLjANCkRhdGU6IEZyaSwgMTMgRGVjIDIwMjQgMTE6MTU6MDEgKzAxMDANCk1lc3NhZ2UtSUQ6IDxDQUVHQVByb3d1ZEduS1h4cXJoTWpPdXhhbVRoN3lBcmp3UDdPRDlVQnEtSnBrYjBYOXdAbWFpbC5nbWFpbC5jb20-DQpTdWJqZWN0OiBUZXN0IGRyYWZ0DQpGcm9tOiBub2RlIHFhIDxub2RlOHFhQGdtYWlsLmNvbT4NClRvOiB0ZXN0QGdtYWlsLmNvbQ0KQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PSIwMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyIg0KDQotLTAwMDAwMDAwMDAwMDlkNThiNjA2MjkyNDFhMjINCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD0iVVRGLTgiDQoNCmRyYWZ0IGJvZHkNCg0KLS0wMDAwMDAwMDAwMDA5ZDU4YjYwNjI5MjQxYTIyDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbDsgY2hhcnNldD0iVVRGLTgiDQoNCjxkaXYgZGlyPSJsdHIiPmRyYWZ0IGJvZHk8YnI-PC9kaXY-DQoNCi0tMDAwMDAwMDAwMDAwOWQ1OGI2MDYyOTI0MWEyMi0t',
},
});
});
testWorkflows(['nodes/Google/Gmail/test/v2/drafts.workflow.json']);
it('should make the correct network calls', () => {
gmailNock.done();
});
});
describe('Threads', () => {
const gmailNock = nock('https://www.googleapis.com/gmail');
beforeAll(() => {
gmailNock.get('/v1/users/me/threads').query({ maxResults: 2 }).reply(200, {
threads: messages,
});
gmailNock
.get('/v1/users/me/threads')
.query((query) => {
return (
query.includeSpamTrash === 'true' &&
query.labelIds === 'CHAT' &&
!!query.q &&
query.q.includes('has:attachment') &&
query.q.includes('before:') &&
query.q.includes('after:')
);
})
.reply(200, { threads: messages });
gmailNock
.post('/v1/users/me/threads/test-thread-id/modify', { addLabelIds: ['CHAT'] })
.reply(200, messages[0]);
gmailNock
.post('/v1/users/me/threads/test-thread-id/modify', { removeLabelIds: ['CHAT'] })
.reply(200, messages[0]);
gmailNock.delete('/v1/users/me/threads/test-thread-id').reply(200, messages[0]);
gmailNock
.get('/v1/users/me/threads/test-thread-id')
.query({
format: 'metadata',
metadataHeaders: ['From', 'To', 'Cc', 'Bcc', 'Subject'],
})
.reply(200, messages[0]);
gmailNock.get('/v1/users/me/labels').times(2).reply(200, { labels });
gmailNock
.get('/v1/users/me/threads/test-thread-id')
.query({ format: 'full' })
.reply(200, messages[0]);
gmailNock.post('/v1/users/me/threads/test-thread-id/trash').reply(200, messages[0]);
gmailNock.post('/v1/users/me/threads/test-thread-id/untrash').reply(200, messages[0]);
gmailNock
.get('/v1/users/me/messages/test%20snippet')
.query({
userId: 'me',
uploadType: 'media',
format: 'metadata',
})
.reply(200, messages[0]);
gmailNock.get('/v1/users/me/profile').reply(200, { emailAddress: 'test@n8n.io' });
gmailNock
.post('/v1/users/me/messages/send')
.query({ userId: 'me', uploadType: 'media', format: 'metadata' })
.reply(200, messages[0]);
});
testWorkflows(['nodes/Google/Gmail/test/v2/threads.workflow.json']);
it('should make the correct network calls', () => {
gmailNock.done();
});
});
describe('loadOptions', () => {
describe('getLabels', () => {
it('should return a list of Gmail labels', async () => {
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
getNode: jest.fn(() => mock<INode>()),
helpers: mock<ILoadOptionsFunctions['helpers']>({
requestWithAuthentication: jest
.fn()
// 2 pages of labels
.mockImplementationOnce(async () => ({ labels, nextPageToken: 'nextPageToken' }))
.mockImplementationOnce(async () => ({ labels })),
}),
});
expect(await getLabels.call(loadOptionsFunctions)).toEqual([
{ name: 'CHAT', value: 'CHAT' },
{ name: 'CHAT', value: 'CHAT' },
{ name: 'SENT', value: 'SENT' },
{ name: 'SENT', value: 'SENT' },
]);
});
});
describe('getThreadMessages', () => {
it('should return a list of Gmail thread messages', async () => {
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
getNode: jest.fn(() => mock<INode>()),
helpers: mock<ILoadOptionsFunctions['helpers']>({
requestWithAuthentication: jest.fn(async () => ({ messages })),
}),
});
expect(await getThreadMessages.call(loadOptionsFunctions)).toEqual([
{
name: "Don't miss our exclusive holiday discounts on all items! Act now before the sale ends.",
value: 'a1b2c3d4e5f6g7h8',
},
{
name: 'Your friend John just shared a new photo with you! Check it out now.',
value: 'z9y8x7w6v5u4t3s2',
},
]);
});
});
describe('getGmailAliases', () => {
it('should return a list of Gmail aliases', async () => {
const loadOptionsFunctions = mockDeep<ILoadOptionsFunctions>({
getNode: jest.fn(() => mock<INode>()),
helpers: mock<ILoadOptionsFunctions['helpers']>({
requestWithAuthentication: jest.fn(async () => ({
sendAs: [
{ isDefault: false, sendAsEmail: 'alias1@n8n.io' },
{ isDefault: true, sendAsEmail: 'alias2@n8n.io' },
],
})),
}),
});
expect(await getGmailAliases.call(loadOptionsFunctions)).toEqual([
{
name: 'alias1@n8n.io',
value: 'alias1@n8n.io',
},
{
name: 'alias2@n8n.io (Default)',
value: 'alias2@n8n.io',
},
]);
});
});
});
});