This commit is contained in:
2025-05-07 20:20:17 +00:00
parent f6377d1973
commit 630e911589
40 changed files with 8745 additions and 333 deletions

61
test/test.base.ts Normal file
View File

@ -0,0 +1,61 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.js';
import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.js';
/**
* Basic test to check if our integrated classes work correctly
*/
tap.test('verify that SenderReputationMonitor and IPWarmupManager are functioning', async () => {
// Create instances of both classes
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2'],
targetDomains: ['example.com']
});
// Test SenderReputationMonitor
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 });
const reputationData = reputationMonitor.getReputationData('example.com');
expect(reputationData).to.not.be.null;
const summary = reputationMonitor.getReputationSummary();
expect(summary.length).to.be.at.least(1);
// Add and remove domains
reputationMonitor.addDomain('test.com');
reputationMonitor.removeDomain('test.com');
// Test IPWarmupManager
ipWarmupManager.setActiveAllocationPolicy('balanced');
const bestIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
if (bestIP) {
ipWarmupManager.recordSend(bestIP);
const canSendMore = ipWarmupManager.canSendMoreToday(bestIP);
expect(typeof canSendMore).to.equal('boolean');
}
const stageCount = ipWarmupManager.getStageCount();
expect(stageCount).to.be.greaterThan(0);
});
// Final clean-up test
tap.test('clean up after tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
export default tap.start();

193
test/test.bouncemanager.ts Normal file
View File

@ -0,0 +1,193 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { SzPlatformService } from '../ts/platformservice.js';
import { BounceManager, BounceType, BounceCategory } from '../ts/email/classes.bouncemanager.js';
/**
* Test the BounceManager class
*/
tap.test('BounceManager - should be instantiable', async () => {
const bounceManager = new BounceManager();
expect(bounceManager).toBeTruthy();
});
tap.test('BounceManager - should process basic bounce categories', async () => {
const bounceManager = new BounceManager();
// Test hard bounce detection
const hardBounce = await bounceManager.processBounce({
recipient: 'invalid@example.com',
sender: 'sender@example.com',
smtpResponse: 'user unknown',
domain: 'example.com'
});
expect(hardBounce.bounceCategory).toEqual(BounceCategory.HARD);
// Test soft bounce detection
const softBounce = await bounceManager.processBounce({
recipient: 'valid@example.com',
sender: 'sender@example.com',
smtpResponse: 'server unavailable',
domain: 'example.com'
});
expect(softBounce.bounceCategory).toEqual(BounceCategory.SOFT);
// Test auto-response detection
const autoResponse = await bounceManager.processBounce({
recipient: 'away@example.com',
sender: 'sender@example.com',
smtpResponse: 'auto-reply: out of office',
domain: 'example.com'
});
expect(autoResponse.bounceCategory).toEqual(BounceCategory.AUTO_RESPONSE);
});
tap.test('BounceManager - should add and check suppression list entries', async () => {
const bounceManager = new BounceManager();
// Add to suppression list permanently
bounceManager.addToSuppressionList('permanent@example.com', 'Test hard bounce', undefined);
// Add to suppression list temporarily (5 seconds)
const expireTime = Date.now() + 5000;
bounceManager.addToSuppressionList('temporary@example.com', 'Test soft bounce', expireTime);
// Check suppression status
expect(bounceManager.isEmailSuppressed('permanent@example.com')).toEqual(true);
expect(bounceManager.isEmailSuppressed('temporary@example.com')).toEqual(true);
expect(bounceManager.isEmailSuppressed('notsuppressed@example.com')).toEqual(false);
// Get suppression info
const info = bounceManager.getSuppressionInfo('permanent@example.com');
expect(info).toBeTruthy();
expect(info.reason).toEqual('Test hard bounce');
expect(info.expiresAt).toBeUndefined();
// Verify temporary suppression info
const tempInfo = bounceManager.getSuppressionInfo('temporary@example.com');
expect(tempInfo).toBeTruthy();
expect(tempInfo.reason).toEqual('Test soft bounce');
expect(tempInfo.expiresAt).toEqual(expireTime);
// Wait for expiration (6 seconds)
await new Promise(resolve => setTimeout(resolve, 6000));
// Verify permanent suppression is still active
expect(bounceManager.isEmailSuppressed('permanent@example.com')).toEqual(true);
// Verify temporary suppression has expired
expect(bounceManager.isEmailSuppressed('temporary@example.com')).toEqual(false);
});
tap.test('BounceManager - should process SMTP failures correctly', async () => {
const bounceManager = new BounceManager();
const result = await bounceManager.processSmtpFailure(
'recipient@example.com',
'550 5.1.1 User unknown',
{
sender: 'sender@example.com',
statusCode: '550'
}
);
expect(result.bounceType).toEqual(BounceType.INVALID_RECIPIENT);
expect(result.bounceCategory).toEqual(BounceCategory.HARD);
// Check that the email was added to the suppression list
expect(bounceManager.isEmailSuppressed('recipient@example.com')).toEqual(true);
});
tap.test('BounceManager - should process bounce emails correctly', async () => {
const bounceManager = new BounceManager();
// Create a mock bounce email as Smartmail
const bounceEmail = new plugins.smartmail.Smartmail({
from: 'mailer-daemon@example.com',
subject: 'Mail delivery failed: returning message to sender',
body: `
This message was created automatically by mail delivery software.
A message that you sent could not be delivered to one or more of its recipients.
The following address(es) failed:
recipient@example.com
mailbox is full
------ This is a copy of the message, including all the headers. ------
Original-Recipient: rfc822;recipient@example.com
Final-Recipient: rfc822;recipient@example.com
Status: 5.2.2
diagnostic-code: smtp; 552 5.2.2 Mailbox full
`,
creationObjectRef: {}
});
const result = await bounceManager.processBounceEmail(bounceEmail);
expect(result).toBeTruthy();
expect(result.bounceType).toEqual(BounceType.MAILBOX_FULL);
expect(result.bounceCategory).toEqual(BounceCategory.HARD);
expect(result.recipient).toEqual('recipient@example.com');
});
tap.test('BounceManager - should handle retries for soft bounces', async () => {
const bounceManager = new BounceManager({
retryStrategy: {
maxRetries: 2,
initialDelay: 100, // 100ms for test
maxDelay: 1000,
backoffFactor: 2
}
});
// First attempt
const result1 = await bounceManager.processBounce({
recipient: 'retry@example.com',
sender: 'sender@example.com',
bounceType: BounceType.SERVER_UNAVAILABLE,
bounceCategory: BounceCategory.SOFT,
domain: 'example.com'
});
// Email should be suppressed temporarily
expect(bounceManager.isEmailSuppressed('retry@example.com')).toEqual(true);
expect(result1.retryCount).toEqual(1);
expect(result1.nextRetryTime).toBeGreaterThan(Date.now());
// Second attempt
const result2 = await bounceManager.processBounce({
recipient: 'retry@example.com',
sender: 'sender@example.com',
bounceType: BounceType.SERVER_UNAVAILABLE,
bounceCategory: BounceCategory.SOFT,
domain: 'example.com',
retryCount: 1
});
expect(result2.retryCount).toEqual(2);
// Third attempt (should convert to hard bounce)
const result3 = await bounceManager.processBounce({
recipient: 'retry@example.com',
sender: 'sender@example.com',
bounceType: BounceType.SERVER_UNAVAILABLE,
bounceCategory: BounceCategory.SOFT,
domain: 'example.com',
retryCount: 2
});
// Should now be a hard bounce after max retries
expect(result3.bounceCategory).toEqual(BounceCategory.HARD);
// Email should be suppressed permanently
expect(bounceManager.isEmailSuppressed('retry@example.com')).toEqual(true);
const info = bounceManager.getSuppressionInfo('retry@example.com');
expect(info.expiresAt).toBeUndefined(); // Permanent
});
export default tap.start();

261
test/test.contentscanner.ts Normal file
View File

@ -0,0 +1,261 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { ContentScanner, ThreatCategory } from '../ts/security/classes.contentscanner.js';
import { Email } from '../ts/mta/classes.email.js';
// Test instantiation
tap.test('ContentScanner - should be instantiable', async () => {
const scanner = ContentScanner.getInstance({
scanBody: true,
scanSubject: true,
scanAttachments: true
});
expect(scanner).toBeTruthy();
});
// Test singleton pattern
tap.test('ContentScanner - should use singleton pattern', async () => {
const scanner1 = ContentScanner.getInstance();
const scanner2 = ContentScanner.getInstance();
// Both instances should be the same object
expect(scanner1 === scanner2).toEqual(true);
});
// Test clean email can be correctly distinguished from high-risk email
tap.test('ContentScanner - should distinguish between clean and suspicious emails', async () => {
// Create an instance with a higher minimum threat score
const scanner = new ContentScanner({
minThreatScore: 50 // Higher threshold to consider clean
});
// Create a truly clean email with no potentially sensitive data patterns
const cleanEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Project Update',
text: 'The project is on track. Let me know if you have questions.',
html: '<p>The project is on track. Let me know if you have questions.</p>'
});
// Create a highly suspicious email
const suspiciousEmail = new Email({
from: 'admin@bank-fake.com',
to: 'victim@example.com',
subject: 'URGENT: Your account needs verification now!',
text: 'Click here to verify your account or it will be suspended: https://bit.ly/12345',
html: '<p>Click here to verify your account or it will be suspended: <a href="https://bit.ly/12345">click here</a></p>'
});
// Test both emails
const cleanResult = await scanner.scanEmail(cleanEmail);
const suspiciousResult = await scanner.scanEmail(suspiciousEmail);
console.log('Clean vs Suspicious results:', {
cleanScore: cleanResult.threatScore,
suspiciousScore: suspiciousResult.threatScore
});
// Verify the scanner can distinguish between them
// Suspicious email should have a significantly higher score
expect(suspiciousResult.threatScore > cleanResult.threatScore + 40).toEqual(true);
// Verify clean email scans all expected elements
expect(cleanResult.scannedElements.length > 0).toEqual(true);
});
// Test phishing detection in subject
tap.test('ContentScanner - should detect phishing in subject', async () => {
// Create a dedicated scanner for this test
const scanner = new ContentScanner({
scanSubject: true,
scanBody: true,
scanAttachments: false,
customRules: []
});
const email = new Email({
from: 'security@bank-account-verify.com',
to: 'victim@example.com',
subject: 'URGENT: Verify your bank account details immediately',
text: 'Your account will be suspended. Please verify your details.',
html: '<p>Your account will be suspended. Please verify your details.</p>'
});
const result = await scanner.scanEmail(email);
console.log('Phishing email scan result:', result);
// We only care that it detected something suspicious
expect(result.threatScore >= 20).toEqual(true);
// Check if any threat was detected (specific type may vary)
expect(result.threatType).toBeTruthy();
});
// Test malware indicators in body
tap.test('ContentScanner - should detect malware indicators in body', async () => {
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'invoice@company.com',
to: 'recipient@example.com',
subject: 'Your invoice',
text: 'Please see the attached invoice. You need to enable macros to view this document properly.',
html: '<p>Please see the attached invoice. You need to enable macros to view this document properly.</p>'
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType === ThreatCategory.MALWARE || result.threatType).toBeTruthy();
expect(result.threatScore >= 30).toEqual(true);
});
// Test suspicious link detection
tap.test('ContentScanner - should detect suspicious links', async () => {
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'newsletter@example.com',
to: 'recipient@example.com',
subject: 'Weekly Newsletter',
text: 'Check our latest offer at https://bit.ly/2x3F5 and https://t.co/abc123',
html: '<p>Check our latest offer at <a href="https://bit.ly/2x3F5">here</a> and <a href="https://t.co/abc123">here</a></p>'
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType).toEqual(ThreatCategory.SUSPICIOUS_LINK);
expect(result.threatScore >= 30).toEqual(true);
});
// Test script injection detection
tap.test('ContentScanner - should detect script injection', async () => {
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'newsletter@example.com',
to: 'recipient@example.com',
subject: 'Newsletter',
text: 'Check our website',
html: '<p>Check our website</p><script>document.cookie="session="+localStorage.getItem("token");</script>'
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType).toEqual(ThreatCategory.XSS);
expect(result.threatScore >= 40).toEqual(true);
});
// Test executable attachment detection
tap.test('ContentScanner - should detect executable attachments', async () => {
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Software Update',
text: 'Please install the attached software update.',
attachments: [{
filename: 'update.exe',
content: Buffer.from('MZ...fake executable content...'),
contentType: 'application/octet-stream'
}]
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType).toEqual(ThreatCategory.EXECUTABLE);
expect(result.threatScore >= 70).toEqual(true);
});
// Test macro document detection
tap.test('ContentScanner - should detect macro documents', async () => {
// Create a mock Office document with macro indicators
const fakeDocContent = Buffer.from('Document content...vbaProject.bin...Auto_Open...DocumentOpen...Microsoft VBA...');
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Financial Report',
text: 'Please review the attached financial report.',
attachments: [{
filename: 'report.docm',
content: fakeDocContent,
contentType: 'application/vnd.ms-word.document.macroEnabled.12'
}]
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType).toEqual(ThreatCategory.MALICIOUS_MACRO);
expect(result.threatScore >= 60).toEqual(true);
});
// Test compound threat detection (multiple indicators)
tap.test('ContentScanner - should detect compound threats', async () => {
const scanner = ContentScanner.getInstance();
const email = new Email({
from: 'security@bank-verify.com',
to: 'victim@example.com',
subject: 'URGENT: Verify your account details immediately',
text: 'Your account will be suspended unless you verify your details at https://bit.ly/2x3F5',
html: '<p>Your account will be suspended unless you verify your details <a href="https://bit.ly/2x3F5">here</a>.</p>',
attachments: [{
filename: 'verification.exe',
content: Buffer.from('MZ...fake executable content...'),
contentType: 'application/octet-stream'
}]
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatScore > 70).toEqual(true); // Should have a high score due to multiple threats
});
// Test custom rules
tap.test('ContentScanner - should apply custom rules', async () => {
// Create a scanner with custom rules
const scanner = new ContentScanner({
customRules: [
{
pattern: /CUSTOM_PATTERN_FOR_TESTING/,
type: ThreatCategory.CUSTOM_RULE,
score: 50,
description: 'Custom pattern detected'
}
]
});
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test Custom Rule',
text: 'This message contains CUSTOM_PATTERN_FOR_TESTING that should be detected.'
});
const result = await scanner.scanEmail(email);
expect(result.isClean).toEqual(false);
expect(result.threatType).toEqual(ThreatCategory.CUSTOM_RULE);
expect(result.threatScore >= 50).toEqual(true);
});
// Test threat level classification
tap.test('ContentScanner - should classify threat levels correctly', async () => {
expect(ContentScanner.getThreatLevel(10)).toEqual('none');
expect(ContentScanner.getThreatLevel(25)).toEqual('low');
expect(ContentScanner.getThreatLevel(50)).toEqual('medium');
expect(ContentScanner.getThreatLevel(80)).toEqual('high');
});
export default tap.start();

