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,239 @@
 | 
			
		||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
 | 
			
		||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
 | 
			
		||||
import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection, generateRandomEmail } from '../../helpers/utils.ts';
 | 
			
		||||
 | 
			
		||||
let testServer: ITestServer;
 | 
			
		||||
 | 
			
		||||
tap.test('setup - start SMTP server with large size limit', async () => {
 | 
			
		||||
  testServer = await startTestServer({
 | 
			
		||||
    port: 2532,
 | 
			
		||||
    hostname: 'localhost',
 | 
			
		||||
    size: 100 * 1024 * 1024 // 100MB limit for testing
 | 
			
		||||
  });
 | 
			
		||||
  expect(testServer).toBeInstanceOf(Object);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('EDGE-01: Very Large Email - test size limits and handling', async () => {
 | 
			
		||||
  const testCases = [
 | 
			
		||||
    { size: 1 * 1024 * 1024, label: '1MB', shouldPass: true },
 | 
			
		||||
    { size: 10 * 1024 * 1024, label: '10MB', shouldPass: true },
 | 
			
		||||
    { size: 50 * 1024 * 1024, label: '50MB', shouldPass: true },
 | 
			
		||||
    { size: 101 * 1024 * 1024, label: '101MB', shouldPass: false } // Over limit
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  for (const testCase of testCases) {
 | 
			
		||||
    console.log(`\n📧 Testing ${testCase.label} email...`);
 | 
			
		||||
    const socket = await connectToSmtp(testServer.hostname, testServer.port);
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      await waitForGreeting(socket);
 | 
			
		||||
      await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
 | 
			
		||||
      
 | 
			
		||||
      // Check SIZE extension
 | 
			
		||||
      await sendSmtpCommand(socket, `MAIL FROM:<large@example.com> SIZE=${testCase.size}`, 
 | 
			
		||||
        testCase.shouldPass ? '250' : '552');
 | 
			
		||||
      
 | 
			
		||||
      if (testCase.shouldPass) {
 | 
			
		||||
        // Continue with transaction
 | 
			
		||||
        await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
 | 
			
		||||
        await sendSmtpCommand(socket, 'DATA', '354');
 | 
			
		||||
        
 | 
			
		||||
        // Send large content in chunks
 | 
			
		||||
        const chunkSize = 65536; // 64KB chunks
 | 
			
		||||
        const totalChunks = Math.ceil(testCase.size / chunkSize);
 | 
			
		||||
        
 | 
			
		||||
        console.log(`   Sending ${totalChunks} chunks...`);
 | 
			
		||||
        
 | 
			
		||||
        // Headers
 | 
			
		||||
        socket.write('From: large@example.com\r\n');
 | 
			
		||||
        socket.write('To: recipient@example.com\r\n');
 | 
			
		||||
        socket.write(`Subject: ${testCase.label} Test Email\r\n`);
 | 
			
		||||
        socket.write('Content-Type: text/plain\r\n');
 | 
			
		||||
        socket.write('\r\n');
 | 
			
		||||
        
 | 
			
		||||
        // Body in chunks
 | 
			
		||||
        let bytesSent = 100; // Approximate header size
 | 
			
		||||
        const startTime = Date.now();
 | 
			
		||||
        
 | 
			
		||||
        for (let i = 0; i < totalChunks; i++) {
 | 
			
		||||
          const chunk = generateRandomEmail(Math.min(chunkSize, testCase.size - bytesSent));
 | 
			
		||||
          socket.write(chunk);
 | 
			
		||||
          bytesSent += chunk.length;
 | 
			
		||||
          
 | 
			
		||||
          // Progress indicator every 10%
 | 
			
		||||
          if (i % Math.floor(totalChunks / 10) === 0) {
 | 
			
		||||
            const progress = (i / totalChunks * 100).toFixed(0);
 | 
			
		||||
            console.log(`   Progress: ${progress}%`);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          // Small delay to avoid overwhelming
 | 
			
		||||
          if (i % 100 === 0) {
 | 
			
		||||
            await new Promise(resolve => setTimeout(resolve, 10));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // End of data
 | 
			
		||||
        socket.write('\r\n.\r\n');
 | 
			
		||||
        
 | 
			
		||||
        // Wait for response with longer timeout for large emails
 | 
			
		||||
        const response = await new Promise<string>((resolve, reject) => {
 | 
			
		||||
          let buffer = '';
 | 
			
		||||
          const timeout = setTimeout(() => reject(new Error('Timeout')), 60000);
 | 
			
		||||
          
 | 
			
		||||
          const onData = (data: Buffer) => {
 | 
			
		||||
            buffer += data.toString();
 | 
			
		||||
            if (buffer.includes('250') || buffer.includes('5')) {
 | 
			
		||||
              clearTimeout(timeout);
 | 
			
		||||
              socket.removeListener('data', onData);
 | 
			
		||||
              resolve(buffer);
 | 
			
		||||
            }
 | 
			
		||||
          };
 | 
			
		||||
          
 | 
			
		||||
          socket.on('data', onData);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        const duration = Date.now() - startTime;
 | 
			
		||||
        const throughputMBps = (testCase.size / 1024 / 1024) / (duration / 1000);
 | 
			
		||||
        
 | 
			
		||||
        expect(response).toInclude('250');
 | 
			
		||||
        console.log(`   ✅ ${testCase.label} email accepted in ${duration}ms`);
 | 
			
		||||
        console.log(`   Throughput: ${throughputMBps.toFixed(2)} MB/s`);
 | 
			
		||||
        
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`   ✅ ${testCase.label} email properly rejected (over size limit)`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (!testCase.shouldPass && error.message.includes('552')) {
 | 
			
		||||
        console.log(`   ✅ ${testCase.label} email properly rejected: ${error.message}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        throw error;
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      await closeSmtpConnection(socket).catch(() => {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('EDGE-01: Email size enforcement - SIZE parameter', async () => {
 | 
			
		||||
  const socket = await connectToSmtp(testServer.hostname, testServer.port);
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    await waitForGreeting(socket);
 | 
			
		||||
    const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
 | 
			
		||||
    
 | 
			
		||||
    // Extract SIZE limit from capabilities
 | 
			
		||||
    const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
 | 
			
		||||
    const sizeLimit = sizeMatch ? parseInt(sizeMatch[1]) : 0;
 | 
			
		||||
    
 | 
			
		||||
    console.log(`📏 Server advertises SIZE limit: ${sizeLimit} bytes`);
 | 
			
		||||
    expect(sizeLimit).toBeGreaterThan(0);
 | 
			
		||||
    
 | 
			
		||||
    // Test SIZE parameter enforcement
 | 
			
		||||
    const testSizes = [
 | 
			
		||||
      { size: 1000, shouldPass: true },
 | 
			
		||||
      { size: sizeLimit - 1000, shouldPass: true },
 | 
			
		||||
      { size: sizeLimit + 1000, shouldPass: false }
 | 
			
		||||
    ];
 | 
			
		||||
    
 | 
			
		||||
    for (const test of testSizes) {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await sendSmtpCommand(
 | 
			
		||||
          socket, 
 | 
			
		||||
          `MAIL FROM:<test@example.com> SIZE=${test.size}`
 | 
			
		||||
        );
 | 
			
		||||
        
 | 
			
		||||
        if (test.shouldPass) {
 | 
			
		||||
          expect(response).toInclude('250');
 | 
			
		||||
          console.log(`   ✅ SIZE=${test.size} accepted`);
 | 
			
		||||
          await sendSmtpCommand(socket, 'RSET', '250');
 | 
			
		||||
        } else {
 | 
			
		||||
          expect(response).toInclude('552');
 | 
			
		||||
          console.log(`   ✅ SIZE=${test.size} rejected`);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (!test.shouldPass) {
 | 
			
		||||
          console.log(`   ✅ SIZE=${test.size} rejected: ${error.message}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    await closeSmtpConnection(socket);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('EDGE-01: Memory efficiency with large emails', async () => {
 | 
			
		||||
  // Get initial memory usage
 | 
			
		||||
  const initialMemory = process.memoryUsage();
 | 
			
		||||
  console.log('📊 Initial memory usage:', {
 | 
			
		||||
    heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
 | 
			
		||||
    rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB`
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // Send a moderately large email
 | 
			
		||||
  const socket = await connectToSmtp(testServer.hostname, testServer.port);
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    await waitForGreeting(socket);
 | 
			
		||||
    await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
 | 
			
		||||
    await sendSmtpCommand(socket, 'MAIL FROM:<memory@test.com>', '250');
 | 
			
		||||
    await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
 | 
			
		||||
    await sendSmtpCommand(socket, 'DATA', '354');
 | 
			
		||||
    
 | 
			
		||||
    // Send 20MB email
 | 
			
		||||
    const size = 20 * 1024 * 1024;
 | 
			
		||||
    const chunkSize = 1024 * 1024; // 1MB chunks
 | 
			
		||||
    
 | 
			
		||||
    socket.write('From: memory@test.com\r\n');
 | 
			
		||||
    socket.write('To: recipient@example.com\r\n');
 | 
			
		||||
    socket.write('Subject: Memory Test\r\n\r\n');
 | 
			
		||||
    
 | 
			
		||||
    for (let i = 0; i < size / chunkSize; i++) {
 | 
			
		||||
      socket.write(generateRandomEmail(chunkSize));
 | 
			
		||||
      // Force garbage collection if available
 | 
			
		||||
      if (global.gc) {
 | 
			
		||||
        global.gc();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    socket.write('\r\n.\r\n');
 | 
			
		||||
    
 | 
			
		||||
    // Wait for response
 | 
			
		||||
    await new Promise<void>((resolve) => {
 | 
			
		||||
      const onData = (data: Buffer) => {
 | 
			
		||||
        if (data.toString().includes('250')) {
 | 
			
		||||
          socket.removeListener('data', onData);
 | 
			
		||||
          resolve();
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      socket.on('data', onData);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Check memory after processing
 | 
			
		||||
    const finalMemory = process.memoryUsage();
 | 
			
		||||
    const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
 | 
			
		||||
    
 | 
			
		||||
    console.log('📊 Final memory usage:', {
 | 
			
		||||
      heapUsed: `${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
 | 
			
		||||
      rss: `${(finalMemory.rss / 1024 / 1024).toFixed(2)} MB`,
 | 
			
		||||
      increase: `${memoryIncrease.toFixed(2)} MB`
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Memory increase should be reasonable (not storing entire email in memory)
 | 
			
		||||
    expect(memoryIncrease).toBeLessThan(50); // Less than 50MB increase for 20MB email
 | 
			
		||||
    console.log('✅ Memory efficiency test passed');
 | 
			
		||||
    
 | 
			
		||||
  } finally {
 | 
			
		||||
    await closeSmtpConnection(socket);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.test('cleanup - stop SMTP server', async () => {
 | 
			
		||||
  await stopTestServer(testServer);
 | 
			
		||||
  console.log('✅ Test server stopped');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default tap.start();
 | 
			
		||||
		Reference in New Issue
	
	Block a user