Archived
mail 用例测试:发信留痕 / 厂商失败不抛 / 幂等键 / 附件来源 / from 对账号
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
/** 发信:先落 sending 再发;厂商失败落 failed 不抛;幂等键不发两封、失败可重发;附件取自 upload;from 对账号 */
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { MailAccountNotFoundError, MailNoDefaultAccountError, MailSecretMissingError, MailSendInvalidError } from '../contract.ts';
|
||||||
|
import { createMailService } from '../internal/usecase/mail-service.ts';
|
||||||
|
import { createPostalMimeParser } from '../internal/adapter/postal-mime-parser.ts';
|
||||||
|
import { account, fakeProvider, fixedClock, memoryAccounts, memoryAttachments, memoryInbound, memoryOutbound, plainSecrets, seqIds } from './fakes.ts';
|
||||||
|
|
||||||
|
function harness() {
|
||||||
|
const clock = fixedClock();
|
||||||
|
const accounts = memoryAccounts(clock);
|
||||||
|
const outbound = memoryOutbound();
|
||||||
|
const attachments = memoryAttachments();
|
||||||
|
const imap = fakeProvider('imap-smtp');
|
||||||
|
const wecom = fakeProvider('wecom');
|
||||||
|
const service = createMailService({
|
||||||
|
accounts,
|
||||||
|
inbound: memoryInbound(),
|
||||||
|
outbound,
|
||||||
|
attachments,
|
||||||
|
providers: { 'imap-smtp': imap, wecom },
|
||||||
|
mime: createPostalMimeParser(),
|
||||||
|
secrets: plainSecrets,
|
||||||
|
clock,
|
||||||
|
ids: seqIds('out'),
|
||||||
|
notify: { changed: () => undefined },
|
||||||
|
});
|
||||||
|
return { service, accounts, outbound, attachments, imap, clock };
|
||||||
|
}
|
||||||
|
const base = { to: ['b@acme.com'], cc: [], bcc: [], subject: '报价', text: '见附件', references: [], attachments: [], requestedBy: { kind: 'ops' as const } };
|
||||||
|
|
||||||
|
describe('send', () => {
|
||||||
|
it('默认账号发出:留痕 sent、Message-ID 是 <id@域>、发件人带显示名、返回 providerRef', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await h.accounts.save(account());
|
||||||
|
const result = await h.service.send(base);
|
||||||
|
expect(result).toEqual({ outboundId: 'out-1', accountId: 'acc-1', messageId: '<out-1@we.com>', state: 'sent', providerRef: 'fake:1' });
|
||||||
|
expect(h.imap.sent[0]?.from).toEqual({ address: 'sales@we.com', name: '销售部' });
|
||||||
|
expect(h.imap.sent[0]?.messageId).toBe('<out-1@we.com>');
|
||||||
|
const row = h.outbound.rows.get('out-1');
|
||||||
|
expect(row?.state).toBe('sent');
|
||||||
|
expect(row?.sentAt).toEqual(h.clock.now());
|
||||||
|
expect((await h.service.listOutbound({}))[0]).toMatchObject({ id: 'out-1', state: 'sent', to: ['b@acme.com'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('厂商失败:落 failed(原话脱敏)、不抛、返回 state=failed', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await h.accounts.save(account());
|
||||||
|
h.imap.failSendWith(Object.assign(new Error('Invalid login'), { response: '535 5.7.8 pw-1234 rejected' }));
|
||||||
|
const result = await h.service.send(base);
|
||||||
|
expect(result.state).toBe('failed');
|
||||||
|
expect(result.error).toBe('Invalid login —— 535 5.7.8 *** rejected');
|
||||||
|
expect(h.outbound.rows.get('out-1')?.state).toBe('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('幂等键:已 sent 的直接回原结果不再发;失败的那一行复用重发', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await h.accounts.save(account());
|
||||||
|
h.imap.failSendWith(new Error('暂时不行'));
|
||||||
|
const failed = await h.service.send({ ...base, idempotencyKey: 'k-1' });
|
||||||
|
expect(failed.state).toBe('failed');
|
||||||
|
h.imap.failSendWith(undefined);
|
||||||
|
const retried = await h.service.send({ ...base, idempotencyKey: 'k-1' });
|
||||||
|
expect(retried).toMatchObject({ outboundId: failed.outboundId, state: 'sent' });
|
||||||
|
const again = await h.service.send({ ...base, idempotencyKey: 'k-1' });
|
||||||
|
expect(again).toMatchObject({ outboundId: failed.outboundId, state: 'sent' });
|
||||||
|
expect(h.imap.sent).toHaveLength(1);
|
||||||
|
expect(h.outbound.rows.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('入参问题抛而不落行:无正文 400、没默认账号 412、from 对不上 404、凭证空 412', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await expect(h.service.send(base)).rejects.toBeInstanceOf(MailNoDefaultAccountError);
|
||||||
|
await h.accounts.save(account());
|
||||||
|
await expect(h.service.send({ ...base, text: undefined })).rejects.toBeInstanceOf(MailSendInvalidError);
|
||||||
|
await expect(h.service.send({ ...base, from: 'nobody@we.com' })).rejects.toBeInstanceOf(MailAccountNotFoundError);
|
||||||
|
await h.accounts.save(account({ secretCipher: null }));
|
||||||
|
await expect(h.service.send(base)).rejects.toBeInstanceOf(MailSecretMissingError);
|
||||||
|
expect(h.outbound.rows.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('附件:uploadRef 取 upload 的字节与类型;dataBase64 直接用;两者都没有 400', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await h.accounts.save(account());
|
||||||
|
const { ref } = await h.attachments.put({ name: 'q.pdf', contentType: 'application/pdf', bytes: new Uint8Array([1, 2]), note: '' });
|
||||||
|
await h.service.send({
|
||||||
|
...base,
|
||||||
|
attachments: [
|
||||||
|
{ name: 'q.pdf', contentType: 'application/octet-stream', uploadRef: ref },
|
||||||
|
{ name: 'n.txt', contentType: 'text/plain', dataBase64: Buffer.from('hi').toString('base64') },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(h.imap.sent[0]?.attachments.map((a) => [a.name, a.contentType, a.bytes.byteLength])).toEqual([
|
||||||
|
['q.pdf', 'application/pdf', 2],
|
||||||
|
['n.txt', 'text/plain', 2],
|
||||||
|
]);
|
||||||
|
await expect(h.service.send({ ...base, attachments: [{ name: 'x', contentType: 'text/plain' }] })).rejects.toBeInstanceOf(MailSendInvalidError);
|
||||||
|
await expect(h.service.send({ ...base, attachments: [{ name: 'x', contentType: 'text/plain', uploadRef: 'up-404' }] })).rejects.toBeInstanceOf(MailSendInvalidError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('from 指定发件地址(channel 用):按地址找账号,inReplyTo / references 原样交给 provider', async () => {
|
||||||
|
const h = harness();
|
||||||
|
await h.accounts.save(account({ isDefault: false }));
|
||||||
|
const result = await h.service.send({ ...base, from: 'SALES@we.com', inReplyTo: '<m1@acme.com>', references: ['<m1@acme.com>'], requestedBy: { kind: 'channel', taskId: 't-1' } });
|
||||||
|
expect(result.accountId).toBe('acc-1');
|
||||||
|
expect(h.imap.sent[0]?.inReplyTo).toBe('<m1@acme.com>');
|
||||||
|
expect(h.outbound.rows.get('out-1')?.requestedBy).toEqual({ kind: 'channel', taskId: 't-1' });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user