Files
TREK/server/tests/unit/nest/smtp-diagnostics.test.ts
T
2026-10-06 21:50:59 +02:00

202 lines
7.7 KiB
TypeScript

/**
* smtp-diagnostics.test.ts
*
* The wording an admin gets when a test email does not go out (#2196). Pure
* functions, no socket and no DB: what is pinned here is that every common SMTP
* failure comes back as a cause rather than as "failed", and that the credential
* never rides along.
*/
import {
describeSmtpFailure,
describeSmtpGap,
parseSmtpPort,
type SmtpTarget,
} from '../../../src/nest/notifications/mailer/smtp-diagnostics';
import { describe, it, expect } from 'vitest';
const PLAIN: SmtpTarget = { host: 'mail.example.com', port: 587, secure: false };
const IMPLICIT_TLS: SmtpTarget = { host: 'mail.example.com', port: 465, secure: true };
/** The nodemailer shape: an Error carrying the SMTP code and the relay's reply. */
function smtpError(message: string, code?: string): Error {
return code ? Object.assign(new Error(message), { code }) : new Error(message);
}
describe('parseSmtpPort', () => {
it('SMTPDIAG-001: accepts a port number, with or without surrounding space', () => {
expect(parseSmtpPort('587')).toBe(587);
expect(parseSmtpPort(' 465 ')).toBe(465);
expect(parseSmtpPort('1')).toBe(1);
expect(parseSmtpPort('65535')).toBe(65535);
});
it('SMTPDIAG-002: refuses everything that would reach net.connect as NaN or out of range', () => {
expect(parseSmtpPort(null)).toBeNull();
expect(parseSmtpPort(undefined)).toBeNull();
expect(parseSmtpPort('')).toBeNull();
expect(parseSmtpPort('smtp.example.com')).toBeNull();
expect(parseSmtpPort('587abc')).toBeNull();
expect(parseSmtpPort('58.7')).toBeNull();
expect(parseSmtpPort('-1')).toBeNull();
expect(parseSmtpPort('0')).toBeNull();
expect(parseSmtpPort('65536')).toBeNull();
});
});
describe('describeSmtpGap', () => {
it('SMTPDIAG-003: lists every field that is still empty', () => {
const gap = describeSmtpGap({ host: null, port: null, from: null });
expect(gap).toContain('SMTP_HOST');
expect(gap).toContain('SMTP_PORT');
expect(gap).toContain('SMTP_FROM');
});
it('SMTPDIAG-004: names only the one that is missing', () => {
const gap = describeSmtpGap({ host: 'mail.example.com', port: '587', from: null });
expect(gap).toContain('SMTP_FROM');
expect(gap).not.toContain('SMTP_HOST');
});
it('SMTPDIAG-005: a port that is not a port is reported as such, not as absent', () => {
const gap = describeSmtpGap({ host: 'mail.example.com', port: 'ssl', from: null });
expect(gap).toContain('"ssl"');
expect(gap).toContain('65535');
expect(gap).not.toContain('missing');
});
it('SMTPDIAG-006: an absurd port value is clipped before it is echoed back', () => {
const gap = describeSmtpGap({ host: 'mail.example.com', port: 'x'.repeat(500), from: 'a@b.c' });
expect(gap.length).toBeLessThan(120);
expect(gap).toContain('...');
});
});
describe('describeSmtpFailure', () => {
it('SMTPDIAG-007: a rejected login points at the credentials and keeps the relay reply', () => {
const failure = describeSmtpFailure(
smtpError('Invalid login: 535 5.7.8 Username and Password not accepted', 'EAUTH'),
PLAIN,
);
expect(failure.code).toBe('EAUTH');
expect(failure.reason).toContain('rejected the credentials');
expect(failure.reason).toContain('535 5.7.8');
});
it('SMTPDIAG-008: an unresolvable host is named as DNS', () => {
const byCode = describeSmtpFailure(smtpError('getaddrinfo failed', 'EDNS'), PLAIN);
const byMessage = describeSmtpFailure(smtpError('getaddrinfo ENOTFOUND mail.example.com', 'ESOCKET'), PLAIN);
expect(byCode.reason).toContain('could not be resolved');
expect(byMessage.reason).toContain('could not be resolved');
});
it('SMTPDIAG-009: a refused connection separates "closed port" from "wrong password"', () => {
const failure = describeSmtpFailure(smtpError('connect ECONNREFUSED 10.0.0.5:587', 'ESOCKET'), PLAIN);
expect(failure.reason).toContain('refused the connection');
expect(failure.reason).toContain('mail.example.com:587');
});
it('SMTPDIAG-010: an unroutable relay is reported as unreachable', () => {
const failure = describeSmtpFailure(smtpError('connect EHOSTUNREACH 10.0.0.5:587', 'ESOCKET'), PLAIN);
expect(failure.reason).toContain('unreachable');
});
it('SMTPDIAG-011: a stalled greeting on 465 suggests the STARTTLS port', () => {
const failure = describeSmtpFailure(smtpError('Greeting never received', 'ETIMEDOUT'), IMPLICIT_TLS);
expect(failure.reason).toContain('did not answer in time');
expect(failure.reason).toContain('587');
});
it('SMTPDIAG-012: a connection timeout on 587 suggests the implicit-TLS port', () => {
const failure = describeSmtpFailure(smtpError('Connection timeout', 'ETIMEDOUT'), PLAIN);
expect(failure.reason).toContain('465');
});
it('SMTPDIAG-013: an untrusted certificate points at the skip-verification switch', () => {
const failure = describeSmtpFailure(smtpError('unable to verify the first certificate', 'ESOCKET'), IMPLICIT_TLS);
expect(failure.reason).toContain('Skip TLS certificate check');
});
it('SMTPDIAG-014: a handshake against a plain-text port is named as TLS', () => {
const failure = describeSmtpFailure(
smtpError('140A:error:0A00010B:SSL routines:ssl3_get_record:wrong version number', 'ESOCKET'),
IMPLICIT_TLS,
);
expect(failure.reason).toContain('TLS handshake');
expect(failure.reason).toContain('587');
});
it('SMTPDIAG-015: a rejected envelope points at the from address', () => {
const failure = describeSmtpFailure(
smtpError('Mail command failed: 550 5.7.1 Sender address rejected', 'EENVELOPE'),
PLAIN,
);
expect(failure.reason).toContain('from address');
expect(failure.reason).toContain('550 5.7.1');
});
it('SMTPDIAG-016: a rejected message says the connection itself was fine', () => {
const failure = describeSmtpFailure(smtpError('Message failed: 552 too large', 'EMESSAGE'), PLAIN);
expect(failure.reason).toContain('rejected the message');
});
it('SMTPDIAG-017: an unclassified failure still carries the code and the message', () => {
const failure = describeSmtpFailure(smtpError('something odd happened', 'EPROTOCOL'), PLAIN);
expect(failure.code).toBe('EPROTOCOL');
expect(failure.reason).toContain('EPROTOCOL');
expect(failure.reason).toContain('something odd happened');
});
it('SMTPDIAG-018: a throw that is not an Error is still described', () => {
const failure = describeSmtpFailure('boom', PLAIN);
expect(failure.code).toBe('UNKNOWN');
expect(failure.reason).toContain('boom');
});
it('SMTPDIAG-019: the password is redacted wherever it turns up in the message', () => {
const failure = describeSmtpFailure(
smtpError('Invalid login for user with correct-horse-battery', 'EAUTH'),
PLAIN,
'correct-horse-battery',
);
expect(failure.reason).not.toContain('correct-horse-battery');
expect(failure.reason).toContain('***');
});
it('SMTPDIAG-020: a secret too short to be one is left alone rather than blanking the text', () => {
const failure = describeSmtpFailure(smtpError('bad login at a b c', 'EAUTH'), PLAIN, 'a b');
expect(failure.reason).toContain('bad login at a b c');
});
it('SMTPDIAG-021: a runaway relay reply is collapsed and clipped', () => {
const failure = describeSmtpFailure(smtpError(`550 ${'x'.repeat(2000)}`, 'EENVELOPE'), PLAIN);
expect(failure.reason.length).toBeLessThan(700);
expect(failure.reason).toContain('...');
});
it('SMTPDIAG-022: newlines in a multi-line reply become one readable line', () => {
const failure = describeSmtpFailure(smtpError('550 first\n 550 second', 'EENVELOPE'), PLAIN);
expect(failure.reason).toContain('550 first 550 second');
});
});