kickload-watcher-mcp 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/email-sender.js +141 -137
- package/kickload-client.js +14 -10
- package/package.json +1 -1
- package/pipeline.js +13 -1
package/email-sender.js
CHANGED
|
@@ -1,137 +1,141 @@
|
|
|
1
|
-
import nodemailer from "nodemailer";
|
|
2
|
-
import { config } from "./config.js";
|
|
3
|
-
|
|
4
|
-
let transporter = null;
|
|
5
|
-
|
|
6
|
-
export function _getTransporter() { return transporter; }
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Initialize email transporter.
|
|
10
|
-
* Does NOT verify — call verifyEmailService() for startup check.
|
|
11
|
-
*/
|
|
12
|
-
export function initEmailSender() {
|
|
13
|
-
const p = config.email.provider;
|
|
14
|
-
|
|
15
|
-
if (p === "smtp") {
|
|
16
|
-
transporter = nodemailer.createTransport({
|
|
17
|
-
host: config.email.smtp.host,
|
|
18
|
-
port: config.email.smtp.port,
|
|
19
|
-
secure: config.email.smtp.port === 465,
|
|
20
|
-
auth: { user: config.email.smtp.user, pass: config.email.smtp.pass },
|
|
21
|
-
});
|
|
22
|
-
} else if (p === "sendgrid") {
|
|
23
|
-
transporter = nodemailer.createTransport({
|
|
24
|
-
host: "smtp.sendgrid.net", port: 587,
|
|
25
|
-
auth: { user: "apikey", pass: config.email.sendgrid.apiKey },
|
|
26
|
-
});
|
|
27
|
-
} else if (p === "ses") {
|
|
28
|
-
transporter = nodemailer.createTransport({
|
|
29
|
-
host: `email-smtp.${config.email.ses.region}.amazonaws.com`,
|
|
30
|
-
port: 587, secure: false,
|
|
31
|
-
auth: { user: config.email.ses.accessKey, pass: config.email.ses.secretKey },
|
|
32
|
-
});
|
|
33
|
-
} else {
|
|
34
|
-
throw new Error(`Unknown email provider: ${p}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* PROBLEM 2 — Email strict mode.
|
|
40
|
-
* Verifies the transporter can authenticate.
|
|
41
|
-
* Exits process if verification fails.
|
|
42
|
-
*/
|
|
43
|
-
export async function verifyEmailService() {
|
|
44
|
-
if (!transporter) {
|
|
45
|
-
console.error("\n❌ Email transporter not initialized before verify()");
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
process.stdout.write(" Verifying email service... ");
|
|
50
|
-
try {
|
|
51
|
-
await transporter.verify();
|
|
52
|
-
console.log("✅ Email service verified");
|
|
53
|
-
} catch (err) {
|
|
54
|
-
console.log(""); // newline after the "..."
|
|
55
|
-
console.error("\n❌ Email verification failed
|
|
56
|
-
console.error(
|
|
57
|
-
console.error(
|
|
58
|
-
console.error(
|
|
59
|
-
console.error("
|
|
60
|
-
console.error(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<tr><td style="padding:28px 32px
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<tr><td style="padding:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
|
|
4
|
+
let transporter = null;
|
|
5
|
+
|
|
6
|
+
export function _getTransporter() { return transporter; }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initialize email transporter.
|
|
10
|
+
* Does NOT verify — call verifyEmailService() for startup check.
|
|
11
|
+
*/
|
|
12
|
+
export function initEmailSender() {
|
|
13
|
+
const p = config.email.provider;
|
|
14
|
+
|
|
15
|
+
if (p === "smtp") {
|
|
16
|
+
transporter = nodemailer.createTransport({
|
|
17
|
+
host: config.email.smtp.host,
|
|
18
|
+
port: config.email.smtp.port,
|
|
19
|
+
secure: config.email.smtp.port === 465,
|
|
20
|
+
auth: { user: config.email.smtp.user, pass: config.email.smtp.pass },
|
|
21
|
+
});
|
|
22
|
+
} else if (p === "sendgrid") {
|
|
23
|
+
transporter = nodemailer.createTransport({
|
|
24
|
+
host: "smtp.sendgrid.net", port: 587,
|
|
25
|
+
auth: { user: "apikey", pass: config.email.sendgrid.apiKey },
|
|
26
|
+
});
|
|
27
|
+
} else if (p === "ses") {
|
|
28
|
+
transporter = nodemailer.createTransport({
|
|
29
|
+
host: `email-smtp.${config.email.ses.region}.amazonaws.com`,
|
|
30
|
+
port: 587, secure: false,
|
|
31
|
+
auth: { user: config.email.ses.accessKey, pass: config.email.ses.secretKey },
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
throw new Error(`Unknown email provider: ${p}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* PROBLEM 2 — Email strict mode.
|
|
40
|
+
* Verifies the transporter can authenticate.
|
|
41
|
+
* Exits process if verification fails.
|
|
42
|
+
*/
|
|
43
|
+
export async function verifyEmailService() {
|
|
44
|
+
if (!transporter) {
|
|
45
|
+
console.error("\n❌ Email transporter not initialized before verify()");
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.stdout.write(" Verifying email service... ");
|
|
50
|
+
try {
|
|
51
|
+
await transporter.verify();
|
|
52
|
+
console.log("✅ Email service verified");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.log(""); // newline after the "..."
|
|
55
|
+
console.error("\n❌ Email verification failed");
|
|
56
|
+
console.error(" Fix:");
|
|
57
|
+
console.error(" • Use Gmail App Password (NOT your real password)");
|
|
58
|
+
console.error(" • Enable 2-Step Verification");
|
|
59
|
+
console.error(" • Check SMTP_USER and SMTP_PASS\n");
|
|
60
|
+
console.error(` Provider : ${config.email.provider}`);
|
|
61
|
+
console.error(` User : ${config.email.smtp.user || "(none)"}`);
|
|
62
|
+
console.error(` Error : ${err.message}`);
|
|
63
|
+
console.error("\n Fix: check SMTP_USER / SMTP_PASS in .env");
|
|
64
|
+
console.error(" For Gmail, use an App Password: myaccount.google.com/apppasswords\n");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function sendResultEmail({ toEmail, endpoint, passed, summary, downloadUrl, pdfFilename, durationMs, testPrompt }) {
|
|
70
|
+
if (!transporter) throw new Error("Email sender not initialized");
|
|
71
|
+
|
|
72
|
+
const status = passed ? "✅ PASS" : "❌ FAIL";
|
|
73
|
+
const statusColor = passed ? "#16a34a" : "#dc2626";
|
|
74
|
+
const statusBg = passed ? "#f0fdf4" : "#fef2f2";
|
|
75
|
+
const statusBorder = passed ? "#86efac" : "#fca5a5";
|
|
76
|
+
const durationSec = durationMs ? Math.round(durationMs / 1000) : null;
|
|
77
|
+
const testedAt = new Date().toLocaleString("en-IN", { timeZone: "Asia/Kolkata", dateStyle: "medium", timeStyle: "short" });
|
|
78
|
+
const subject = `[KickLoad Watcher] ${status} — ${endpoint}`;
|
|
79
|
+
|
|
80
|
+
const rows = [
|
|
81
|
+
["Endpoint", `<span style="font-family:monospace">${endpoint}</span>`],
|
|
82
|
+
["Tested At", `${testedAt} IST`],
|
|
83
|
+
summary?.averageLatency != null ? ["Latency", `${summary.averageLatency}ms`] : null,
|
|
84
|
+
summary?.errorPercentage != null ? ["Error Rate", `${summary.errorPercentage}%`] : null,
|
|
85
|
+
summary?.throughput != null ? ["Throughput", `${summary.throughput?.toLocaleString()} req/s`] : null,
|
|
86
|
+
durationSec != null ? ["Duration", `${durationSec}s`] : null,
|
|
87
|
+
].filter(Boolean).map(([label, value]) =>
|
|
88
|
+
`<tr style="border-bottom:1px solid #f3f4f6;">
|
|
89
|
+
<td style="padding:8px 16px;color:#6b7280;font-size:14px;">${label}</td>
|
|
90
|
+
<td style="padding:8px 16px;color:#111827;font-size:14px;font-weight:600;text-align:right;">${value}</td>
|
|
91
|
+
</tr>`
|
|
92
|
+
).join("");
|
|
93
|
+
|
|
94
|
+
const downloadBtn = downloadUrl
|
|
95
|
+
? `<a href="${downloadUrl}" style="display:inline-block;background:#f47c20;color:#fff;text-decoration:none;padding:12px 28px;border-radius:8px;font-weight:700;font-size:14px;">Download Report</a>
|
|
96
|
+
<p style="color:#9ca3af;font-size:12px;margin-top:8px;">${pdfFilename || ""} · expires in 24hrs</p>`
|
|
97
|
+
: `<p style="color:#9ca3af;font-size:13px;">Report available in KickLoad dashboard.</p>`;
|
|
98
|
+
|
|
99
|
+
const html = `<!DOCTYPE html><html><body style="margin:0;padding:0;background:#f9fafb;font-family:Inter,Arial,sans-serif;">
|
|
100
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="padding:40px 20px;"><tr><td align="center">
|
|
101
|
+
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.1);">
|
|
102
|
+
<tr><td style="background:linear-gradient(135deg,#1e202a,#303952);padding:28px 32px;">
|
|
103
|
+
<h1 style="margin:0;color:#fff;font-size:22px;font-weight:800;">KickLoad Watcher</h1>
|
|
104
|
+
<p style="margin:4px 0 0;color:#9ca3af;font-size:13px;">OneQA — Automated API Testing</p>
|
|
105
|
+
</td></tr>
|
|
106
|
+
<tr><td style="padding:28px 32px 0;">
|
|
107
|
+
<div style="background:${statusBg};border:1px solid ${statusBorder};border-radius:10px;padding:16px 20px;">
|
|
108
|
+
<div style="font-size:20px;font-weight:800;color:${statusColor};">${status}</div>
|
|
109
|
+
<div style="font-size:13px;color:#6b7280;margin-top:2px;font-family:monospace;">${endpoint}</div>
|
|
110
|
+
</div>
|
|
111
|
+
</td></tr>
|
|
112
|
+
<tr><td style="padding:20px 32px 0;">
|
|
113
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;">
|
|
114
|
+
<tr style="background:#f9fafb;"><td colspan="2" style="padding:10px 16px;font-size:12px;font-weight:700;color:#9ca3af;text-transform:uppercase;border-bottom:1px solid #e5e7eb;">Test Details</td></tr>
|
|
115
|
+
${rows}
|
|
116
|
+
</table>
|
|
117
|
+
</td></tr>
|
|
118
|
+
<tr><td style="padding:24px 32px;text-align:center;">${downloadBtn}</td></tr>
|
|
119
|
+
<tr><td style="padding:20px 32px;border-top:1px solid #f3f4f6;">
|
|
120
|
+
<p style="margin:0;color:#9ca3af;font-size:12px;text-align:center;">
|
|
121
|
+
KickLoad Watcher · <a href="https://kickload.neeyatai.com" style="color:#f47c20;text-decoration:none;">NeeyatAI</a>
|
|
122
|
+
</p>
|
|
123
|
+
</td></tr>
|
|
124
|
+
</table></td></tr></table></body></html>`;
|
|
125
|
+
|
|
126
|
+
const text =
|
|
127
|
+
`KickLoad Watcher — ${status}\nEndpoint: ${endpoint}\nTested: ${testedAt} IST\n` +
|
|
128
|
+
(summary?.averageLatency != null ? `Latency: ${summary.averageLatency}ms\n` : "") +
|
|
129
|
+
(summary?.errorPercentage != null ? `Errors: ${summary.errorPercentage}%\n` : "") +
|
|
130
|
+
(summary?.throughput != null ? `Throughput: ${summary.throughput} req/s\n` : "") +
|
|
131
|
+
(durationSec != null ? `Duration: ${durationSec}s\n` : "") +
|
|
132
|
+
(downloadUrl ? `\nReport: ${downloadUrl}\n` : "") +
|
|
133
|
+
`\n— KickLoad Watcher`;
|
|
134
|
+
|
|
135
|
+
const info = await transporter.sendMail({
|
|
136
|
+
from: `"${config.email.fromName}" <${config.email.fromAddress}>`,
|
|
137
|
+
to: toEmail, subject, html, text,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`📧 Email sent → ${toEmail} (${info.messageId})`);
|
|
141
|
+
}
|
package/kickload-client.js
CHANGED
|
@@ -59,16 +59,20 @@ export class KickloadClient {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
if (response.status === 401 || response.status === 403) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Kickload authentication failed — check your API token or subscription"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (response.status === 504) {
|
|
69
|
+
throw new Error("Kickload server timeout (504)");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Kickload ${method} ${endpoint} → ${response.status} ${response.statusText}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
72
76
|
|
|
73
77
|
return parsed;
|
|
74
78
|
}
|
package/package.json
CHANGED
package/pipeline.js
CHANGED
|
@@ -352,7 +352,19 @@ return {
|
|
|
352
352
|
return { success: true, backendUrl, jmxFilename, taskId, jtlFilename: taskResult.result_file, pdfFilename, downloadUrl, summary };
|
|
353
353
|
|
|
354
354
|
} catch (err) {
|
|
355
|
-
|
|
355
|
+
if (err.message.includes("504") || err.message.includes("Gateway")) {
|
|
356
|
+
logger.error("❌ Kickload server is currently unavailable (504 timeout)");
|
|
357
|
+
logger.error(" This is a server-side issue, not your API");
|
|
358
|
+
logger.error(" Try again after some time\n");
|
|
359
|
+
} else if (err.message.includes("401") || err.message.includes("403")) {
|
|
360
|
+
logger.error("❌ Kickload authentication failed");
|
|
361
|
+
logger.error(" Possible reasons:");
|
|
362
|
+
logger.error(" • Invalid API token");
|
|
363
|
+
logger.error(" • Token expired");
|
|
364
|
+
logger.error(" • Subscription required\n");
|
|
365
|
+
} else {
|
|
366
|
+
logger.error(`Pipeline failed: ${err.message}`);
|
|
367
|
+
}
|
|
356
368
|
|
|
357
369
|
if (devEmail) {
|
|
358
370
|
await safeResultEmail({
|