update
This commit is contained in:
399
test/suite/error-handling/test.temporary-failures.ts
Normal file
399
test/suite/error-handling/test.temporary-failures.ts
Normal file
@ -0,0 +1,399 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
||||
import type { ITestServer } from '../../helpers/server.loader.js';
|
||||
|
||||
// Test configuration
|
||||
const TEST_PORT = 2525;
|
||||
const TEST_TIMEOUT = 10000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
// Setup
|
||||
tap.test('setup - start SMTP server', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: TEST_PORT,
|
||||
tlsEnabled: false,
|
||||
hostname: 'localhost'
|
||||
});
|
||||
|
||||
expect(testServer).toBeTypeofObject();
|
||||
expect(testServer.port).toEqual(TEST_PORT);
|
||||
});
|
||||
|
||||
// Test: Temporary failure response codes
|
||||
tap.test('Temporary Failures - should handle 4xx response codes properly', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
let receivedData = '';
|
||||
let currentStep = 'connecting';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
receivedData += data.toString();
|
||||
|
||||
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||||
currentStep = 'ehlo';
|
||||
socket.write('EHLO test.example.com\r\n');
|
||||
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
|
||||
currentStep = 'mail_from';
|
||||
// Use a special address that might trigger temporary failure
|
||||
socket.write('MAIL FROM:<temporary-failure@test.com>\r\n');
|
||||
} else if (currentStep === 'mail_from') {
|
||||
const responseCode = receivedData.match(/(\d{3})/)?.[1];
|
||||
|
||||
if (responseCode?.startsWith('4')) {
|
||||
// Temporary failure - expected
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
expect(responseCode).toMatch(/^4\d{2}$/);
|
||||
done.resolve();
|
||||
}, 100);
|
||||
} else if (responseCode === '250') {
|
||||
// Continue if accepted
|
||||
currentStep = 'rcpt_to';
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
}
|
||||
} else if (currentStep === 'rcpt_to') {
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
// Test passed - server handled the flow
|
||||
done.resolve();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
done.reject(error);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Retry after temporary failure
|
||||
tap.test('Temporary Failures - should allow retry after temporary failure', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const attemptConnection = async (attemptNumber: number): Promise<{ success: boolean; responseCode?: string }> => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
let receivedData = '';
|
||||
let currentStep = 'connecting';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
receivedData += data.toString();
|
||||
|
||||
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||||
currentStep = 'ehlo';
|
||||
socket.write('EHLO test.example.com\r\n');
|
||||
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
|
||||
currentStep = 'mail_from';
|
||||
// Include attempt number to potentially vary server response
|
||||
socket.write(`MAIL FROM:<retry-test-${attemptNumber}@example.com>\r\n`);
|
||||
} else if (currentStep === 'mail_from') {
|
||||
const responseCode = receivedData.match(/(\d{3})/)?.[1];
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve({ success: responseCode === '250', responseCode });
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
resolve({ success: false });
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve({ success: false });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Try multiple attempts
|
||||
const attempt1 = await attemptConnection(1);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait before retry
|
||||
const attempt2 = await attemptConnection(2);
|
||||
|
||||
// At least one attempt should work
|
||||
expect(attempt1.success || attempt2.success).toBeTrue();
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Temporary failure during DATA
|
||||
tap.test('Temporary Failures - should handle temporary failure during DATA phase', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
let receivedData = '';
|
||||
let currentStep = 'connecting';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
receivedData += data.toString();
|
||||
|
||||
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||||
currentStep = 'ehlo';
|
||||
socket.write('EHLO test.example.com\r\n');
|
||||
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
|
||||
currentStep = 'mail_from';
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
|
||||
currentStep = 'rcpt_to';
|
||||
socket.write('RCPT TO:<temp-fail-data@example.com>\r\n');
|
||||
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
|
||||
currentStep = 'data';
|
||||
socket.write('DATA\r\n');
|
||||
} else if (currentStep === 'data' && receivedData.includes('354')) {
|
||||
currentStep = 'message';
|
||||
// Send a message that might trigger temporary failure
|
||||
const message = 'Subject: Temporary Failure Test\r\n' +
|
||||
'X-Test-Header: temporary-failure\r\n' +
|
||||
'\r\n' +
|
||||
'This message tests temporary failure handling.\r\n' +
|
||||
'.\r\n';
|
||||
socket.write(message);
|
||||
} else if (currentStep === 'message') {
|
||||
const responseCode = receivedData.match(/(\d{3})/)?.[1];
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
// Either accepted (250) or temporary failure (4xx)
|
||||
expect(responseCode).toMatch(/^(250|4\d{2})$/);
|
||||
done.resolve();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
done.reject(error);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Common temporary failure codes
|
||||
tap.test('Temporary Failures - verify proper temporary failure codes', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Common temporary failure codes and their meanings
|
||||
const temporaryFailureCodes = {
|
||||
'421': 'Service not available, closing transmission channel',
|
||||
'450': 'Requested mail action not taken: mailbox unavailable',
|
||||
'451': 'Requested action aborted: local error in processing',
|
||||
'452': 'Requested action not taken: insufficient system storage'
|
||||
};
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
let receivedData = '';
|
||||
let currentStep = 'connecting';
|
||||
let foundTemporaryCode = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
receivedData += data.toString();
|
||||
|
||||
// Check for any temporary failure codes
|
||||
for (const code of Object.keys(temporaryFailureCodes)) {
|
||||
if (receivedData.includes(code)) {
|
||||
foundTemporaryCode = true;
|
||||
console.log(`Found temporary failure code: ${code} - ${temporaryFailureCodes[code as keyof typeof temporaryFailureCodes]}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||||
currentStep = 'testing';
|
||||
// Try various commands that might trigger temporary failures
|
||||
socket.write('EHLO test.example.com\r\n');
|
||||
} else if (currentStep === 'testing') {
|
||||
// Continue with normal flow
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
// Test passes whether we found temporary codes or not
|
||||
// (server may not expose them in normal operation)
|
||||
done.resolve();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
done.reject(error);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Server overload simulation
|
||||
tap.test('Temporary Failures - should handle server overload gracefully', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const connections: net.Socket[] = [];
|
||||
const results: Array<{ connected: boolean; responseCode?: string }> = [];
|
||||
|
||||
// Create multiple rapid connections to simulate load
|
||||
const connectionPromises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
connectionPromises.push(
|
||||
new Promise<void>((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 2000
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
connections.push(socket);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const response = data.toString();
|
||||
const responseCode = response.match(/(\d{3})/)?.[1];
|
||||
|
||||
if (responseCode?.startsWith('4')) {
|
||||
// Temporary failure due to load
|
||||
results.push({ connected: true, responseCode });
|
||||
} else if (responseCode === '220') {
|
||||
// Normal greeting
|
||||
results.push({ connected: true, responseCode });
|
||||
}
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
results.push({ connected: false });
|
||||
resolve();
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
results.push({ connected: false });
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(connectionPromises);
|
||||
|
||||
// Clean up any remaining connections
|
||||
for (const socket of connections) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Should handle connections (either accept or temporary failure)
|
||||
const handled = results.filter(r => r.connected).length;
|
||||
expect(handled).toBeGreaterThan(0);
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Temporary failure with retry header
|
||||
tap.test('Temporary Failures - should provide retry information if available', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
let receivedData = '';
|
||||
let currentStep = 'connecting';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
receivedData += data.toString();
|
||||
|
||||
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||||
currentStep = 'ehlo';
|
||||
socket.write('EHLO test.example.com\r\n');
|
||||
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
|
||||
currentStep = 'mail_from';
|
||||
// Try to trigger a temporary failure
|
||||
socket.write('MAIL FROM:<test-retry@example.com>\r\n');
|
||||
} else if (currentStep === 'mail_from') {
|
||||
const response = receivedData;
|
||||
|
||||
// Check if response includes retry information
|
||||
if (response.includes('try again') || response.includes('retry') || response.includes('later')) {
|
||||
console.log('Server provided retry guidance in temporary failure');
|
||||
}
|
||||
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
done.resolve();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
done.reject(error);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
|
||||
});
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Teardown
|
||||
tap.test('teardown - stop SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
// Start the test
|
||||
tap.start();
|
Reference in New Issue
Block a user