Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
d059b80682
commit
59eab82f02
6 changed files with 1091 additions and 403 deletions
|
@ -1,23 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
|
||||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
|
||||||
transform: {
|
|
||||||
'^.+\\.ts$': 'ts-jest',
|
|
||||||
},
|
|
||||||
collectCoverageFrom: [
|
|
||||||
'src/**/*.ts',
|
|
||||||
'!src/**/*.d.ts',
|
|
||||||
'!src/index.ts',
|
|
||||||
'!**/node_modules/**',
|
|
||||||
'!**/examples/**',
|
|
||||||
],
|
|
||||||
coverageDirectory: 'coverage',
|
|
||||||
coverageReporters: ['text', 'lcov', 'html'],
|
|
||||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
|
|
||||||
testTimeout: 30000,
|
|
||||||
verbose: true,
|
|
||||||
clearMocks: true,
|
|
||||||
restoreMocks: true,
|
|
||||||
};
|
|
16
package.json
16
package.json
|
@ -24,19 +24,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:cjs && npm run build:esm && npm run build:types",
|
"build": "bun run build:cjs && bun run build:esm && bun run build:types",
|
||||||
"build:cjs": "tsc -p tsconfig.cjs.json",
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
||||||
"build:esm": "echo 'ESM build disabled - use CommonJS for now'",
|
"build:esm": "echo 'ESM build disabled - use CommonJS for now'",
|
||||||
"build:types": "tsc -p tsconfig.types.json",
|
"build:types": "tsc -p tsconfig.types.json",
|
||||||
"dev": "tsc --watch",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublishOnly": "npm run clean && npm run build",
|
"prepublishOnly": "npm run clean && npm run build",
|
||||||
"test": "jest",
|
"lint": "eslint src/**/*.ts && tsc --noEmit",
|
||||||
"test:watch": "jest --watch",
|
"lint:tsc": "tsc --noEmit",
|
||||||
"test:coverage": "jest --coverage",
|
"lint:eslint": "eslint src/**/*.ts",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint:eslint:fix": "eslint src/**/*.ts --fix",
|
||||||
"lint:fix": "eslint src/**/*.ts --fix",
|
|
||||||
"format": "prettier --write 'src/**/*.ts'",
|
"format": "prettier --write 'src/**/*.ts'",
|
||||||
"docs": "typedoc src/index.ts",
|
"docs": "typedoc src/index.ts",
|
||||||
"example:basic": "ts-node examples/basic-usage.ts",
|
"example:basic": "ts-node examples/basic-usage.ts",
|
||||||
|
@ -89,11 +86,8 @@
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"jest": "^29.7.0",
|
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^29.1.1",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typedoc": "^0.25.1",
|
"typedoc": "^0.25.1",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
import { SMTPOverWSClient, ConnectionState, MessagePriority } from '../src/index';
|
|
||||||
|
|
||||||
describe('SMTPOverWSClient', () => {
|
|
||||||
let client: SMTPOverWSClient;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
client = new SMTPOverWSClient({
|
|
||||||
url: 'ws://localhost:3000/smtp',
|
|
||||||
apiKey: 'test-api-key'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
if (client) {
|
|
||||||
await client.shutdown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('should create client with default configuration', () => {
|
|
||||||
expect(client.getConnectionState()).toBe(ConnectionState.DISCONNECTED);
|
|
||||||
expect(client.getQueueSize()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error for missing URL', () => {
|
|
||||||
expect(() => {
|
|
||||||
new SMTPOverWSClient({
|
|
||||||
url: '',
|
|
||||||
apiKey: 'test-key'
|
|
||||||
});
|
|
||||||
}).toThrow('URL is required');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error for missing API key', () => {
|
|
||||||
expect(() => {
|
|
||||||
new SMTPOverWSClient({
|
|
||||||
url: 'ws://localhost:3000',
|
|
||||||
apiKey: ''
|
|
||||||
});
|
|
||||||
}).toThrow('API key is required');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('sendSMTPCommand', () => {
|
|
||||||
it('should queue message and return promise', async () => {
|
|
||||||
const promise = client.sendSMTPCommand('EHLO example.com\\r\\n');
|
|
||||||
|
|
||||||
expect(client.getQueueSize()).toBe(1);
|
|
||||||
expect(promise).toBeInstanceOf(Promise);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
client.clearQueue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respect priority ordering', async () => {
|
|
||||||
// Queue messages with different priorities
|
|
||||||
const lowPromise = client.sendSMTPCommand('LOW', { priority: MessagePriority.LOW });
|
|
||||||
const highPromise = client.sendSMTPCommand('HIGH', { priority: MessagePriority.HIGH });
|
|
||||||
const normalPromise = client.sendSMTPCommand('NORMAL', { priority: MessagePriority.NORMAL });
|
|
||||||
|
|
||||||
expect(client.getQueueSize()).toBe(3);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
client.clearQueue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject when client is shutting down', async () => {
|
|
||||||
const shutdownPromise = client.shutdown();
|
|
||||||
|
|
||||||
await expect(client.sendSMTPCommand('TEST')).rejects.toThrow('Client is shutting down');
|
|
||||||
await shutdownPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject when queue is full', async () => {
|
|
||||||
const smallQueueClient = new SMTPOverWSClient({
|
|
||||||
url: 'ws://localhost:3000/smtp',
|
|
||||||
apiKey: 'test-key',
|
|
||||||
maxQueueSize: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fill queue
|
|
||||||
smallQueueClient.sendSMTPCommand('MSG1');
|
|
||||||
smallQueueClient.sendSMTPCommand('MSG2');
|
|
||||||
|
|
||||||
// This should fail
|
|
||||||
await expect(smallQueueClient.sendSMTPCommand('MSG3')).rejects.toThrow('Queue is full');
|
|
||||||
|
|
||||||
await smallQueueClient.shutdown();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('statistics', () => {
|
|
||||||
it('should provide initial statistics', () => {
|
|
||||||
const stats = client.getStats();
|
|
||||||
|
|
||||||
expect(stats).toEqual({
|
|
||||||
messagesQueued: 0,
|
|
||||||
messagesProcessed: 0,
|
|
||||||
messagesFailed: 0,
|
|
||||||
reconnectionAttempts: 0,
|
|
||||||
totalConnections: 0,
|
|
||||||
averageResponseTime: 0,
|
|
||||||
queueSize: 0,
|
|
||||||
connectionUptime: 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('queue management', () => {
|
|
||||||
it('should clear queue', () => {
|
|
||||||
client.sendSMTPCommand('MSG1');
|
|
||||||
client.sendSMTPCommand('MSG2');
|
|
||||||
|
|
||||||
expect(client.getQueueSize()).toBe(2);
|
|
||||||
|
|
||||||
client.clearQueue();
|
|
||||||
expect(client.getQueueSize()).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('shutdown', () => {
|
|
||||||
it('should shutdown gracefully', async () => {
|
|
||||||
await expect(client.shutdown()).resolves.toBeUndefined();
|
|
||||||
expect(client.getConnectionState()).toBe(ConnectionState.DISCONNECTED);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should timeout if shutdown takes too long', async () => {
|
|
||||||
await expect(client.shutdown(100)).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,15 +0,0 @@
|
||||||
/**
|
|
||||||
* Jest test setup
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Extend Jest timeout for integration tests
|
|
||||||
jest.setTimeout(30000);
|
|
||||||
|
|
||||||
// Mock WebSocket for tests
|
|
||||||
(global as any).WebSocket = jest.fn().mockImplementation(() => ({
|
|
||||||
send: jest.fn(),
|
|
||||||
close: jest.fn(),
|
|
||||||
terminate: jest.fn(),
|
|
||||||
on: jest.fn(),
|
|
||||||
readyState: 1, // OPEN
|
|
||||||
}));
|
|
|
@ -1,223 +0,0 @@
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import { SMTPWSTransport, createTransport } from '../src/transport';
|
|
||||||
|
|
||||||
describe('SMTPWSTransport', () => {
|
|
||||||
let transport: SMTPWSTransport;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
transport = createTransport({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000,
|
|
||||||
auth: {
|
|
||||||
user: 'test-api-key'
|
|
||||||
},
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await transport.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('should create transport with correct configuration', () => {
|
|
||||||
expect(transport.name).toBe('SMTPWS');
|
|
||||||
expect(transport.version).toBe('1.0.0');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle secure connection configuration', () => {
|
|
||||||
const secureTransport = createTransport({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 443,
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: 'test-key'
|
|
||||||
},
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = secureTransport.getTransportInfo();
|
|
||||||
expect(info.secure).toBe(true);
|
|
||||||
expect(info.port).toBe(443);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTransportInfo', () => {
|
|
||||||
it('should return transport information', () => {
|
|
||||||
const info = transport.getTransportInfo();
|
|
||||||
|
|
||||||
expect(info).toMatchObject({
|
|
||||||
name: 'SMTPWS',
|
|
||||||
version: '1.0.0',
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('send', () => {
|
|
||||||
it('should send mail message', async () => {
|
|
||||||
const mockMail = {
|
|
||||||
data: {
|
|
||||||
envelope: {
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: ['recipient@example.com']
|
|
||||||
},
|
|
||||||
raw: 'Subject: Test\r\n\r\nTest message'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock the internal client
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockResolvedValueOnce('250 Hello') // EHLO
|
|
||||||
.mockResolvedValueOnce('250 OK') // MAIL FROM
|
|
||||||
.mockResolvedValueOnce('250 OK') // RCPT TO
|
|
||||||
.mockResolvedValueOnce('354 Start mail input') // DATA
|
|
||||||
.mockResolvedValueOnce('250 Message accepted') // Message content
|
|
||||||
.mockResolvedValueOnce('221 Bye'); // QUIT
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
const result = await transport.send(mockMail);
|
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
envelope: mockMail.data.envelope,
|
|
||||||
accepted: ['recipient@example.com'],
|
|
||||||
rejected: [],
|
|
||||||
pending: []
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockSendCommand).toHaveBeenCalledTimes(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle rejected recipients', async () => {
|
|
||||||
const mockMail = {
|
|
||||||
data: {
|
|
||||||
envelope: {
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: ['good@example.com', 'bad@example.com']
|
|
||||||
},
|
|
||||||
raw: 'Subject: Test\r\n\r\nTest message'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockResolvedValueOnce('250 Hello') // EHLO
|
|
||||||
.mockResolvedValueOnce('250 OK') // MAIL FROM
|
|
||||||
.mockResolvedValueOnce('250 OK') // RCPT TO (good)
|
|
||||||
.mockResolvedValueOnce('550 No such user') // RCPT TO (bad)
|
|
||||||
.mockResolvedValueOnce('354 Start mail input') // DATA
|
|
||||||
.mockResolvedValueOnce('250 Message accepted') // Message content
|
|
||||||
.mockResolvedValueOnce('221 Bye'); // QUIT
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
const result = await transport.send(mockMail);
|
|
||||||
|
|
||||||
expect(result.accepted).toEqual(['good@example.com']);
|
|
||||||
expect(result.rejected).toEqual(['bad@example.com']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call callback on success', (done) => {
|
|
||||||
const mockMail = {
|
|
||||||
data: {
|
|
||||||
envelope: {
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: ['recipient@example.com']
|
|
||||||
},
|
|
||||||
raw: 'Test message'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockResolvedValueOnce('250 Hello')
|
|
||||||
.mockResolvedValueOnce('250 OK')
|
|
||||||
.mockResolvedValueOnce('250 OK')
|
|
||||||
.mockResolvedValueOnce('354 Start mail input')
|
|
||||||
.mockResolvedValueOnce('250 Message accepted')
|
|
||||||
.mockResolvedValueOnce('221 Bye');
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
transport.send(mockMail, (err, info) => {
|
|
||||||
expect(err).toBeNull();
|
|
||||||
expect(info).toBeDefined();
|
|
||||||
expect(info?.accepted).toEqual(['recipient@example.com']);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call callback on error', (done) => {
|
|
||||||
const mockMail = {
|
|
||||||
envelope: {
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: ['recipient@example.com']
|
|
||||||
},
|
|
||||||
raw: 'Test message'
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockRejectedValueOnce(new Error('Connection failed'));
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
transport.send(mockMail, (err, info) => {
|
|
||||||
expect(err).toBeDefined();
|
|
||||||
expect(err?.message).toBe('Connection failed');
|
|
||||||
expect(info).toBeUndefined();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('verify', () => {
|
|
||||||
it('should verify transport connectivity', async () => {
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockResolvedValueOnce('250 Hello');
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
const result = await transport.verify();
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(mockSendCommand).toHaveBeenCalledWith('EHLO transport-verify\r\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error on verification failure', async () => {
|
|
||||||
const mockSendCommand = jest.fn()
|
|
||||||
.mockRejectedValueOnce(new Error('Connection refused'));
|
|
||||||
|
|
||||||
(transport as any).client.sendSMTPCommand = mockSendCommand;
|
|
||||||
|
|
||||||
await expect(transport.verify()).rejects.toThrow('Transport verification failed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error on missing API key during construction', async () => {
|
|
||||||
expect(() => createTransport({
|
|
||||||
host: 'localhost',
|
|
||||||
auth: { user: '' },
|
|
||||||
debug: false
|
|
||||||
})).toThrow('API key is required');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isIdle', () => {
|
|
||||||
it('should return true when transport is idle', () => {
|
|
||||||
expect(transport.isIdle()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when transport has queued messages', () => {
|
|
||||||
// Mock queue size
|
|
||||||
(transport as any).client.getQueueSize = jest.fn().mockReturnValue(5);
|
|
||||||
|
|
||||||
expect(transport.isIdle()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('nodemailer integration', () => {
|
|
||||||
it('should work as nodemailer transport', async () => {
|
|
||||||
const transporter = nodemailer.createTransport(transport);
|
|
||||||
expect(transporter).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue