trash-cleaner
Version:
Finds and deletes trash email in the mailbox
228 lines (187 loc) • 8.11 kB
text/typescript
import sinon from 'sinon';
import { Email } from '../../lib/client/email-client.js';
import { TrashCleaner, TrashCleanerFactory } from '../../lib/trash-cleaner.js';
import { ProgressReporter } from '../../lib/reporter/progress-reporter.js';
/**
* Integration tests that exercise the full flow with mocked email clients.
*/
describe('Integration: Full cleanup flow', () => {
let client: any, reporter: ProgressReporter;
before(() => {
sinon.stub(console, 'log');
});
after(() => {
(console.log as sinon.SinonStub).restore();
});
beforeEach(() => {
client = {
getUnreadEmails: sinon.stub(),
deleteEmails: sinon.stub().resolves(),
archiveEmails: sinon.stub().resolves(),
markAsReadEmails: sinon.stub().resolves()
};
reporter = new ProgressReporter();
});
it('processes mixed actions correctly across multiple emails', async () => {
const spamEmail = new Email();
spamEmail.id = '1';
spamEmail.from = 'spammer@evil.com';
spamEmail.subject = 'Win a casino prize!';
spamEmail.body = 'Click here to win big at the casino';
spamEmail.snippet = 'Click here to win big';
spamEmail.labels = ['spam'];
const newsletter = new Email();
newsletter.id = '2';
newsletter.from = 'news@company.com';
newsletter.subject = 'Weekly newsletter digest';
newsletter.body = 'Here is your weekly newsletter';
newsletter.snippet = 'Here is your weekly';
newsletter.labels = ['inbox'];
const notification = new Email();
notification.id = '3';
notification.from = 'noreply@service.com';
notification.subject = 'New notification received';
notification.body = 'You have a new notification';
notification.snippet = 'You have a new';
notification.labels = ['inbox'];
const safeEmail = new Email();
safeEmail.id = '4';
safeEmail.from = 'friend@example.com';
safeEmail.subject = 'Hello!';
safeEmail.body = 'How are you?';
safeEmail.snippet = 'How are you?';
safeEmail.labels = ['inbox'];
client.getUnreadEmails.resolves([spamEmail, newsletter, notification, safeEmail]);
const keywords = [
{ value: 'casino', fields: ['*'], labels: ['spam'], action: 'delete' },
{ value: 'newsletter', fields: ['subject'], labels: ['inbox'], action: 'archive' },
{ value: 'notification', fields: ['subject'], labels: ['inbox'], action: 'mark-as-read' }
];
const cleaner = new TrashCleaner(client, keywords, reporter);
await cleaner.cleanTrash(false);
sinon.assert.calledOnce(client.deleteEmails);
sinon.assert.calledWith(client.deleteEmails, [spamEmail]);
sinon.assert.calledOnce(client.archiveEmails);
sinon.assert.calledWith(client.archiveEmails, [newsletter]);
sinon.assert.calledOnce(client.markAsReadEmails);
sinon.assert.calledWith(client.markAsReadEmails, [notification]);
});
it('respects allowlist even when rules match', async () => {
const email = new Email();
email.id = '1';
email.from = 'boss@trusted.com';
email.subject = 'Free casino bonus';
email.body = 'casino offer';
email.snippet = 'casino offer';
email.labels = ['inbox'];
client.getUnreadEmails.resolves([email]);
const keywords = [
{ value: 'casino', fields: ['*'], labels: ['*'], action: 'delete' }
];
const allowlist = ['@trusted\\.com'];
const cleaner = new TrashCleaner(client, keywords, reporter, allowlist);
await cleaner.cleanTrash(false);
sinon.assert.notCalled(client.deleteEmails);
});
it('dry-run mode identifies but does not act on emails', async () => {
const email = new Email();
email.id = '1';
email.from = 'spammer@test.com';
email.subject = 'Buy now';
email.body = 'casino win';
email.snippet = 'casino';
email.labels = ['spam'];
client.getUnreadEmails.resolves([email]);
const keywords = [
{ value: 'casino', fields: ['*'], labels: ['*'], action: 'delete' }
];
const cleaner = new TrashCleaner(client, keywords, reporter);
await cleaner.cleanTrash(true /* dryRun */);
sinon.assert.notCalled(client.deleteEmails);
sinon.assert.notCalled(client.archiveEmails);
sinon.assert.notCalled(client.markAsReadEmails);
});
it('handles empty mailbox gracefully', async () => {
client.getUnreadEmails.resolves([]);
const keywords = [
{ value: 'spam', fields: ['*'], labels: ['*'] }
];
const cleaner = new TrashCleaner(client, keywords, reporter);
await cleaner.cleanTrash(false);
sinon.assert.notCalled(client.deleteEmails);
});
it('handles no matching emails gracefully', async () => {
const email = new Email();
email.id = '1';
email.from = 'friend@example.com';
email.subject = 'Hello';
email.body = 'Hi there';
email.snippet = 'Hi there';
email.labels = ['inbox'];
client.getUnreadEmails.resolves([email]);
const keywords = [
{ value: 'casino', fields: ['*'], labels: ['spam'] }
];
const cleaner = new TrashCleaner(client, keywords, reporter);
await cleaner.cleanTrash(false);
sinon.assert.notCalled(client.deleteEmails);
});
});
describe('Integration: TrashCleanerFactory full flow', () => {
before(() => {
sinon.stub(console, 'log');
});
after(() => {
(console.log as sinon.SinonStub).restore();
});
it('creates cleaner and processes emails end-to-end', async () => {
const configStore: any = {
getJson: sinon.stub(),
putJson: sinon.stub().resolves()
};
configStore.getJson.withArgs('keywords.json').resolves([
{ value: 'spam', fields: '*', labels: '*', action: 'delete' },
{ value: 'promo', fields: 'subject', labels: 'inbox', action: 'archive' }
]);
configStore.getJson.withArgs('allowlist.json').resolves(['vip@company\\.com']);
configStore.getJson.withArgs('seen.json').resolves(null);
configStore.getJson.withArgs('llm-providers.json').resolves(null);
const email1 = new Email();
email1.id = '1';
email1.from = 'spammer@evil.com';
email1.body = 'spam content';
email1.subject = 'spam';
email1.snippet = 'spam';
email1.labels = ['junk'];
const email2 = new Email();
email2.id = '2';
email2.from = 'store@shop.com';
email2.body = 'sale';
email2.subject = 'Weekly promo deals';
email2.snippet = 'promo';
email2.labels = ['inbox'];
const email3 = new Email();
email3.id = '3';
email3.from = 'vip@company.com';
email3.body = 'spam content';
email3.subject = 'Meeting spam';
email3.snippet = 'spam';
email3.labels = ['inbox'];
const client = {
getUnreadEmails: sinon.stub().resolves([email1, email2, email3]),
deleteEmails: sinon.stub().resolves(),
archiveEmails: sinon.stub().resolves(),
markAsReadEmails: sinon.stub().resolves()
};
const factory = new TrashCleanerFactory(configStore, client, false);
const cleaner = await factory.getInstance();
await cleaner.cleanTrash(false);
// email1 matches 'spam' → delete
sinon.assert.calledWith(client.deleteEmails, [email1]);
// email2 matches 'promo' in subject → archive
sinon.assert.calledWith(client.archiveEmails, [email2]);
// email3 matches 'spam' but sender is allowlisted → no action
sinon.assert.calledOnce(client.deleteEmails);
sinon.assert.calledOnce(client.archiveEmails);
});
});