feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
		@@ -0,0 +1,335 @@
 | 
			
		||||
import * as plugins from '@git.zone/tstest/tapbundle';
 | 
			
		||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
 | 
			
		||||
import * as net from 'net';
 | 
			
		||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.ts';
 | 
			
		||||
 | 
			
		||||
const TEST_PORT = 2525;
 | 
			
		||||
 | 
			
		||||
let testServer;
 | 
			
		||||
 | 
			
		||||
interface DnsTestResult {
 | 
			
		||||
  scenario: string;
 | 
			
		||||
  domain: string;
 | 
			
		||||
  expectedBehavior: string;
 | 
			
		||||
  mailFromSuccess: boolean;
 | 
			
		||||
  rcptToSuccess: boolean;
 | 
			
		||||
  mailFromResponse: string;
 | 
			
		||||
  rcptToResponse: string;
 | 
			
		||||
  handledGracefully: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to wait for SMTP response
 | 
			
		||||
const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise<string> => {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let buffer = '';
 | 
			
		||||
    const timer = setTimeout(() => {
 | 
			
		||||
      socket.removeListener('data', handler);
 | 
			
		||||
      reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`));
 | 
			
		||||
    }, timeout);
 | 
			
		||||
    
 | 
			
		||||
    const handler = (data: Buffer) => {
 | 
			
		||||
      buffer += data.toString();
 | 
			
		||||
      const lines = buffer.split('\r\n');
 | 
			
		||||
      
 | 
			
		||||
      // Check if we have a complete response
 | 
			
		||||
      for (const line of lines) {
 | 
			
		||||
        if (expectedCode) {
 | 
			
		||||
          if (line.startsWith(expectedCode + ' ')) {
 | 
			
		||||
            clearTimeout(timer);
 | 
			
		||||
            socket.removeListener('data', handler);
 | 
			
		||||
            resolve(buffer);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          // Any complete response line
 | 
			
		||||
          if (line.match(/^\d{3} /)) {
 | 
			
		||||
            clearTimeout(timer);
 | 
			
		||||
            socket.removeListener('data', handler);
 | 
			
		||||
            resolve(buffer);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    socket.on('data', handler);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
tap.test('prepare server', async () => {
 | 
			
		||||
  testServer = await startTestServer({ port: TEST_PORT });
 | 
			
		||||
  await new Promise(resolve => setTimeout(resolve, 100));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('REL-05: DNS resolution failure handling - Non-existent domains', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: 30000
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', resolve);
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Read greeting
 | 
			
		||||
    await waitForResponse(socket, '220');
 | 
			
		||||
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO dns-test\r\n');
 | 
			
		||||
    await waitForResponse(socket, '250');
 | 
			
		||||
 | 
			
		||||
    console.log('Testing DNS resolution for non-existent domains...');
 | 
			
		||||
 | 
			
		||||
    // Test 1: Non-existent domain in MAIL FROM
 | 
			
		||||
    socket.write('MAIL FROM:<sender@non-existent-domain-12345.invalid>\r\n');
 | 
			
		||||
    const mailResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  MAIL FROM response:', mailResponse.trim());
 | 
			
		||||
    
 | 
			
		||||
    // Server should either accept (and defer later) or reject immediately
 | 
			
		||||
    const mailFromHandled = mailResponse.includes('250') || 
 | 
			
		||||
                           mailResponse.includes('450') || 
 | 
			
		||||
                           mailResponse.includes('550');
 | 
			
		||||
    expect(mailFromHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    // Reset if needed
 | 
			
		||||
    if (mailResponse.includes('250')) {
 | 
			
		||||
      socket.write('RSET\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test 2: Non-existent domain in RCPT TO
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailFromResp = await waitForResponse(socket, '250');
 | 
			
		||||
    expect(mailFromResp).toInclude('250');
 | 
			
		||||
 | 
			
		||||
    socket.write('RCPT TO:<recipient@non-existent-domain-xyz.invalid>\r\n');
 | 
			
		||||
    const rcptResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  RCPT TO response:', rcptResponse.trim());
 | 
			
		||||
    
 | 
			
		||||
    // Server may accept (and defer validation) or reject immediately
 | 
			
		||||
    const rcptToHandled = rcptResponse.includes('250') || // Accepted (for later validation)
 | 
			
		||||
                         rcptResponse.includes('450') || // Temporary failure
 | 
			
		||||
                         rcptResponse.includes('550') || // Permanent failure
 | 
			
		||||
                         rcptResponse.includes('553');   // Address error
 | 
			
		||||
    expect(rcptToHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('REL-05: DNS resolution failure handling - Malformed domains', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: 30000
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', resolve);
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Read greeting
 | 
			
		||||
    await waitForResponse(socket, '220');
 | 
			
		||||
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO malformed-test\r\n');
 | 
			
		||||
    await waitForResponse(socket, '250');
 | 
			
		||||
 | 
			
		||||
    console.log('\nTesting malformed domain handling...');
 | 
			
		||||
 | 
			
		||||
    const malformedDomains = [
 | 
			
		||||
      'malformed..domain..test',
 | 
			
		||||
      'invalid-.domain.com',
 | 
			
		||||
      'domain.with.spaces .com',
 | 
			
		||||
      '.leading-dot.com',
 | 
			
		||||
      'trailing-dot.com.',
 | 
			
		||||
      'domain@with@at.com',
 | 
			
		||||
      'a'.repeat(255) + '.toolong.com' // Domain too long
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    for (const domain of malformedDomains) {
 | 
			
		||||
      console.log(`  Testing: ${domain.substring(0, 50)}${domain.length > 50 ? '...' : ''}`);
 | 
			
		||||
 | 
			
		||||
      socket.write(`MAIL FROM:<test@${domain}>\r\n`);
 | 
			
		||||
      const response = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
      // Server should reject malformed domains or accept for later validation
 | 
			
		||||
      const properlyHandled = response.includes('250') || // Accepted (may validate later)
 | 
			
		||||
                             response.includes('501') || // Syntax error
 | 
			
		||||
                             response.includes('550') || // Rejected
 | 
			
		||||
                             response.includes('553');   // Address error
 | 
			
		||||
      
 | 
			
		||||
      console.log(`    Response: ${response.trim().substring(0, 50)}`);
 | 
			
		||||
      expect(properlyHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
      // Reset if needed
 | 
			
		||||
      if (!response.includes('5')) {
 | 
			
		||||
        socket.write('RSET\r\n');
 | 
			
		||||
        await waitForResponse(socket, '250');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('REL-05: DNS resolution failure handling - Special cases', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: 30000
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', resolve);
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Read greeting
 | 
			
		||||
    await waitForResponse(socket, '220');
 | 
			
		||||
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO special-test\r\n');
 | 
			
		||||
    await waitForResponse(socket, '250');
 | 
			
		||||
 | 
			
		||||
    console.log('\nTesting special DNS cases...');
 | 
			
		||||
 | 
			
		||||
    // Test 1: Localhost (may be accepted or rejected)
 | 
			
		||||
    socket.write('MAIL FROM:<sender@localhost>\r\n');
 | 
			
		||||
    const localhostResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  Localhost response:', localhostResponse.trim());
 | 
			
		||||
    const localhostHandled = localhostResponse.includes('250') || localhostResponse.includes('501');
 | 
			
		||||
    expect(localhostHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    // Only reset if transaction was started
 | 
			
		||||
    if (localhostResponse.includes('250')) {
 | 
			
		||||
      socket.write('RSET\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test 2: IP address (should work)
 | 
			
		||||
    socket.write('MAIL FROM:<sender@[127.0.0.1]>\r\n');
 | 
			
		||||
    const ipResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  IP address response:', ipResponse.trim());
 | 
			
		||||
    const ipHandled = ipResponse.includes('250') || ipResponse.includes('501');
 | 
			
		||||
    expect(ipHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    // Only reset if transaction was started
 | 
			
		||||
    if (ipResponse.includes('250')) {
 | 
			
		||||
      socket.write('RSET\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Test 3: Empty domain
 | 
			
		||||
    socket.write('MAIL FROM:<sender@>\r\n');
 | 
			
		||||
    const emptyResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  Empty domain response:', emptyResponse.trim());
 | 
			
		||||
    expect(emptyResponse).toMatch(/50[1-3]/); // Should reject
 | 
			
		||||
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('REL-05: DNS resolution failure handling - Mixed valid/invalid recipients', async (tools) => {
 | 
			
		||||
  const done = tools.defer();
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    const socket = net.createConnection({
 | 
			
		||||
      host: 'localhost',
 | 
			
		||||
      port: TEST_PORT,
 | 
			
		||||
      timeout: 30000
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await new Promise<void>((resolve, reject) => {
 | 
			
		||||
      socket.once('connect', resolve);
 | 
			
		||||
      socket.once('error', reject);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Read greeting
 | 
			
		||||
    await waitForResponse(socket, '220');
 | 
			
		||||
 | 
			
		||||
    // Send EHLO
 | 
			
		||||
    socket.write('EHLO mixed-test\r\n');
 | 
			
		||||
    await waitForResponse(socket, '250');
 | 
			
		||||
 | 
			
		||||
    console.log('\nTesting mixed valid/invalid recipients...');
 | 
			
		||||
 | 
			
		||||
    // Start transaction
 | 
			
		||||
    socket.write('MAIL FROM:<sender@example.com>\r\n');
 | 
			
		||||
    const mailFromResp = await waitForResponse(socket, '250');
 | 
			
		||||
    expect(mailFromResp).toInclude('250');
 | 
			
		||||
 | 
			
		||||
    // Add valid recipient
 | 
			
		||||
    socket.write('RCPT TO:<valid@example.com>\r\n');
 | 
			
		||||
    const validRcptResponse = await waitForResponse(socket, '250');
 | 
			
		||||
 | 
			
		||||
    console.log('  Valid recipient:', validRcptResponse.trim());
 | 
			
		||||
    expect(validRcptResponse).toInclude('250');
 | 
			
		||||
 | 
			
		||||
    // Add invalid recipient
 | 
			
		||||
    socket.write('RCPT TO:<invalid@non-existent-domain-abc.invalid>\r\n');
 | 
			
		||||
    const invalidRcptResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    console.log('  Invalid recipient:', invalidRcptResponse.trim());
 | 
			
		||||
    
 | 
			
		||||
    // Server may accept (for later validation) or reject invalid domain
 | 
			
		||||
    const invalidHandled = invalidRcptResponse.includes('250') || // Accepted (for later validation)
 | 
			
		||||
                          invalidRcptResponse.includes('450') || 
 | 
			
		||||
                          invalidRcptResponse.includes('550') ||
 | 
			
		||||
                          invalidRcptResponse.includes('553');
 | 
			
		||||
    expect(invalidHandled).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    // Try to send data (should work if at least one valid recipient)
 | 
			
		||||
    socket.write('DATA\r\n');
 | 
			
		||||
    const dataResponse = await waitForResponse(socket);
 | 
			
		||||
 | 
			
		||||
    if (dataResponse.includes('354')) {
 | 
			
		||||
      socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n');
 | 
			
		||||
      await waitForResponse(socket, '250');
 | 
			
		||||
      console.log('  Message accepted with valid recipient');
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('  Server rejected DATA (acceptable behavior)');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.write('QUIT\r\n');
 | 
			
		||||
    socket.end();
 | 
			
		||||
    done.resolve();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    done.reject(error);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup server', async () => {
 | 
			
		||||
  await stopTestServer(testServer);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default tap.start();
 | 
			
		||||
		Reference in New Issue
	
	Block a user