calendar-ai-bot
Version:
Serverless email calendar invite creation bot using AWS Lambda, SES and OpenAI API
244 lines (215 loc) • 9.22 kB
JavaScript
/* 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);
});
});
});