UNPKG

calendar-ai-bot

Version:

Serverless email calendar invite creation bot using AWS Lambda, SES and OpenAI API

244 lines (215 loc) 9.22 kB
/* global describe, it */ var assert = require("assert"); var index = require("../index"); describe('index.js', function() { describe('#sendCalendarInvite()', function() { it('should send calendar invite when event is detected', function(done) { var sentEmails = []; var data = { eventInfo: { hasEvent: true, title: 'Team Meeting', description: 'Quarterly review meeting', dateTime: '2024-01-02T14:00:00', location: 'Conference Room A', duration: 'PT1H' }, senderEmail: 'jane@example.com', config: { fromEmail: 'bot@example.com', subjectPrefix: 'Calendar Invite: ' }, log: function() {}, ses: { send: function(options, callback) { sentEmails.push(options); callback(null, {MessageId: 'test-message-id'}); } } }; index.sendCalendarInvite(data) .then(function() { assert.equal(sentEmails.length, 1, 'Should send one email'); var sentEmail = sentEmails[0]; assert.equal(sentEmail.input.Destination.ToAddresses[0], 'jane@example.com', 'Should send to original sender'); assert.equal(sentEmail.input.FromEmailAddress, 'bot@example.com', 'Should send from bot email'); assert.ok(sentEmail.input.Content.Raw, 'Should use Raw content format for calendar invite'); var rawContent = sentEmail.input.Content.Raw.Data.toString(); assert.ok(rawContent.includes('Subject: Calendar Invite: Team Meeting'), 'Should include subject prefix and title'); assert.ok(rawContent.includes('Team Meeting'), 'Should include event title in email body'); assert.ok(rawContent.includes('Content-Type: text/calendar;method=REQUEST;name="invite.ics"'), 'Should include calendar MIME type'); assert.ok(rawContent.includes('Content-Transfer-Encoding: base64'), 'Should include ICS content as attachment'); done(); }) .catch(done); }); it('should not send invite when no event is detected', function(done) { var data = { eventInfo: { hasEvent: false }, senderEmail: 'jane@example.com', config: { fromEmail: 'bot@example.com' }, log: function() {}, callback: function() { done(); // Should call callback to exit early } }; // This should call the callback and not return a promise index.sendCalendarInvite(data); }); it('should handle SES sending errors gracefully', function(done) { var data = { eventInfo: { hasEvent: true, title: 'Test Event', dateTime: '2024-01-02T10:00:00' }, senderEmail: 'test@example.com', config: { fromEmail: 'bot@example.com', subjectPrefix: '' }, log: function() {}, ses: { send: function(options, callback) { callback(new Error('SES sending failed')); } } }; index.sendCalendarInvite(data) .catch(function(err) { assert.ok(err, 'Should throw error when SES fails'); assert.ok(err.message.includes('Calendar invite sending failed'), 'Should have descriptive error message'); done(); }); }); it('should generate valid ICS content', function(done) { var sentEmails = []; var data = { eventInfo: { hasEvent: true, title: 'Test Event with Special Characters & Symbols', description: 'Event description with details', dateTime: '2024-01-02T15:30:00', location: 'Room 123, Building A', duration: 'PT2H' }, senderEmail: 'organizer@example.com', config: { fromEmail: 'bot@example.com', subjectPrefix: '' }, log: function() {}, ses: { send: function(options, callback) { sentEmails.push(options); callback(null, {MessageId: 'test-id'}); } } }; index.sendCalendarInvite(data) .then(function() { var rawContent = sentEmails[0].input.Content.Raw.Data.toString(); // Check that the email contains base64 encoded content assert.ok(rawContent.includes('Content-Transfer-Encoding: base64'), 'Should contain base64 encoded ICS'); // Check that the email structure includes proper calendar MIME type assert.ok(rawContent.includes('Content-Type: text/calendar;method=REQUEST;name="invite.ics"'), 'Should include proper calendar MIME type'); assert.ok(rawContent.includes('Content-Disposition: attachment; filename="invite.ics"'), 'Should include attachment disposition'); assert.ok(rawContent.includes('Content-Transfer-Encoding: base64'), 'Should use base64 encoding'); // The ICS content is base64 encoded - we validate that the structure is correct // Gmail will decode this properly when it receives the email done(); }) .catch(done); }); it('should handle missing event details gracefully', function(done) { var sentEmails = []; var data = { eventInfo: { hasEvent: true, title: 'Minimal Event', dateTime: '2024-01-02T10:00:00' // Missing description and location }, senderEmail: 'test@example.com', config: { fromEmail: 'bot@example.com', subjectPrefix: 'Invite: ' }, log: function() {}, ses: { send: function(options, callback) { sentEmails.push(options); callback(null, {MessageId: 'test-id'}); } } }; index.sendCalendarInvite(data) .then(function() { var rawContent = sentEmails[0].input.Content.Raw.Data.toString(); // Check that the email contains base64 encoded content assert.ok(rawContent.includes('Content-Transfer-Encoding: base64'), 'Should contain base64 encoded ICS'); // For testing purposes, just check the structure for missing fields case // In real usage, Gmail will decode the base64 content properly // Check that missing fields are handled assert.ok(rawContent.includes('Not specified'), 'Should show "Not specified" for missing location'); assert.ok(rawContent.includes('No description'), 'Should show "No description" for missing description'); // The ICS is base64 encoded, but we can verify the email structure is correct assert.ok(rawContent.includes('Content-Type: text/calendar;method=REQUEST'), 'Should include calendar content type'); done(); }) .catch(done); }); it('should generate timezone-aware ICS content for Europe/London', function(done) { var sentEmails = []; var data = { eventInfo: { hasEvent: true, title: 'UK Meeting', description: 'Summer timezone test', dateTime: '2024-07-15T11:00:00', // Summer time (BST) location: 'London Office', duration: 'PT1H' }, senderEmail: 'london@example.com', config: { fromEmail: 'bot@example.com', subjectPrefix: '', defaultTimezone: 'Europe/London' }, log: function() {}, ses: { send: function(options, callback) { sentEmails.push(options); callback(null, {MessageId: 'test-id'}); } } }; index.sendCalendarInvite(data) .then(function() { var rawContent = sentEmails[0].input.Content.Raw.Data.toString(); // Check that the email contains base64 encoded content assert.ok(rawContent.includes('Content-Transfer-Encoding: base64'), 'Should contain base64 encoded ICS'); // Decode the base64 content to verify timezone handling var icsMatch = rawContent.match(/Content-Transfer-Encoding: base64\r\n\r\n([A-Za-z0-9+/=\r\n]+)/); if (icsMatch) { var base64Content = icsMatch[1].replace(/\r\n/g, ''); var icsContent = Buffer.from(base64Content, 'base64').toString('utf8'); // Check that local times are preserved (no timezone offset adjustment) // Summer time meeting at 11:00 should stay as 11:00 local time without Z suffix assert.ok(icsContent.includes('DTSTART:20240715T110000'), 'Should preserve local time (11am stays as 11am)'); assert.ok(icsContent.includes('DTEND:20240715T120000'), 'Should preserve local end time (11am + 1 hour = 12pm)'); assert.ok(icsContent.includes('BEGIN:VEVENT'), 'Should include event block'); assert.ok(icsContent.includes('SUMMARY:UK Meeting'), 'Should include event title'); // Should NOT include VTIMEZONE blocks as they are not needed assert.ok(!icsContent.includes('BEGIN:VTIMEZONE'), 'Should not include VTIMEZONE definition'); } done(); }) .catch(done); }); }); });