318 lines
8.7 KiB
TypeScript
318 lines
8.7 KiB
TypeScript
|
import * as plugins from '@push.rocks/tapbundle';
|
||
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||
|
import * as net from 'net';
|
||
|
import { startTestServer, stopTestServer } from '../server.loader.js';
|
||
|
|
||
|
const TEST_PORT = 2525;
|
||
|
const TEST_TIMEOUT = 10000;
|
||
|
|
||
|
tap.test('prepare server', async () => {
|
||
|
await startTestServer();
|
||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
});
|
||
|
|
||
|
// Test: Basic NOOP command
|
||
|
tap.test('NOOP - should accept NOOP command', 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 = 'noop';
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else if (currentStep === 'noop' && receivedData.includes('250')) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(receivedData).toInclude('250'); // NOOP response
|
||
|
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: Multiple NOOP commands
|
||
|
tap.test('NOOP - should handle multiple consecutive NOOP commands', async (tools) => {
|
||
|
const done = tools.defer();
|
||
|
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: TEST_TIMEOUT
|
||
|
});
|
||
|
|
||
|
let receivedData = '';
|
||
|
let currentStep = 'connecting';
|
||
|
let noopCount = 0;
|
||
|
const maxNoops = 3;
|
||
|
|
||
|
socket.on('data', (data) => {
|
||
|
receivedData += data.toString();
|
||
|
|
||
|
if (currentStep === 'connecting' && receivedData.includes('220')) {
|
||
|
currentStep = 'ehlo';
|
||
|
receivedData = ''; // Clear buffer after processing
|
||
|
socket.write('EHLO test.example.com\r\n');
|
||
|
} else if (currentStep === 'ehlo' && receivedData.includes('250 ')) {
|
||
|
currentStep = 'noop';
|
||
|
receivedData = ''; // Clear buffer after processing
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else if (currentStep === 'noop' && receivedData.includes('250 OK')) {
|
||
|
noopCount++;
|
||
|
receivedData = ''; // Clear buffer after processing
|
||
|
|
||
|
if (noopCount < maxNoops) {
|
||
|
// Send another NOOP command
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(noopCount).toEqual(maxNoops);
|
||
|
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: NOOP during transaction
|
||
|
tap.test('NOOP - should work during email transaction', 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 = 'noop_after_mail';
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else if (currentStep === 'noop_after_mail' && receivedData.includes('250')) {
|
||
|
currentStep = 'rcpt_to';
|
||
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||
|
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
|
||
|
currentStep = 'noop_after_rcpt';
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else if (currentStep === 'noop_after_rcpt' && receivedData.includes('250')) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(receivedData).toInclude('250');
|
||
|
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: NOOP with parameter (should be ignored)
|
||
|
tap.test('NOOP - should handle NOOP with parameters', 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 = 'noop_with_param';
|
||
|
socket.write('NOOP ignored parameter\r\n'); // Parameters should be ignored
|
||
|
} else if (currentStep === 'noop_with_param' && receivedData.includes('250')) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(receivedData).toInclude('250');
|
||
|
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: NOOP before EHLO/HELO
|
||
|
tap.test('NOOP - should work before EHLO', 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 = 'noop_before_ehlo';
|
||
|
socket.write('NOOP\r\n');
|
||
|
} else if (currentStep === 'noop_before_ehlo' && receivedData.includes('250')) {
|
||
|
currentStep = 'ehlo';
|
||
|
socket.write('EHLO test.example.com\r\n');
|
||
|
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(receivedData).toInclude('250');
|
||
|
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: Rapid NOOP commands (stress test)
|
||
|
tap.test('NOOP - should handle rapid NOOP commands', async (tools) => {
|
||
|
const done = tools.defer();
|
||
|
|
||
|
const socket = net.createConnection({
|
||
|
host: 'localhost',
|
||
|
port: TEST_PORT,
|
||
|
timeout: TEST_TIMEOUT
|
||
|
});
|
||
|
|
||
|
let receivedData = '';
|
||
|
let currentStep = 'connecting';
|
||
|
let noopsSent = 0;
|
||
|
let noopsReceived = 0;
|
||
|
const rapidNoops = 10;
|
||
|
|
||
|
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 = 'rapid_noop';
|
||
|
// Send multiple NOOPs rapidly
|
||
|
for (let i = 0; i < rapidNoops; i++) {
|
||
|
socket.write('NOOP\r\n');
|
||
|
noopsSent++;
|
||
|
}
|
||
|
} else if (currentStep === 'rapid_noop') {
|
||
|
// Count 250 responses
|
||
|
const matches = receivedData.match(/250 /g);
|
||
|
if (matches) {
|
||
|
noopsReceived = matches.length - 1; // -1 for EHLO response
|
||
|
}
|
||
|
|
||
|
if (noopsReceived >= rapidNoops) {
|
||
|
socket.write('QUIT\r\n');
|
||
|
setTimeout(() => {
|
||
|
socket.destroy();
|
||
|
expect(noopsReceived).toBeGreaterThan(rapidNoops - 1);
|
||
|
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;
|
||
|
});
|
||
|
|
||
|
tap.test('cleanup server', async () => {
|
||
|
await stopTestServer();
|
||
|
});
|
||
|
|
||
|
tap.start();
|