View File

@ -0,0 +1,51 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
// Import the components we want to test
import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.js';
import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.js';
// Ensure test directories exist
paths.ensureDirectories();
// Test SenderReputationMonitor functionality
tap.test('SenderReputationMonitor should track sending events', async () => {
// Initialize monitor with test domain
const monitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['test-domain.com']
});
// Record some events
monitor.recordSendEvent('test-domain.com', { type: 'sent', count: 100 });
monitor.recordSendEvent('test-domain.com', { type: 'delivered', count: 95 });
// Get domain metrics
const metrics = monitor.getReputationData('test-domain.com');
// Verify metrics were recorded
if (metrics) {
expect(metrics.volume.sent).toEqual(100);
expect(metrics.volume.delivered).toEqual(95);
}
});
// Test IPWarmupManager functionality
tap.test('IPWarmupManager should handle IP allocation policies', async () => {
// Initialize warmup manager
const manager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2'],
targetDomains: ['test-domain.com']
});
// Set allocation policy
manager.setActiveAllocationPolicy('balanced');
// Verify allocation methods work
const canSend = manager.canSendMoreToday('192.168.1.1');
expect(typeof canSend).toEqual('boolean');
});
export default tap.start();

199
test/test.emailauth.ts Normal file
View File

@ -0,0 +1,199 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SzPlatformService } from '../ts/platformservice.js';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mta/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mta/classes.dmarcverifier.js';
import { Email } from '../ts/mta/classes.email.js';
/**
* Test email authentication systems: SPF and DMARC
*/
// Setup platform service for testing
let platformService: SzPlatformService;
tap.test('Setup test environment', async () => {
platformService = new SzPlatformService();
await platformService.init('test');
expect(platformService.mtaService).toBeTruthy();
});
// SPF Verifier Tests
tap.test('SPF Verifier - should parse SPF record', async () => {
const spfVerifier = new SpfVerifier(platformService.mtaService);
// Test valid SPF record parsing
const record = 'v=spf1 a mx ip4:192.168.0.1/24 include:example.org ~all';
const parsedRecord = spfVerifier.parseSpfRecord(record);
expect(parsedRecord).toBeTruthy();
expect(parsedRecord.version).toEqual('spf1');
expect(parsedRecord.mechanisms.length).toEqual(5);
// Check specific mechanisms
expect(parsedRecord.mechanisms[0].type).toEqual(SpfMechanismType.A);
expect(parsedRecord.mechanisms[0].qualifier).toEqual(SpfQualifier.PASS);
expect(parsedRecord.mechanisms[1].type).toEqual(SpfMechanismType.MX);
expect(parsedRecord.mechanisms[1].qualifier).toEqual(SpfQualifier.PASS);
expect(parsedRecord.mechanisms[2].type).toEqual(SpfMechanismType.IP4);
expect(parsedRecord.mechanisms[2].value).toEqual('192.168.0.1/24');
expect(parsedRecord.mechanisms[3].type).toEqual(SpfMechanismType.INCLUDE);
expect(parsedRecord.mechanisms[3].value).toEqual('example.org');
expect(parsedRecord.mechanisms[4].type).toEqual(SpfMechanismType.ALL);
expect(parsedRecord.mechanisms[4].qualifier).toEqual(SpfQualifier.SOFTFAIL);
// Test invalid record
const invalidRecord = 'not-a-spf-record';
const invalidParsed = spfVerifier.parseSpfRecord(invalidRecord);
expect(invalidParsed).toBeNull();
});
// DMARC Verifier Tests
tap.test('DMARC Verifier - should parse DMARC record', async () => {
const dmarcVerifier = new DmarcVerifier(platformService.mtaService);
// Test valid DMARC record parsing
const record = 'v=DMARC1; p=reject; sp=quarantine; pct=50; adkim=s; aspf=r; rua=mailto:dmarc@example.com';
const parsedRecord = dmarcVerifier.parseDmarcRecord(record);
expect(parsedRecord).toBeTruthy();
expect(parsedRecord.version).toEqual('DMARC1');
expect(parsedRecord.policy).toEqual(DmarcPolicy.REJECT);
expect(parsedRecord.subdomainPolicy).toEqual(DmarcPolicy.QUARANTINE);
expect(parsedRecord.pct).toEqual(50);
expect(parsedRecord.adkim).toEqual(DmarcAlignment.STRICT);
expect(parsedRecord.aspf).toEqual(DmarcAlignment.RELAXED);
expect(parsedRecord.reportUriAggregate).toContain('dmarc@example.com');
// Test invalid record
const invalidRecord = 'not-a-dmarc-record';
const invalidParsed = dmarcVerifier.parseDmarcRecord(invalidRecord);
expect(invalidParsed).toBeNull();
});
tap.test('DMARC Verifier - should verify DMARC alignment', async () => {
const dmarcVerifier = new DmarcVerifier(platformService.mtaService);
// Test email domains with DMARC alignment
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.net',
subject: 'Test DMARC alignment',
text: 'This is a test email'
});
// Test when both SPF and DKIM pass with alignment
const dmarcResult = await dmarcVerifier.verify(
email,
{ domain: 'example.com', result: true }, // SPF - aligned and passed
{ domain: 'example.com', result: true } // DKIM - aligned and passed
);
expect(dmarcResult).toBeTruthy();
expect(dmarcResult.spfPassed).toEqual(true);
expect(dmarcResult.dkimPassed).toEqual(true);
expect(dmarcResult.spfDomainAligned).toEqual(true);
expect(dmarcResult.dkimDomainAligned).toEqual(true);
expect(dmarcResult.action).toEqual('pass');
// Test when neither SPF nor DKIM is aligned
const dmarcResult2 = await dmarcVerifier.verify(
email,
{ domain: 'differentdomain.com', result: true }, // SPF - passed but not aligned
{ domain: 'anotherdomain.com', result: true } // DKIM - passed but not aligned
);
expect(dmarcResult2).toBeTruthy();
expect(dmarcResult2.spfPassed).toEqual(true);
expect(dmarcResult2.dkimPassed).toEqual(true);
expect(dmarcResult2.spfDomainAligned).toEqual(false);
expect(dmarcResult2.dkimDomainAligned).toEqual(false);
// Since there's no DMARC record in test environment, we expect "none" policy
expect(dmarcResult2.policyEvaluated).toEqual(DmarcPolicy.NONE);
});
tap.test('DMARC Verifier - should apply policy correctly', async () => {
const dmarcVerifier = new DmarcVerifier(platformService.mtaService);
// Create test email
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.net',
subject: 'Test DMARC policy application',
text: 'This is a test email'
});
// Test pass action
const passResult = {
hasDmarc: true,
spfDomainAligned: true,
dkimDomainAligned: true,
spfPassed: true,
dkimPassed: true,
policyEvaluated: DmarcPolicy.NONE,
actualPolicy: DmarcPolicy.NONE,
appliedPercentage: 100,
action: 'pass',
details: 'DMARC passed'
};
const passApplied = dmarcVerifier.applyPolicy(email, passResult);
expect(passApplied).toEqual(true);
expect(email.mightBeSpam).toEqual(false);
expect(email.headers['X-DMARC-Result']).toEqual('DMARC passed');
// Test quarantine action
const quarantineResult = {
hasDmarc: true,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: false,
dkimPassed: false,
policyEvaluated: DmarcPolicy.QUARANTINE,
actualPolicy: DmarcPolicy.QUARANTINE,
appliedPercentage: 100,
action: 'quarantine',
details: 'DMARC failed, policy=quarantine'
};
// Reset email spam flag
email.mightBeSpam = false;
email.headers = {};
const quarantineApplied = dmarcVerifier.applyPolicy(email, quarantineResult);
expect(quarantineApplied).toEqual(true);
expect(email.mightBeSpam).toEqual(true);
expect(email.headers['X-Spam-Flag']).toEqual('YES');
expect(email.headers['X-DMARC-Result']).toEqual('DMARC failed, policy=quarantine');
// Test reject action
const rejectResult = {
hasDmarc: true,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: false,
dkimPassed: false,
policyEvaluated: DmarcPolicy.REJECT,
actualPolicy: DmarcPolicy.REJECT,
appliedPercentage: 100,
action: 'reject',
details: 'DMARC failed, policy=reject'
};
// Reset email spam flag
email.mightBeSpam = false;
email.headers = {};
const rejectApplied = dmarcVerifier.applyPolicy(email, rejectResult);
expect(rejectApplied).toEqual(false);
expect(email.mightBeSpam).toEqual(true);
});
tap.test('Cleanup test environment', async () => {
await platformService.stop();
});
export default tap.start();

