Files
admin/host-stacks/local/sched-pipelines/unisender-mock/index.cjs

93 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Мок Unisender Go (goapi.unisender.ru/ru/transactional/api/v1/email/send.json).
* Реальных отправок НЕТ — каждое письмо пишется в MAIL_DIR как JSON-файл.
* Валидирует контракт как настоящий API (те же коды ошибок) — чтобы ловить
* регрессии формы (body.text vs plaintext и т.п.) без платных писем.
*
* Эндпоинты:
* POST /ru/transactional/api/v1/email/send.json — приём письма (X-API-KEY или api_key)
* GET /health — живость
*/
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const PORT = Number(process.env.PORT || 8080);
const MAIL_DIR = process.env.MAIL_DIR || '/mail';
fs.mkdirSync(MAIL_DIR, { recursive: true });
function json(res, code, body) {
const payload = JSON.stringify(body);
res.writeHead(code, { 'content-type': 'application/json' });
res.end(payload);
}
function apiError(res, code, message) {
json(res, 400, { status: 'error', code, message });
}
function saveMail(payload, apiKey) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const jobId = `mock-${ts}`;
const file = path.join(MAIL_DIR, `${ts}.json`);
const msg = payload.message || {};
const recipients = (msg.recipients || []).map((r) => r.email);
const record = {
job_id: jobId,
receivedAt: new Date().toISOString(),
api_key: apiKey,
from_email: msg.from_email || null,
from_name: msg.from_name || null,
subject: msg.subject || null,
recipients,
body: msg.body || null,
raw: payload,
};
fs.writeFileSync(file, JSON.stringify(record, null, 2) + '\n');
return { jobId, recipients };
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
if (req.method === 'GET' && url.pathname === '/health') return json(res, 200, { ok: true });
if (req.method === 'POST' && url.pathname === '/ru/transactional/api/v1/email/send.json') {
const chunks = [];
for await (const c of req) chunks.push(c);
let payload;
try {
payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
} catch {
return apiError(res, 111, 'JSON parsing error');
}
const apiKey = req.headers['x-api-key'] || payload.api_key;
if (!apiKey) return apiError(res, 101, 'API key is missing');
const msg = payload.message;
if (!msg || typeof msg !== 'object') return apiError(res, 2000, "Error in 'message' field. Message object required.");
if (!Array.isArray(msg.recipients) || msg.recipients.length === 0 || !msg.recipients[0].email) {
return apiError(res, 2001, "Error in 'recipients' field. Recipients required.");
}
const body = msg.body || {};
if (!body.plaintext && !body.html) {
// зеркалит реальный ответ: 400 code 1577 No message body passed
return apiError(res, 1577, "Error in 'body' field. No message body passed.");
}
if (!msg.from_email) return apiError(res, 2002, "Error in 'from_email' field. Sender required.");
const { jobId, recipients } = saveMail(payload, apiKey);
console.log(`✉️ mock email: ${msg.subject || '(no subject)'} -> ${recipients.join(', ')} (${jobId})`);
return json(res, 200, { status: 'success', job_id: jobId, emails: recipients, tags: [] });
}
return json(res, 404, { status: 'error', code: 404, message: 'not found' });
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`unisender-mock on :${PORT}, mail dir: ${MAIL_DIR}`);
});