feat(wire): Add wire protocol, WireTarget & WireParser, Smartmail JSON serialization; refactor plugins and update dependencies

This commit is contained in:
2025-11-29 15:36:36 +00:00
parent 4277ace8cd
commit ddf442274e
14 changed files with 3969 additions and 2567 deletions

View File

@@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartmail from '../ts/index.js';
import * as plugins from '../ts/smartmail.plugins.js';
@@ -134,14 +134,285 @@ tap.test('should add email headers', async () => {
body: 'Test body',
from: 'noreply@example.com'
});
headerMailer.addHeader('X-Test-Header', 'TestValue');
headerMailer.addHeader('X-Tracking-ID', '12345');
const mimeObj = await headerMailer.toMimeFormat();
expect(mimeObj.headers['X-Test-Header']).toEqual('TestValue');
expect(mimeObj.headers['X-Tracking-ID']).toEqual('12345');
});
tap.start();
// Fluent Chaining Tests
tap.test('should support fluent chaining for all mutation methods', async () => {
const email = new smartmail.Smartmail({
from: 'sender@example.com',
subject: 'Fluent {{name}}',
body: 'Hello {{name}}'
});
// Chain all methods together
const result = email
.addRecipient('user1@example.com')
.addRecipient('cc@example.com', 'cc')
.addRecipients(['user2@example.com', 'user3@example.com'])
.setReplyTo('reply@example.com')
.setPriority('high')
.addHeader('X-Custom', 'value')
.applyVariables({ name: 'John' });
// Result should be the same instance
expect(result).toEqual(email);
// All values should be set
expect(email.options.to!.length).toEqual(3);
expect(email.options.cc!.length).toEqual(1);
expect(email.options.replyTo).toEqual('reply@example.com');
expect(email.options.priority).toEqual('high');
expect(email.options.headers!['X-Custom']).toEqual('value');
expect(email.options.subject).toEqual('Fluent John');
expect(email.options.body).toEqual('Hello John');
});
// Wire Format Serialization Tests
tap.test('should serialize to JSON and back with toObject/fromObject', async () => {
const original = new smartmail.Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
cc: ['cc@example.com'],
subject: 'Test Subject',
body: 'Test Body',
htmlBody: '<p>Test HTML</p>',
priority: 'high',
headers: { 'X-Custom': 'value' },
creationObjectRef: { orderId: '12345' }
});
const obj = original.toObject();
expect(obj.from).toEqual('sender@example.com');
expect(obj.to).toInclude('recipient@example.com');
expect(obj.cc).toInclude('cc@example.com');
expect(obj.subject).toEqual('Test Subject');
expect(obj.body).toEqual('Test Body');
expect(obj.htmlBody).toEqual('<p>Test HTML</p>');
expect(obj.priority).toEqual('high');
expect(obj.headers!['X-Custom']).toEqual('value');
expect(obj.creationObjectRef).toEqual({ orderId: '12345' });
expect(obj.attachments).toBeDefined();
// Reconstruct from object
const reconstructed = smartmail.Smartmail.fromObject(obj);
expect(reconstructed.options.from).toEqual(original.options.from);
expect(reconstructed.options.subject).toEqual(original.options.subject);
expect(reconstructed.options.body).toEqual(original.options.body);
expect(reconstructed.options.htmlBody).toEqual(original.options.htmlBody);
expect(reconstructed.options.priority).toEqual(original.options.priority);
expect(reconstructed.getCreationObject()).toEqual({ orderId: '12345' });
});
tap.test('should serialize to JSON string and back with toJson/fromJson', async () => {
const original = new smartmail.Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'JSON Test',
body: 'JSON Body'
});
const jsonString = original.toJson();
// Should be valid JSON
const parsed = JSON.parse(jsonString);
expect(parsed.from).toEqual('sender@example.com');
expect(parsed.subject).toEqual('JSON Test');
// Reconstruct from JSON string
const reconstructed = smartmail.Smartmail.fromJson(jsonString);
expect(reconstructed.options.from).toEqual(original.options.from);
expect(reconstructed.options.subject).toEqual(original.options.subject);
expect(reconstructed.options.body).toEqual(original.options.body);
});
tap.test('should serialize attachments to base64 and back', async () => {
const testContent = 'Hello, this is test file content!';
const testBuffer = Buffer.from(testContent);
const original = new smartmail.Smartmail({
from: 'sender@example.com',
subject: 'Attachment Test',
body: 'Test Body'
});
// Create a SmartFile and add it as attachment
const smartfile = new plugins.smartfile.SmartFile({
path: 'test-file.txt',
contentBuffer: testBuffer,
base: './'
});
original.addAttachment(smartfile);
// Serialize to object
const obj = original.toObject();
expect(obj.attachments.length).toEqual(1);
expect(obj.attachments[0].filename).toEqual('test-file.txt');
expect(obj.attachments[0].contentBase64).toEqual(testBuffer.toString('base64'));
// Reconstruct
const reconstructed = smartmail.Smartmail.fromObject(obj);
expect(reconstructed.attachments.length).toEqual(1);
expect(reconstructed.attachments[0].contentBuffer.toString()).toEqual(testContent);
});
// Wire Protocol Message Types Tests
tap.test('should have correct wire message type interfaces', async () => {
// Test createMessageId and createTimestamp helpers
const messageId = smartmail.createMessageId();
const timestamp = smartmail.createTimestamp();
expect(typeof messageId).toEqual('string');
expect(messageId.length).toBeGreaterThan(0);
expect(typeof timestamp).toEqual('string');
expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
});
// WireParser Tests
tap.test('should parse and handle mail.send requests with WireParser', async () => {
let receivedEmail: smartmail.Smartmail<any> | null = null;
const parser = new smartmail.WireParser({
onMailSend: async (email, options) => {
receivedEmail = email;
return {
type: 'mail.send.response',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
success: true,
deliveryId: 'test-delivery-id'
};
}
});
const testEmail = new smartmail.Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Wire Test',
body: 'Wire Body'
});
const request: smartmail.IMailSendRequest = {
type: 'mail.send',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
email: testEmail.toObject()
};
const response = await parser.handle(request) as smartmail.IMailSendResponse;
expect(response.type).toEqual('mail.send.response');
expect(response.success).toBeTrue();
expect(response.deliveryId).toEqual('test-delivery-id');
expect(receivedEmail).not.toBeNull();
expect(receivedEmail!.options.subject).toEqual('Wire Test');
});
tap.test('should parse and handle settings.update requests with WireParser', async () => {
let receivedSettings: smartmail.IWireSettings | null = null;
const parser = new smartmail.WireParser({
onSettingsUpdate: async (settings) => {
receivedSettings = settings;
return {
type: 'settings.update.response',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
success: true
};
}
});
const request: smartmail.ISettingsUpdateRequest = {
type: 'settings.update',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
settings: {
smtp: {
host: 'smtp.example.com',
port: 587,
secure: true,
username: 'user',
password: 'pass'
},
defaultFrom: 'noreply@example.com',
customSetting: 'custom-value'
}
};
const response = await parser.handle(request) as smartmail.ISettingsUpdateResponse;
expect(response.type).toEqual('settings.update.response');
expect(response.success).toBeTrue();
expect(receivedSettings).not.toBeNull();
expect(receivedSettings!.smtp!.host).toEqual('smtp.example.com');
expect(receivedSettings!.defaultFrom).toEqual('noreply@example.com');
expect(receivedSettings!.customSetting).toEqual('custom-value');
});
tap.test('should handle parseAndHandle convenience method', async () => {
const parser = new smartmail.WireParser({
onMailSend: async (email) => ({
type: 'mail.send.response',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
success: true,
deliveryId: 'convenience-test-id'
})
});
const testEmail = new smartmail.Smartmail({
from: 'sender@example.com',
subject: 'Convenience Test',
body: 'Test Body'
});
const requestJson = JSON.stringify({
type: 'mail.send',
messageId: smartmail.createMessageId(),
timestamp: smartmail.createTimestamp(),
email: testEmail.toObject()
});
const responseJson = await parser.parseAndHandle(requestJson);
const response = JSON.parse(responseJson);
expect(response.type).toEqual('mail.send.response');
expect(response.success).toBeTrue();
expect(response.deliveryId).toEqual('convenience-test-id');
});
tap.test('should return error response for unsupported handlers', async () => {
const parser = new smartmail.WireParser({}); // No handlers
const request: smartmail.IMailSendRequest = {
type: 'mail.send',
messageId: 'test-msg-id',
timestamp: smartmail.createTimestamp(),
email: new smartmail.Smartmail({
from: 'sender@example.com',
subject: 'Test',
body: 'Test'
}).toObject()
};
const response = await parser.handle(request) as smartmail.IMailSendResponse;
expect(response.type).toEqual('mail.send.response');
expect(response.success).toBeFalse();
expect(response.error).toEqual('Mail send not supported');
});
export default tap.start();