View File

@ -0,0 +1,175 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { IPReputationChecker, ReputationThreshold, IPType } from '../ts/security/classes.ipreputationchecker.js';
import * as plugins from '../ts/plugins.js';
// Mock for dns lookup
const originalDnsResolve = plugins.dns.promises.resolve;
let mockDnsResolveImpl: (hostname: string) => Promise<string[]> = async () => ['127.0.0.1'];
// Setup mock DNS resolver
plugins.dns.promises.resolve = async (hostname: string) => {
return mockDnsResolveImpl(hostname);
};
// Test instantiation
tap.test('IPReputationChecker - should be instantiable', async () => {
const checker = IPReputationChecker.getInstance({
enableDNSBL: false,
enableIPInfo: false,
enableLocalCache: false
});
expect(checker).toBeTruthy();
});
// Test singleton pattern
tap.test('IPReputationChecker - should use singleton pattern', async () => {
const checker1 = IPReputationChecker.getInstance();
const checker2 = IPReputationChecker.getInstance();
// Both instances should be the same object
expect(checker1 === checker2).toEqual(true);
});
// Test IP validation
tap.test('IPReputationChecker - should validate IP address format', async () => {
const checker = IPReputationChecker.getInstance({
enableDNSBL: false,
enableIPInfo: false,
enableLocalCache: false
});
// Valid IP should work
const result = await checker.checkReputation('192.168.1.1');
expect(result.score).toBeGreaterThan(0);
expect(result.error).toBeUndefined();
// Invalid IP should fail with error
const invalidResult = await checker.checkReputation('invalid.ip');
expect(invalidResult.error).toBeTruthy();
});
// Test DNSBL lookups
tap.test('IPReputationChecker - should check IP against DNSBL', async () => {
try {
// Setup mock implementation for DNSBL
mockDnsResolveImpl = async (hostname: string) => {
// Listed in DNSBL if IP contains 2
if (hostname.includes('2.1.168.192') && hostname.includes('zen.spamhaus.org')) {
return ['127.0.0.2'];
}
throw { code: 'ENOTFOUND' };
};
// Create a new instance with specific settings for this test
const testInstance = new IPReputationChecker({
dnsblServers: ['zen.spamhaus.org'],
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 1 // Small cache for testing
});
// Clean IP should have good score
const cleanResult = await testInstance.checkReputation('192.168.1.1');
expect(cleanResult.isSpam).toEqual(false);
expect(cleanResult.score).toEqual(100);
// Blacklisted IP should have reduced score
const blacklistedResult = await testInstance.checkReputation('192.168.1.2');
expect(blacklistedResult.isSpam).toEqual(true);
expect(blacklistedResult.score < 100).toEqual(true); // Less than 100
expect(blacklistedResult.blacklists).toBeTruthy();
expect((blacklistedResult.blacklists || []).length > 0).toEqual(true);
} catch (err) {
console.error('Test error:', err);
throw err;
}
});
// Test caching behavior
tap.test('IPReputationChecker - should cache reputation results', async () => {
// Create a fresh instance for this test
const testInstance = new IPReputationChecker({
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 10 // Small cache for testing
});
// Check that first look performs a lookup and second uses cache
const ip = '192.168.1.10';
// First check should add to cache
const result1 = await testInstance.checkReputation(ip);
expect(result1).toBeTruthy();
// Manually verify it's in cache - access private member for testing
const hasInCache = (testInstance as any).reputationCache.has(ip);
expect(hasInCache).toEqual(true);
// Call again, should use cache
const result2 = await testInstance.checkReputation(ip);
expect(result2).toBeTruthy();
// Results should be identical
expect(result1.score).toEqual(result2.score);
});
// Test risk level classification
tap.test('IPReputationChecker - should classify risk levels correctly', async () => {
expect(IPReputationChecker.getRiskLevel(10)).toEqual('high');
expect(IPReputationChecker.getRiskLevel(30)).toEqual('medium');
expect(IPReputationChecker.getRiskLevel(60)).toEqual('low');
expect(IPReputationChecker.getRiskLevel(90)).toEqual('trusted');
});
// Test IP type detection
tap.test('IPReputationChecker - should detect special IP types', async () => {
const testInstance = new IPReputationChecker({
enableDNSBL: false,
enableIPInfo: true,
enableLocalCache: false,
maxCacheSize: 5 // Small cache for testing
});
// Test Tor exit node detection
const torResult = await testInstance.checkReputation('171.25.1.1');
expect(torResult.isTor).toEqual(true);
expect(torResult.score < 90).toEqual(true);
// Test VPN detection
const vpnResult = await testInstance.checkReputation('185.156.1.1');
expect(vpnResult.isVPN).toEqual(true);
expect(vpnResult.score < 90).toEqual(true);
// Test proxy detection
const proxyResult = await testInstance.checkReputation('34.92.1.1');
expect(proxyResult.isProxy).toEqual(true);
expect(proxyResult.score < 90).toEqual(true);
});
// Test error handling
tap.test('IPReputationChecker - should handle DNS lookup errors gracefully', async () => {
// Setup mock implementation to simulate error
mockDnsResolveImpl = async () => {
throw new Error('DNS server error');
};
const checker = IPReputationChecker.getInstance({
dnsblServers: ['zen.spamhaus.org'],
enableIPInfo: false,
enableLocalCache: false,
maxCacheSize: 300 // Force new instance
});
// Should return a result despite errors
const result = await checker.checkReputation('192.168.1.1');
expect(result.score).toEqual(100); // No blacklist hits found due to error
expect(result.isSpam).toEqual(false);
});
// Restore original implementation at the end
tap.test('Cleanup - restore mocks', async () => {
plugins.dns.promises.resolve = originalDnsResolve;
});
export default tap.start();

View File

@ -0,0 +1,300 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.js';
// Cleanup any temporary test data
const cleanupTestData = () => {
const warmupDataPath = plugins.path.join(paths.dataDir, 'warmup');
if (plugins.fs.existsSync(warmupDataPath)) {
plugins.smartfile.memory.unlinkDir(warmupDataPath);
}
};
// Helper to reset the singleton instance between tests
const resetSingleton = () => {
// @ts-ignore - accessing private static field for testing
IPWarmupManager._instance = null;
};
// Before running any tests
tap.test('setup', async () => {
cleanupTestData();
});
// Test initialization of IPWarmupManager
tap.test('should initialize IPWarmupManager with default settings', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance();
expect(ipWarmupManager).to.be.an('object');
expect(ipWarmupManager.getBestIPForSending).to.be.a('function');
expect(ipWarmupManager.canSendMoreToday).to.be.a('function');
expect(ipWarmupManager.getStageCount).to.be.a('function');
expect(ipWarmupManager.setActiveAllocationPolicy).to.be.a('function');
});
// Test initialization with custom settings
tap.test('should initialize IPWarmupManager with custom settings', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2'],
targetDomains: ['example.com', 'test.com'],
fallbackPercentage: 75
});
// Test setting allocation policy
ipWarmupManager.setActiveAllocationPolicy('roundRobin');
// Get best IP for sending
const bestIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
// Check if we can send more today
const canSendMore = ipWarmupManager.canSendMoreToday('192.168.1.1');
// Check stage count
const stageCount = ipWarmupManager.getStageCount();
expect(stageCount).to.be.a('number');
});
// Test IP allocation policies
tap.test('should allocate IPs using balanced policy', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'],
targetDomains: ['example.com', 'test.com'],
allocationPolicy: 'balanced'
});
ipWarmupManager.setActiveAllocationPolicy('balanced');
// Use getBestIPForSending multiple times and check if all IPs are used
const usedIPs = new Set();
for (let i = 0; i < 30; i++) {
const ip = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
if (ip) usedIPs.add(ip);
}
// We should use at least 2 different IPs with balanced policy
expect(usedIPs.size).to.be.at.least(2);
});
// Test round robin allocation policy
tap.test('should allocate IPs using round robin policy', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'],
targetDomains: ['example.com', 'test.com'],
allocationPolicy: 'roundRobin'
});
ipWarmupManager.setActiveAllocationPolicy('roundRobin');
// First few IPs should rotate through the available IPs
const firstIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
const secondIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
const thirdIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
// Round robin should give us different IPs for consecutive calls
expect(firstIP).to.not.equal(secondIP);
// Fourth call should cycle back to first IP
const fourthIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
expect(fourthIP).to.equal(firstIP);
});
// Test dedicated domain allocation policy
tap.test('should allocate IPs using dedicated domain policy', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'],
targetDomains: ['example.com', 'test.com', 'other.com'],
allocationPolicy: 'dedicatedDomain'
});
ipWarmupManager.setActiveAllocationPolicy('dedicatedDomain');
// Map domains to IPs
ipWarmupManager.mapDomainToIP('example.com', '192.168.1.1');
ipWarmupManager.mapDomainToIP('test.com', '192.168.1.2');
ipWarmupManager.mapDomainToIP('other.com', '192.168.1.3');
// Each domain should get its dedicated IP
const exampleIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@gmail.com'],
domain: 'example.com'
});
const testIP = ipWarmupManager.getBestIPForSending({
from: 'test@test.com',
to: ['recipient@gmail.com'],
domain: 'test.com'
});
const otherIP = ipWarmupManager.getBestIPForSending({
from: 'test@other.com',
to: ['recipient@gmail.com'],
domain: 'other.com'
});
expect(exampleIP).to.equal('192.168.1.1');
expect(testIP).to.equal('192.168.1.2');
expect(otherIP).to.equal('192.168.1.3');
});
// Test daily sending limits
tap.test('should enforce daily sending limits', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1'],
targetDomains: ['example.com'],
allocationPolicy: 'balanced'
});
// Override the warmup stage for testing
// @ts-ignore - accessing private method for testing
ipWarmupManager.warmupStatus.set('192.168.1.1', {
isActive: true,
currentStage: 0,
startDate: new Date(),
dailySendCount: 0,
hourlySendCount: {}
});
// Set a very low daily limit for testing
// @ts-ignore - accessing private method for testing
ipWarmupManager.warmupStages = [
{ dailyLimit: 5, duration: 5, hourlyPercentage: { min: 0, max: 40 } }
];
// First 5 sends should succeed
for (let i = 0; i < 5; i++) {
const ip = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
expect(ip).to.equal('192.168.1.1');
ipWarmupManager.recordSend(ip);
}
// 6th send should not get an IP due to daily limit
const sixthIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
expect(sixthIP).to.be.null;
});
// Test recording sends
tap.test('should record send events correctly', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2'],
targetDomains: ['example.com'],
});
// Set allocation policy
ipWarmupManager.setActiveAllocationPolicy('balanced');
// Get an IP for sending
const ip = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
// If we got an IP, record some sends
if (ip) {
// Record a few sends
for (let i = 0; i < 5; i++) {
ipWarmupManager.recordSend(ip);
}
// Check if we can still send more
const canSendMore = ipWarmupManager.canSendMoreToday(ip);
expect(canSendMore).to.be.a('boolean');
}
});
// Test that DedicatedDomainPolicy assigns IPs correctly
tap.test('should assign IPs using dedicated domain policy', async () => {
resetSingleton();
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'],
targetDomains: ['example.com', 'test.com', 'other.com']
});
// Set allocation policy to dedicated domains
ipWarmupManager.setActiveAllocationPolicy('dedicated');
// Check allocation by querying for different domains
const ip1 = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
const ip2 = ipWarmupManager.getBestIPForSending({
from: 'test@test.com',
to: ['recipient@test.com'],
domain: 'test.com'
});
// If we got IPs, they should be consistently assigned
if (ip1 && ip2) {
// Requesting the same domain again should return the same IP
const ip1again = ipWarmupManager.getBestIPForSending({
from: 'another@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
expect(ip1again).to.equal(ip1);
}
});
// After all tests, clean up
tap.test('cleanup', async () => {
cleanupTestData();
});
export default tap.start();

