Mailspring/app/spec_disabled/tasks/send-draft-task-spec.es6
2017-09-26 11:36:58 -07:00

590 lines
22 KiB
JavaScript

import {
APIError,
Actions,
AccountStore,
DatabaseStore,
DatabaseWriter,
Message,
Contact,
Task,
SendDraftTask,
NylasAPIRequest,
SoundRegistry,
SyncbackMetadataTask,
} from 'nylas-exports';
const DBt = DatabaseWriter.prototype;
const withoutWhitespace = s => s.replace(/[\n\r\s]/g, '');
xdescribe('SendDraftTask', function sendDraftTask() {
describe('assertDraftValidity', () => {
it('rejects if no from address is specified', () => {
const badTask = new SendDraftTask('1');
badTask.draft = new Message({
from: [],
files: [],
accountId: TEST_ACCOUNT_ID,
headerMessageId: '1',
});
badTask
.assertDraftValidity()
.then(() => {
throw new Error("Shouldn't succeed");
})
.catch(err => {
expect(err.message).toBe('SendDraftTask - you must populate `from` before sending.');
});
});
it('rejects if the from address does not map to any account', () => {
const badTask = new SendDraftTask('1');
badTask.draft = new Message({
from: [new Contact({ email: 'not-configuredthis.nylas.com' })],
accountId: TEST_ACCOUNT_ID,
headerMessageId: '1',
});
badTask
.assertDraftValidity()
.then(() => {
throw new Error("Shouldn't succeed");
})
.catch(err => {
expect(err.message).toBe(
'SendDraftTask - you can only send drafts from a configured account.'
);
});
});
});
describe('performRemote', () => {
beforeEach(() => {
this.response = {
version: 2,
id: '1233123AEDF1',
account_id: TEST_ACCOUNT_ID,
from: [new Contact({ email: TEST_ACCOUNT_EMAIL })],
subject: 'New Draft',
body: 'hello world',
to: [
new Contact({
name: 'Dummy',
email: 'dummythis.nylas.com',
}),
],
};
spyOn(NylasAPIRequest.prototype, 'run').andCallFake(options => {
if (options.success) {
options.success(this.response);
}
return Promise.resolve(this.response);
});
spyOn(DBt, 'unpersistModel').andReturn(Promise.resolve());
spyOn(DBt, 'persistModel').andReturn(Promise.resolve());
spyOn(SoundRegistry, 'playSound');
spyOn(Actions, 'draftDeliverySucceeded');
});
// The tests below are invoked twice, once with a new this.draft and one with a
// persisted this.draft.
const sharedTests = () => {
it('should return Task.Status.Success', () => {
waitsForPromise(() => {
this.task.performLocal();
return this.task.performRemote().then(status => {
expect(status).toBe(Task.Status.Success);
});
});
});
it('makes a send request with the correct data', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();
expect(NylasAPIRequest.prototype.run.callCount).toBe(1);
const options = NylasAPIRequest.prototype.run.mostRecentCall.args[0];
expect(options.path).toBe('/send');
expect(options.method).toBe('POST');
expect(options.accountId).toBe(TEST_ACCOUNT_ID);
expect(options.body).toEqual(this.draft.toJSON());
})
);
});
it('should always send the draft body in the request body (joined attribute check)', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(NylasAPIRequest.prototype.run.calls.length).toBe(1);
const options = NylasAPIRequest.prototype.run.mostRecentCall.args[0];
expect(options.body.body).toBe('hello world');
})
);
});
describe('saving the sent message', () => {
it('should preserve the draft client id', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(DBt.persistModel).toHaveBeenCalled();
const model = DBt.persistModel.mostRecentCall.args[0];
expect(model.headerMessageId).toEqual(this.draft.headerMessageId);
expect(model.id).toEqual(this.response.id);
expect(model.draft).toEqual(false);
})
);
});
it('should preserve metadata, but not version numbers', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(DBt.persistModel).toHaveBeenCalled();
const model = DBt.persistModel.mostRecentCall.args[0];
expect(model.pluginMetadata.length).toEqual(this.draft.pluginMetadata.length);
for (const { pluginId, value } of this.draft.pluginMetadata) {
const updated = model.metadataObjectForPluginId(pluginId);
expect(updated.value).toEqual(value);
expect(updated.version).toEqual(0);
}
})
);
});
});
it('should notify the draft was sent', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
const args = Actions.draftDeliverySucceeded.calls[0].args[0];
expect(args.headerMessageId).toBe(this.draft.headerMessageId);
})
);
});
it('should queue tasks to sync back the metadata on the new message', () => {
spyOn(Actions, 'queueTask');
waitsForPromise(() =>
this.task.performRemote().then(() => {
let metadataTasks = Actions.queueTask.calls.map(call => call.args[0]);
metadataTasks = metadataTasks.filter(task => task instanceof SyncbackMetadataTask);
expect(metadataTasks.length).toEqual(this.draft.pluginMetadata.length);
this.draft.pluginMetadata.forEach((pluginMetadatum, idx) => {
expect(metadataTasks[idx].headerMessageId).toEqual(this.draft.headerMessageId);
expect(metadataTasks[idx].modelClassName).toEqual('Message');
expect(metadataTasks[idx].pluginId).toEqual(pluginMetadatum.pluginId);
});
})
);
});
it('should play a sound', () => {
spyOn(AppEnv.config, 'get').andReturn(true);
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(AppEnv.config.get).toHaveBeenCalledWith('core.sending.sounds');
expect(SoundRegistry.playSound).toHaveBeenCalledWith('send');
})
);
});
it("shouldn't play a sound if the config is disabled", () => {
spyOn(AppEnv.config, 'get').andReturn(false);
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(AppEnv.config.get).toHaveBeenCalledWith('core.sending.sounds');
expect(SoundRegistry.playSound).not.toHaveBeenCalled();
})
);
});
describe('when there are errors', () => {
beforeEach(() => {
spyOn(Actions, 'draftDeliveryFailed');
jasmine.unspy(NylasAPIRequest.prototype, 'run');
});
it('notifies of a permanent error of misc error types', () => {
// DB error
let thrownError = null;
spyOn(AppEnv, 'reportError');
jasmine.unspy(DBt, 'persistModel');
spyOn(DBt, 'persistModel').andCallFake(() => {
thrownError = new Error('db error');
throw thrownError;
});
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status[0]).toBe(Task.Status.Failed);
expect(status[1]).toBe(thrownError);
expect(Actions.draftDeliveryFailed).toHaveBeenCalled();
expect(AppEnv.reportError).toHaveBeenCalled();
})
);
});
it("retries the task if 'Invalid message public id'", () => {
spyOn(NylasAPIRequest.prototype, 'run').andCallFake(options => {
if (options.body.reply_to_message_id) {
const err = new APIError({ body: 'Invalid message public id' });
return Promise.reject(err);
}
if (options.success) {
options.success(this.response);
}
return Promise.resolve(this.response);
});
this.draft.replyToHeaderMessageId = 'reply-123';
this.draft.threadId = 'thread-123';
waitsForPromise(() => {
return this.task.performRemote(this.draft).then(() => {
expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();
expect(NylasAPIRequest.prototype.run.callCount).toEqual(2);
const req1 = NylasAPIRequest.prototype.run.calls[0].args[0];
const req2 = NylasAPIRequest.prototype.run.calls[1].args[0];
expect(req1.body.reply_to_message_id).toBe('reply-123');
expect(req1.body.thread_id).toBe('thread-123');
expect(req2.body.reply_to_message_id).toBe(null);
expect(req2.body.thread_id).toBe('thread-123');
});
});
});
it("retries the task if 'Invalid message public id'", () => {
spyOn(NylasAPIRequest.prototype, 'run').andCallFake(options => {
if (options.body.reply_to_message_id) {
return Promise.reject(new APIError({ body: 'Invalid thread' }));
}
if (options.success) {
options.success(this.response);
}
return Promise.resolve(this.response);
});
this.draft.replyToHeaderMessageId = 'reply-123';
this.draft.threadId = 'thread-123';
waitsForPromise(() =>
this.task.performRemote(this.draft).then(() => {
expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();
expect(NylasAPIRequest.prototype.run.callCount).toEqual(2);
const req1 = NylasAPIRequest.prototype.run.calls[0].args[0];
const req2 = NylasAPIRequest.prototype.run.calls[1].args[0];
expect(req1.body.reply_to_message_id).toBe('reply-123');
expect(req1.body.thread_id).toBe('thread-123');
expect(req2.body.reply_to_message_id).toBe(null);
expect(req2.body.thread_id).toBe(null);
})
);
});
it('notifies of a permanent error on 500 errors', () => {
const thrownError = new APIError({ statusCode: 500, body: 'err' });
spyOn(AppEnv, 'reportError');
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status[0]).toBe(Task.Status.Failed);
expect(status[1]).toBe(thrownError);
expect(Actions.draftDeliveryFailed).toHaveBeenCalled();
})
);
});
it('notifies us and users of a permanent error on 400 errors', () => {
const thrownError = new APIError({ statusCode: 400, body: 'err' });
spyOn(AppEnv, 'reportError');
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status[0]).toBe(Task.Status.Failed);
expect(status[1]).toBe(thrownError);
expect(Actions.draftDeliveryFailed).toHaveBeenCalled();
})
);
});
it('presents helpful error messages for 402 errors (security blocked)', () => {
const thrownError = new APIError({
statusCode: 402,
body: {
message: 'Message content rejected for security reasons',
server_error:
'552 : 5.7.0 This message was blocked because its content presents a potential\n5.7.0 security issue. Please visit\n5.7.0 https://support.google.com/mail/answer/6590 to review our message\n5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp',
type: 'api_error',
},
});
const expectedMessage = `
Sorry, this message could not be sent because it was rejected by your mail provider. (Message content rejected for security reasons)
552 : 5.7.0 This message was blocked because its content presents a potential
5.7.0 security issue. Please visit
5.7.0 https://support.google.com/mail/answer/6590 to review our message
5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp
`;
spyOn(AppEnv, 'reportError');
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status[0]).toBe(Task.Status.Failed);
expect(status[1]).toBe(thrownError);
expect(Actions.draftDeliveryFailed).toHaveBeenCalled();
const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage;
expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));
})
);
});
it('presents helpful error messages for 402 errors (recipient failed)', () => {
const thrownError = new APIError({
statusCode: 402,
body: {
message: 'Sending to at least one recipient failed.',
server_error: "<<Don't know what this looks like >>",
type: 'api_error',
},
});
const expectedMessage =
'This message could not be delivered to at least one recipient. (Note: other recipients may have received this message - you should check Sent Mail before re-sending this message.)';
spyOn(AppEnv, 'reportError');
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status[0]).toBe(Task.Status.Failed);
expect(status[1]).toBe(thrownError);
expect(Actions.draftDeliveryFailed).toHaveBeenCalled();
const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage;
expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));
})
);
});
describe('checking the promise chain halts on errors', () => {
beforeEach(() => {
spyOn(AppEnv, 'reportError');
spyOn(this.task, 'sendMessage').andCallThrough();
spyOn(this.task, 'onSuccess').andCallThrough();
spyOn(this.task, 'onError').andCallThrough();
this.expectBlockedChain = () => {
expect(this.task.sendMessage).toHaveBeenCalled();
expect(this.task.onSuccess).not.toHaveBeenCalled();
expect(this.task.onError).toHaveBeenCalled();
};
});
it('halts on 500s', () => {
const thrownError = new APIError({ statusCode: 500, body: 'err' });
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() => this.task.performRemote().then(() => this.expectBlockedChain()));
});
it('halts on 400s', () => {
const thrownError = new APIError({ statusCode: 400, body: 'err' });
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() => this.task.performRemote().then(() => this.expectBlockedChain()));
});
it('halts and retries on not permanent error codes', () => {
const thrownError = new APIError({ statusCode: 409, body: 'err' });
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() => this.task.performRemote().then(() => this.expectBlockedChain()));
});
it('halts on other errors', () => {
const thrownError = new Error('oh no');
spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));
waitsForPromise(() => this.task.performRemote().then(() => this.expectBlockedChain()));
});
it("doesn't halt on success", () => {
// Don't spy reportError to make sure to fail the test on unexpected
// errors
jasmine.unspy(AppEnv, 'reportError');
spyOn(NylasAPIRequest.prototype, 'run').andCallFake(options => {
if (options.success) {
options.success(this.response);
}
return Promise.resolve(this.response);
});
waitsForPromise(() =>
this.task.performRemote().then(status => {
expect(status).toBe(Task.Status.Success);
expect(this.task.sendMessage).toHaveBeenCalled();
expect(this.task.onSuccess).toHaveBeenCalled();
expect(this.task.onError).not.toHaveBeenCalled();
})
);
});
});
});
};
describe('with a new draft', () => {
beforeEach(() => {
this.draft = new Message({
version: 1,
headerMessageId: 'client-id',
accountId: TEST_ACCOUNT_ID,
from: [new Contact({ email: TEST_ACCOUNT_EMAIL })],
subject: 'New Draft',
draft: true,
body: 'hello world',
files: [],
});
this.draft.directlyAttachMetadata('pluginIdA', { tracked: true });
this.draft.directlyAttachMetadata('pluginIdB', { a: true, b: 2 });
this.draft.metadataObjectForPluginId('pluginIdA').version = 2;
this.task = new SendDraftTask('client-id');
this.calledBody = "ERROR: The body wasn't included!";
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(this.draft));
});
sharedTests();
it('should locally convert the draft to a message on send', () => {
waitsForPromise(() =>
this.task.performRemote().then(() => {
expect(DBt.persistModel).toHaveBeenCalled();
const model = DBt.persistModel.calls[0].args[0];
expect(model.headerMessageId).toBe(this.draft.headerMessageId);
expect(model.id).toBe(this.response.id);
expect(model.draft).toBe(false);
})
);
});
});
describe('with an existing persisted draft', () => {
beforeEach(() => {
this.draft = new Message({
version: 1,
headerMessageId: 'client-id',
id: 'server-123',
accountId: TEST_ACCOUNT_ID,
from: [new Contact({ email: TEST_ACCOUNT_EMAIL })],
subject: 'New Draft',
draft: true,
body: 'hello world',
to: [
new Contact({
name: 'Dummy',
email: 'dummythis.nylas.com',
}),
],
files: [],
});
this.draft.directlyAttachMetadata('pluginIdA', { tracked: true });
this.draft.directlyAttachMetadata('pluginIdB', { a: true, b: 2 });
this.draft.metadataObjectForPluginId('pluginIdA').version = 2;
this.task = new SendDraftTask('client-id');
this.calledBody = "ERROR: The body wasn't included!";
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(this.draft));
});
sharedTests();
});
});
describe('hasCustomBodyPerRecipient', () => {
beforeEach(() => {
this.task = new SendDraftTask('client-id');
this.task.draft = new Message({
version: 1,
headerMessageId: 'client-id',
id: 'server-123',
accountId: TEST_ACCOUNT_ID,
from: [new Contact({ email: TEST_ACCOUNT_EMAIL })],
subject: 'New Draft',
draft: true,
body: 'hello world',
to: [
new Contact({
name: 'Dummy',
email: 'dummythis.nylas.com',
}),
],
files: [],
});
this.task.draft.directlyAttachMetadata('open-tracking', true);
this.task.draft.directlyAttachMetadata('link-tracking', true);
this.applySpies = (customValues = {}) => {
let value = { provider: customValues['AccountStore.accountForId'] || 'gmail' };
spyOn(AccountStore, 'accountForId').andReturn(value);
value = customValues['AppEnv.packages.pluginIdFor'] || (name => name);
spyOn(AppEnv.packages, 'pluginIdFor').andCallFake(value);
value = { length: customValues['draft.participants'] || 5 };
spyOn(this.task.draft, 'participants').andReturn(value);
};
});
it('should return false if the provider is eas', () => {
this.applySpies({ 'AccountStore.accountForId': 'eas' });
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it('should return false if the open-tracking id is null', () => {
const fake = name => {
return name === 'open-tracking' ? null : name;
};
this.applySpies({ 'AppEnv.packages.pluginIdFor': fake });
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it('should return false if the link-tracking id is null', () => {
const fake = name => {
return name === 'link-tracking' ? null : name;
};
this.applySpies({ 'AppEnv.packages.pluginIdFor': fake });
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it('should return false if neither open-tracking nor link-tracking is on', () => {
this.applySpies();
this.task.draft.directlyAttachMetadata('open-tracking', false);
this.task.draft.directlyAttachMetadata('link-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it('should return true if only open-tracking is on', () => {
this.applySpies();
this.task.draft.directlyAttachMetadata('link-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(true);
});
it('should return true if only link-tracking is on', () => {
this.applySpies();
this.task.draft.directlyAttachMetadata('open-tracking', false);
expect(this.task.hasCustomBodyPerRecipient()).toBe(true);
});
it('should return false if there are too many participants', () => {
this.applySpies({ 'draft.participants': 15 });
expect(this.task.hasCustomBodyPerRecipient()).toBe(false);
});
it('should return true otherwise', () => {
this.applySpies();
expect(this.task.hasCustomBodyPerRecipient()).toBe(true);
});
});
});