62
test/test.minimal.ts Normal file
View File

@ -0,0 +1,62 @@
import { tap } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.js';
import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.js';
/**
* Basic test to check if our integrated classes work correctly
*/
tap.test('verify that SenderReputationMonitor and IPWarmupManager are functioning', async (tools) => {
// Create instances of both classes
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
const ipWarmupManager = IPWarmupManager.getInstance({
enabled: true,
ipAddresses: ['192.168.1.1', '192.168.1.2'],
targetDomains: ['example.com']
});
// Test SenderReputationMonitor
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 });
const reputationData = reputationMonitor.getReputationData('example.com');
const summary = reputationMonitor.getReputationSummary();
// Basic checks
tools.ok(reputationData, 'Got reputation data');
tools.ok(summary.length > 0, 'Got reputation summary');
// Add and remove domains
reputationMonitor.addDomain('test.com');
reputationMonitor.removeDomain('test.com');
// Test IPWarmupManager
ipWarmupManager.setActiveAllocationPolicy('balanced');
const bestIP = ipWarmupManager.getBestIPForSending({
from: 'test@example.com',
to: ['recipient@test.com'],
domain: 'example.com'
});
if (bestIP) {
ipWarmupManager.recordSend(bestIP);
const canSendMore = ipWarmupManager.canSendMoreToday(bestIP);
tools.ok(canSendMore !== undefined, 'Can check if sending more is allowed');
}
const stageCount = ipWarmupManager.getStageCount();
tools.ok(stageCount > 0, 'Got stage count');
});
// Final clean-up test
tap.test('clean up after tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
export default tap.start();

137
test/test.ratelimiter.ts Normal file
View File

@ -0,0 +1,137 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { RateLimiter } from '../ts/mta/classes.ratelimiter.js';
tap.test('RateLimiter - should be instantiable', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 10,
periodMs: 1000,
perKey: true
});
expect(limiter).toBeTruthy();
});
tap.test('RateLimiter - should allow requests within rate limit', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 5,
periodMs: 1000,
perKey: true
});
// Should allow 5 requests
for (let i = 0; i < 5; i++) {
expect(limiter.isAllowed('test')).toEqual(true);
}
// 6th request should be denied
expect(limiter.isAllowed('test')).toEqual(false);
});
tap.test('RateLimiter - should enforce per-key limits', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 3,
periodMs: 1000,
perKey: true
});
// Should allow 3 requests for key1
for (let i = 0; i < 3; i++) {
expect(limiter.isAllowed('key1')).toEqual(true);
}
// 4th request for key1 should be denied
expect(limiter.isAllowed('key1')).toEqual(false);
// But key2 should still be allowed
expect(limiter.isAllowed('key2')).toEqual(true);
});
tap.test('RateLimiter - should refill tokens over time', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 2,
periodMs: 100, // Short period for testing
perKey: true
});
// Use all tokens
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(false);
// Wait for refill
await new Promise(resolve => setTimeout(resolve, 150));
// Should have tokens again
expect(limiter.isAllowed('test')).toEqual(true);
});
tap.test('RateLimiter - should support burst allowance', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 2,
periodMs: 100,
perKey: true,
burstTokens: 2, // Allow 2 extra tokens for bursts
initialTokens: 4 // Start with max + burst tokens
});
// Should allow 4 requests (2 regular + 2 burst)
for (let i = 0; i < 4; i++) {
expect(limiter.isAllowed('test')).toEqual(true);
}
// 5th request should be denied
expect(limiter.isAllowed('test')).toEqual(false);
// Wait for refill
await new Promise(resolve => setTimeout(resolve, 150));
// Should have 2 tokens again (rate-limited to normal max, not burst)
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
// 3rd request after refill should fail (only normal max is refilled, not burst)
expect(limiter.isAllowed('test')).toEqual(false);
});
tap.test('RateLimiter - should return correct stats', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 10,
periodMs: 1000,
perKey: true
});
// Make some requests
limiter.isAllowed('test');
limiter.isAllowed('test');
limiter.isAllowed('test');
// Get stats
const stats = limiter.getStats('test');
expect(stats.remaining).toEqual(7);
expect(stats.limit).toEqual(10);
expect(stats.allowed).toEqual(3);
expect(stats.denied).toEqual(0);
});
tap.test('RateLimiter - should reset limits', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 3,
periodMs: 1000,
perKey: true
});
// Use all tokens
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(false);
// Reset
limiter.reset('test');
// Should have tokens again
expect(limiter.isAllowed('test')).toEqual(true);
});
export default tap.start();

View File

@ -0,0 +1,238 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.js';
// Cleanup any temporary test data
const cleanupTestData = () => {
const reputationDataPath = plugins.path.join(paths.dataDir, 'reputation');
if (plugins.fs.existsSync(reputationDataPath)) {
plugins.smartfile.memory.unlinkDir(reputationDataPath);
}
};
// Helper to reset the singleton instance between tests
const resetSingleton = () => {
// @ts-ignore - accessing private static field for testing
SenderReputationMonitor._instance = null;
};
// Before running any tests
tap.test('setup', async () => {
cleanupTestData();
});
// Test initialization of SenderReputationMonitor
tap.test('should initialize SenderReputationMonitor with default settings', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance();
expect(reputationMonitor).to.be.an('object');
// Check if the object has the expected methods
expect(reputationMonitor.recordSendEvent).to.be.a('function');
expect(reputationMonitor.getReputationData).to.be.a('function');
expect(reputationMonitor.getReputationSummary).to.be.a('function');
});
// Test initialization with custom settings
tap.test('should initialize SenderReputationMonitor with custom settings', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com', 'test.com'],
updateFrequency: 12 * 60 * 60 * 1000, // 12 hours
alertThresholds: {
minReputationScore: 80,
maxComplaintRate: 0.05
}
});
// Test adding domains
reputationMonitor.addDomain('newdomain.com');
// Test retrieving reputation data
const data = reputationMonitor.getReputationData('example.com');
expect(data).to.be.an('object');
expect(data.domain).to.equal('example.com');
});
// Test recording and tracking send events
tap.test('should record send events and update metrics', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
// Record a series of events
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 });
reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: true, count: 3 });
reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: false, count: 2 });
reputationMonitor.recordSendEvent('example.com', { type: 'complaint', count: 1 });
// Check metrics
const metrics = reputationMonitor.getReputationData('example.com');
expect(metrics).to.be.an('object');
expect(metrics.volume.sent).to.equal(100);
expect(metrics.volume.delivered).to.equal(95);
expect(metrics.volume.hardBounces).to.equal(3);
expect(metrics.volume.softBounces).to.equal(2);
expect(metrics.complaints.total).to.equal(1);
});
// Test reputation score calculation
tap.test('should calculate reputation scores correctly', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['high.com', 'medium.com', 'low.com']
});
// Record events for different domains
reputationMonitor.recordSendEvent('high.com', { type: 'sent', count: 1000 });
reputationMonitor.recordSendEvent('high.com', { type: 'delivered', count: 990 });
reputationMonitor.recordSendEvent('high.com', { type: 'open', count: 500 });
reputationMonitor.recordSendEvent('medium.com', { type: 'sent', count: 1000 });
reputationMonitor.recordSendEvent('medium.com', { type: 'delivered', count: 950 });
reputationMonitor.recordSendEvent('medium.com', { type: 'open', count: 300 });
reputationMonitor.recordSendEvent('low.com', { type: 'sent', count: 1000 });
reputationMonitor.recordSendEvent('low.com', { type: 'delivered', count: 850 });
reputationMonitor.recordSendEvent('low.com', { type: 'open', count: 100 });
// Get reputation summary
const summary = reputationMonitor.getReputationSummary();
expect(summary).to.be.an('array');
expect(summary.length).to.be.at.least(3);
// Check that domains are included in the summary
const domains = summary.map(item => item.domain);
expect(domains).to.include('high.com');
expect(domains).to.include('medium.com');
expect(domains).to.include('low.com');
});
// Test adding and removing domains
tap.test('should add and remove domains for monitoring', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
// Add a new domain
reputationMonitor.addDomain('newdomain.com');
// Record data for the new domain
reputationMonitor.recordSendEvent('newdomain.com', { type: 'sent', count: 50 });
// Check that data was recorded for the new domain
const metrics = reputationMonitor.getReputationData('newdomain.com');
expect(metrics).to.be.an('object');
expect(metrics.volume.sent).to.equal(50);
// Remove a domain
reputationMonitor.removeDomain('newdomain.com');
// Check that data is no longer available
const removedMetrics = reputationMonitor.getReputationData('newdomain.com');
expect(removedMetrics).to.be.null;
});
// Test handling open and click events
tap.test('should track engagement metrics correctly', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
// Record basic sending metrics
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 1000 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 950 });
// Record engagement events
reputationMonitor.recordSendEvent('example.com', { type: 'open', count: 500 });
reputationMonitor.recordSendEvent('example.com', { type: 'click', count: 250 });
// Check engagement metrics
const metrics = reputationMonitor.getReputationData('example.com');
expect(metrics).to.be.an('object');
expect(metrics.engagement.opens).to.equal(500);
expect(metrics.engagement.clicks).to.equal(250);
expect(metrics.engagement.openRate).to.be.a('number');
expect(metrics.engagement.clickRate).to.be.a('number');
});
// Test historical data tracking
tap.test('should store historical reputation data', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
// Record events over multiple days
const today = new Date();
// Record data
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 1000 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 950 });
// Get metrics data
const metrics = reputationMonitor.getReputationData('example.com');
// Check that historical data exists
expect(metrics.historical).to.be.an('object');
expect(metrics.historical.reputationScores).to.be.an('object');
// Check that daily send volume is tracked
expect(metrics.volume.dailySendVolume).to.be.an('object');
const todayStr = today.toISOString().split('T')[0];
expect(metrics.volume.dailySendVolume[todayStr]).to.equal(1000);
});
// Test event recording for different event types
tap.test('should correctly handle different event types', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
domains: ['example.com']
});
// Record different types of events
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 });
reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 });
reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: true, count: 3 });
reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: false, count: 2 });
reputationMonitor.recordSendEvent('example.com', { type: 'complaint', receivingDomain: 'gmail.com', count: 1 });
reputationMonitor.recordSendEvent('example.com', { type: 'open', count: 50 });
reputationMonitor.recordSendEvent('example.com', { type: 'click', count: 25 });
// Check metrics for different event types
const metrics = reputationMonitor.getReputationData('example.com');
// Check volume metrics
expect(metrics.volume.sent).to.equal(100);
expect(metrics.volume.delivered).to.equal(95);
expect(metrics.volume.hardBounces).to.equal(3);
expect(metrics.volume.softBounces).to.equal(2);
// Check complaint metrics
expect(metrics.complaints.total).to.equal(1);
expect(metrics.complaints.topDomains[0].domain).to.equal('gmail.com');
// Check engagement metrics
expect(metrics.engagement.opens).to.equal(50);
expect(metrics.engagement.clicks).to.equal(25);
});
// After all tests, clean up
tap.test('cleanup', async () => {
cleanupTestData();
});
export default tap.start();