diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
| commit | d80640d01eeb84907e36ff4af33add9081eda17c (patch) | |
| tree | 27698b34e06e28d111c2196700015d9c82f78274 /test | |
| download | wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.tar.gz wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.zip | |
Diffstat (limited to 'test')
| -rw-r--r-- | test/compat.test.js | 25 | ||||
| -rw-r--r-- | test/fixtures.js | 30 | ||||
| -rw-r--r-- | test/fixtures/neomutt-adjunto.eml | 34 | ||||
| -rw-r--r-- | test/fixtures/neomutt-respuesta.eml | 16 | ||||
| -rw-r--r-- | test/format.test.js | 66 | ||||
| -rw-r--r-- | test/wamail-mime.test.js | 48 | ||||
| -rw-r--r-- | test/wamail-sync.test.js | 172 |
7 files changed, 391 insertions, 0 deletions
diff --git a/test/compat.test.js b/test/compat.test.js new file mode 100644 index 0000000..d5a3d65 --- /dev/null +++ b/test/compat.test.js @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import test from 'node:test'; +import { patchStructures } from '../src/compat.js'; +import * as ser from '../src/serialize.js'; + +const require = createRequire(import.meta.url); + +test('MsgKey with $1 (WA Web 2.3000.104x) recovers _serialized in Message and Reaction', () => { + patchStructures(); + patchStructures(); // idempotent + const S = require('whatsapp-web.js/src/structures/index.js'); + const key = { fromMe: false, remote: '5491122334455@c.us', id: '3EB0AAA1', self: 'in', $1: 'false_5491122334455@c.us_3EB0AAA1' }; + + const m = new S.Message(null, { id: { ...key }, body: 'hola', t: 1789500000, type: 'chat', from: key.remote, to: 'yo@c.us' }); + assert.equal(m.id._serialized, key.$1); + assert.equal(ser.message(m).id, key.$1); + + const r = new S.Reaction(null, { msgKey: { ...key, $1: 'x' }, parentMsgKey: { ...key }, reactionText: 'đ', timestamp: 1 }); + assert.equal(r.msgId._serialized, key.$1); + + // IDs that already have _serialized are left alone + const old = new S.Message(null, { id: { ...key, _serialized: 'old' }, body: '', t: 1, type: 'chat' }); + assert.equal(old.id._serialized, 'old'); +}); diff --git a/test/fixtures.js b/test/fixtures.js new file mode 100644 index 0000000..141b43b --- /dev/null +++ b/test/fixtures.js @@ -0,0 +1,30 @@ +// Sample serialized messages (the shape of src/serialize.js). +const base = { + fromMe: false, author: null, hasMedia: false, media: null, hasQuotedMsg: false, quotedStanzaId: null, + quotedParticipant: null, isForwarded: false, isStarred: false, isStatus: false, ack: 1, mentionedIds: [], links: [], +}; + +export const messages = [ + { + ...base, + id: 'false_5491122334455@c.us_3EB0AAA1', stanzaId: '3EB0AAA1', chatId: '5491122334455@c.us', chatName: 'Juan PĂ©rez', + from: '5491122334455@c.us', to: '5491199999999@c.us', sender: '5491122334455@c.us', senderName: 'Juan PĂ©rez', + timestamp: 1789500000, date: '2026-09-15T19:20:00.000Z', type: 'chat', + body: 'Hola! ÂżvenĂs mañana? đ\nFrom here on, una lĂnea muy larga ' + 'ñ'.repeat(120), + }, + { + ...base, + id: 'true_5491122334455@c.us_3EB0BBB2', stanzaId: '3EB0BBB2', chatId: '5491122334455@c.us', chatName: 'Juan PĂ©rez', + fromMe: true, from: '5491199999999@c.us', to: '5491122334455@c.us', sender: '5491199999999@c.us', senderName: 'Elvis', + timestamp: 1789500060, date: '2026-09-15T19:21:00.000Z', type: 'image', body: 'la foto', + hasMedia: true, hasQuotedMsg: true, quotedStanzaId: '3EB0AAA1', + media: { mimetype: 'image/png', filename: 'captura año.png', size: 70, data: Buffer.from('PNGDATA'.repeat(10)).toString('base64') }, + }, + { + ...base, + id: 'false_120363000000@g.us_3EB0CCC3_5491133334444@c.us', stanzaId: '3EB0CCC3', chatId: '120363000000@g.us', + chatName: 'Familia', from: '120363000000@g.us', to: '5491199999999@c.us', author: '5491133334444@c.us', + sender: '5491133334444@c.us', senderName: null, timestamp: 1789500120, date: '2026-09-15T19:22:00.000Z', + type: 'document', body: '', hasMedia: true, media: { mimetype: 'application/pdf', filename: 'boleta.pdf', size: 20480 }, + }, +]; diff --git a/test/fixtures/neomutt-adjunto.eml b/test/fixtures/neomutt-adjunto.eml new file mode 100644 index 0000000..6e989ee --- /dev/null +++ b/test/fixtures/neomutt-adjunto.eml @@ -0,0 +1,34 @@ +Date: Wed, 16 Sep 2026 21:57:55 -0300 +From: Yo Test <5491100000000@c.us> +To: Juan <5491122334455@c.us> +Subject: asunto =?utf-8?B?w7E=?= +Message-ID: <aqs7E1I6QJ7lxe7w@legion> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="xbbzzxoqus32yqz5" +Content-Disposition: inline +Content-Transfer-Encoding: 8bit + + +--xbbzzxoqus32yqz5 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Disposition: inline +Content-Transfer-Encoding: 8bit + +hola, ÂżquĂ© tal? ñandĂș đ + +> lĂnea citada +-- +firma + +--xbbzzxoqus32yqz5 +Content-Type: application/pdf +Content-Disposition: attachment; filename*=utf-8''informe%20a%C3%B1o%2Epdf +Content-Transfer-Encoding: quoted-printable + +PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF= +-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FA= +KE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-= +PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF= +-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FA= +KE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE-PDF-FAKE- +--xbbzzxoqus32yqz5-- diff --git a/test/fixtures/neomutt-respuesta.eml b/test/fixtures/neomutt-respuesta.eml new file mode 100644 index 0000000..bb05caa --- /dev/null +++ b/test/fixtures/neomutt-respuesta.eml @@ -0,0 +1,16 @@ +Date: Wed, 16 Sep 2026 22:00:18 -0300 +From: Yo Test <5491100000000@c.us> +To: 5491122334455@c.us, 120363000000@g.us +Subject: Re: hola +Message-ID: <aqs7olQst6WFfDsY@legion> +In-Reply-To: <3EB0AAA1@wa> +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Disposition: inline +Content-Transfer-Encoding: 8bit + +Dale, nos vemos mañana a las 10 en la facultad, llevo los apuntes y la calculadora que me pediste la otra vez + +On Wed, Juan wrote: +> ÂżvenĂs mañana? +> tipo 10 diff --git a/test/format.test.js b/test/format.test.js new file mode 100644 index 0000000..0eb5bf9 --- /dev/null +++ b/test/format.test.js @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import test from 'node:test'; +import { format } from '../src/format/index.js'; +import { messages } from './fixtures.js'; + +// Validates with the Python mail parser (the same kind of strict parser MUAs use). +function parseMbox(text) { + const py = ` +import sys, json, mailbox, email.policy, tempfile, os +f = tempfile.NamedTemporaryFile('w', delete=False, suffix='.mbox'); f.write(sys.stdin.read()); f.close() +out = [] +for m in mailbox.mbox(f.name, factory=lambda fp: email.message_from_binary_file(fp, policy=email.policy.default)): + body = m.get_body(('plain',)) + atts = [(a.get_filename(), a.get_content_type(), len(a.get_content())) for a in m.iter_attachments()] + out.append({k: str(m[k]) for k in ('From','To','Reply-To','Subject','Message-ID','In-Reply-To','X-WA-Id','X-WA-Chat-Name')} | {'body': body.get_content(), 'atts': atts, 'defects': [str(d) for d in m.defects]}) +os.unlink(f.name) +print(json.dumps(out))`; + return JSON.parse(execFileSync('python3', ['-c', py], { input: text, encoding: 'utf8' })); +} + +test('mbox: headers, QP body, attachments and threads', () => { + const parsed = parseMbox(format(messages, 'mbox') + '\n'); + assert.equal(parsed.length, 3); + const [a, b, c] = parsed; + + assert.equal(a.From, 'Juan PĂ©rez <5491122334455@c.us>'); + assert.equal(a['Message-ID'], '<3EB0AAA1@wa>'); + assert.equal(a.body.trimEnd(), messages[0].body); // QP round trip, including the "From " line + assert.match(a.Subject, /^Hola! ÂżvenĂs mañana\? đ$/); + assert.deepEqual(a.defects, []); + + assert.equal(b['In-Reply-To'], '<3EB0AAA1@wa>'); + assert.equal(b.To, 'Juan PĂ©rez <5491122334455@c.us>'); + assert.deepEqual(b.atts, [['captura año.png', 'image/png', 70]]); + assert.equal(b.body.trimEnd(), 'la foto'); + + assert.equal(c['Reply-To'], 'Familia <120363000000@g.us>'); + assert.equal(c.From, '5491133334444@c.us'); + assert.match(c.body, /attachment not downloaded: application\/pdf, 20 KB/); + assert.equal(c['X-WA-Id'], messages[2].id); +}); + +test('eml: a single message; with several it asks for mbox', () => { + assert.match(format(messages[0], 'eml'), /^From: =\?UTF-8\?B\?/); + assert.throws(() => format(messages, 'eml'), /use --format mbox/); + assert.throws(() => format([{ id: 1 }], 'mbox'), /only apply to messages/); +}); + +test('table / csv / yaml / text', () => { + process.stdout.columns = 80; + const t = format(messages, 'table', { columns: ['date', 'senderName', 'body'] }); + assert.ok(t.split('\n').every((l) => l.length <= 80), t); + assert.match(t.split('\n')[0], /^DATE\s+SENDERNAME\s+BODY$/); + + const csv = format([{ a: 'x,y', b: 'di "hola"', c: { d: 1 } }], 'csv', { columns: ['a', 'b', 'c.d'] }); + assert.equal(csv, 'a,b,c.d\n"x,y","di ""hola""",1'); + + assert.equal(format({ a: 'yes', b: [1, { c: 'x: y' }], e: {} }, 'yaml'), 'a: "yes"\nb:\n - 1\n - c: "x: y"\ne: {}'); + + const txt = format(messages, 'text'); + assert.match(txt, /\] Juan PĂ©rez: Hola!/); + assert.match(txt, /\] me: â© \[image captura año.png\] la foto/); + assert.match(txt, /\] 5491133334444@c.us: \[document boleta.pdf\]/); + assert.equal(format({ x: 1 }, 'ndjson'), '{"x":1}'); +}); diff --git a/test/wamail-mime.test.js b/test/wamail-mime.test.js new file mode 100644 index 0000000..a2ff7cb --- /dev/null +++ b/test/wamail-mime.test.js @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; +import { addresses, decodeWords, parseMessage, unflow } from '../src/wamail/mime.js'; + +const fixture = (name) => fs.readFileSync(new URL(`./fixtures/${name}`, import.meta.url)); + +test('neomutt: 8-bit flowed text + QP attachment with an RFC 2231 filename', () => { + const m = parseMessage(fixture('neomutt-adjunto.eml')); + assert.equal(m.header('Subject'), 'asunto ñ'); + assert.deepEqual(addresses(m.header('To')), ['5491122334455@c.us']); + assert.match(m.text, /^hola, ÂżquĂ© tal\? ñandĂș đ\n\n> lĂnea citada\n-- \nfirma/); + assert.equal(m.attachments.length, 1); + assert.equal(m.attachments[0].filename, 'informe año.pdf'); + assert.equal(m.attachments[0].contentType, 'application/pdf'); + assert.equal(m.attachments[0].data.toString(), 'PDF-FAKE-'.repeat(50)); +}); + +test('neomutt: reply with In-Reply-To and several recipients', () => { + const m = parseMessage(fixture('neomutt-respuesta.eml')); + assert.equal(m.header('In-Reply-To'), '<3EB0AAA1@wa>'); + assert.deepEqual(addresses(m.header('To')), ['5491122334455@c.us', '120363000000@g.us']); + assert.equal(m.attachments.length, 0); + assert.match(m.text, /la otra vez \nOn Wed, Juan wrote:\n> ÂżvenĂs mañana\?/); // "vez " + empty line = soft break +}); + +test('encoded-words, flowed and addresses', () => { + assert.equal(decodeWords('=?UTF-8?B?SnVhbiBQw6lyZXo=?= <x@c.us>'), 'Juan PĂ©rez <x@c.us>'); + assert.equal(decodeWords('=?utf-8?Q?a=C3=B1o?= =?utf-8?Q?_nuevo?='), 'año nuevo'); + assert.equal(unflow('una lĂnea \nlarga\n-- \nfirma'), 'una lĂnea larga\n-- \nfirma'); + assert.deepEqual(addresses('"PĂ©rez, Juan" <1@c.us>, 2@g.us'), ['1@c.us', '2@g.us']); +}); + +test('multipart with base64 and a latin1 charset', () => { + const raw = [ + 'To: 1@c.us', 'Subject: =?iso-8859-1?Q?caf=E9?=', 'MIME-Version: 1.0', + 'Content-Type: multipart/mixed; boundary="b1"', '', + '--b1', 'Content-Type: multipart/alternative; boundary="b2"', '', + '--b2', 'Content-Type: text/plain; charset=iso-8859-1', 'Content-Transfer-Encoding: quoted-printable', '', 'caf=E9 con leche', '--b2--', + '--b1', 'Content-Type: image/png; name="foto.png"', 'Content-Transfer-Encoding: base64', '', Buffer.from('PNG!').toString('base64'), + '--b1--', '', + ].join('\r\n'); + const m = parseMessage(raw); + assert.equal(m.header('subject'), 'cafĂ©'); + assert.equal(m.text, 'cafĂ© con leche'); + assert.equal(m.attachments[0].filename, 'foto.png'); + assert.equal(m.attachments[0].data.toString(), 'PNG!'); +}); diff --git a/test/wamail-sync.test.js b/test/wamail-sync.test.js new file mode 100644 index 0000000..909a42d --- /dev/null +++ b/test/wamail-sync.test.js @@ -0,0 +1,172 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +// Isolate state/cache/Maildir before importing (store.js reads XDG_* on load). +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wamail-test-')); +process.env.XDG_STATE_HOME = path.join(tmp, 'state'); +process.env.XDG_CACHE_HOME = path.join(tmp, 'cache'); +const { syncAccount, wamailConfig } = await import('../src/wamail/sync.js'); +const { loadState } = await import('../src/wamail/store.js'); +const { mailboxesPath } = await import('../src/wamail/neomutt.js'); +const { planSend } = await import('../src/wamail/send.js'); +const { parseMessage } = await import('../src/wamail/mime.js'); + +const ME = '5491100000000@c.us'; +const NOW = 1789500000; +const A = '5491122334455@c.us'; +const B = '5491166667777@c.us'; +const G = '120363000000@g.us'; + +function fakeWhatsApp() { + const chats = new Map(); + const msgs = new Map(); + const calls = []; + const msg = (chatId, stanza, ts, extra = {}) => { + const fromMe = Boolean(extra.fromMe); + const sender = fromMe ? ME : (extra.author ?? chatId); + const m = { + id: `${fromMe}_${chatId}_${stanza}`, stanzaId: stanza, chatId, chatName: chats.get(chatId)?.name ?? null, + fromMe, from: fromMe ? ME : chatId, to: fromMe ? chatId : ME, author: extra.author ?? null, sender, + senderName: fromMe ? 'Yo' : 'Otro', timestamp: ts, date: new Date(ts * 1000).toISOString(), type: 'chat', + body: '', hasMedia: false, media: null, hasQuotedMsg: false, quotedStanzaId: null, quotedParticipant: null, + isForwarded: false, isStarred: false, isStatus: false, ack: 1, mentionedIds: [], links: [], ...extra, + }; + (msgs.get(chatId) ?? msgs.set(chatId, []).get(chatId)).push(m); + const c = chats.get(chatId); + c.timestamp = Math.max(c.timestamp, ts); + return m; + }; + const rpc = async (method, p = {}) => { + calls.push([method, p]); + const list = (id) => msgs.get(id) ?? []; + switch (method) { + case 'auth.status': return { me: { id: ME, name: 'Yo' } }; + case 'chats.list': return [...chats.values()].map((c) => ({ ...c })); + case 'chats.get': return { ...chats.get(p.chatId) }; + case 'messages.list': { + const l = list(p.chatId); + return p.since !== undefined ? l.filter((m) => m.timestamp >= p.since) : l.slice(-p.limit); + } + case 'messages.get': { + const m = [...msgs.values()].flat().find((x) => x.id === p.messageId); + return p.includeMedia && m.hasMedia ? { ...m, media: { ...m.media, data: Buffer.from('IMG').toString('base64') } } : m; + } + case 'chats.read': chats.get(p.chatId).unreadCount = 0; return { ok: true }; + case 'chats.unread': chats.get(p.chatId).unreadCount = -1; return { ok: true }; + case 'messages.star': case 'messages.unstar': return { ok: true }; + default: throw new Error(`rpc inesperado ${method}`); + } + }; + const chat = (id, name, extra = {}) => chats.set(id, { id, name, isGroup: id.endsWith('@g.us'), unreadCount: 0, timestamp: 0, archived: false, pinned: false, ...extra }); + return { chats, msgs, calls, rpc, msg, chat, called: (m) => calls.filter(([x]) => x === m) }; +} + +const config = wamailConfig('t', { maildir: path.join(tmp, 'Mail') }); +const files = (dirName) => { + const d = path.join(tmp, 'Mail', dirName); + return ['new', 'cur'].flatMap((sub) => (fs.existsSync(path.join(d, sub)) ? fs.readdirSync(path.join(d, sub)).map((f) => `${sub}/${f}`) : [])).sort(); +}; +const run = (wa, opts = {}) => syncAccount({ account: 't', rpc: wa.rpc, config, now: () => NOW * 1000, ...opts }); + +test('full sync scenario', async () => { + const wa = fakeWhatsApp(); + wa.chat(A, 'Juan PĂ©rez'); + wa.chat(B, 'Viejo'); + wa.chat(G, 'Familia `rm -rf` $HOME', { pinned: false }); + wa.msg(A, 'M1', NOW - 300, { body: 'hola' }); + wa.msg(A, 'M2', NOW - 200, { fromMe: true, body: 'quĂ© tal' }); + wa.msg(A, 'M3', NOW - 100, { type: 'image', hasMedia: true, media: { mimetype: 'image/jpeg', filename: 'foto.jpg', size: 3 } }); + wa.chats.get(A).unreadCount = 1; + wa.msg(B, 'OLD', NOW - 60 * 86400, { body: 'hace dos meses' }); + wa.msg(G, 'G1', NOW - 50, { author: A, body: 'en el grupo' }); + wa.msg(G, 'N1', NOW - 40, { type: 'e2e_notification' }); + + // 1. first sync: backfill of recent chats, flags from unreadCount + let stats = await run(wa); + let st = loadState('t'); + const dirA = st.chats[A].dir; + assert.equal(stats.nuevos, 4); + assert.equal(st.chats[B].dir, null, 'old chat not backfilled'); + assert.deepEqual(files(dirA).map((f) => f.replace(/^\w+\/\d+\./, '')), ['M1.wa:2,S', 'M2.wa:2,S', 'M3.wa'].sort()); + assert.ok(files(dirA).includes(`new/${NOW - 100}.M3.wa`)); + const eml = fs.readFileSync(path.join(tmp, 'Mail', dirA, 'new', `${NOW - 100}.M3.wa`)); + assert.equal(parseMessage(eml).attachments[0].filename, 'foto.jpg'); + assert.ok(!files(st.chats[G].dir).some((f) => f.includes('N1')), 'notifications left out'); + const mbox = fs.readFileSync(mailboxesPath('t'), 'utf8'); + assert.match(mbox, /named-mailboxes ' Familia rm -rf HOME'/, 'sanitized name'); + assert.doesNotMatch(mbox, /Viejo/); + assert.match(mbox, new RegExp(`spoolfile = '.*${st.chats[G].dir}'`), 'spoolfile = most recent chat'); + assert.match(mbox, /alternates '\^5491100000000@c\\.us\$'/); + + // 2. reading M3 in neomutt -> chats.read; the next sync does not mark it again + const d = path.join(tmp, 'Mail', dirA); + fs.renameSync(path.join(d, 'new', `${NOW - 100}.M3.wa`), path.join(d, 'cur', `${NOW - 100}.M3.wa:2,S`)); + await run(wa); + assert.equal(wa.called('chats.read').length, 1); + await run(wa); + assert.equal(wa.called('chats.read').length, 1, 'does not repeat'); + assert.ok(files(dirA).includes(`cur/${NOW - 100}.M3.wa:2,S`)); + + // 3. M4 arrives (hook: --chat) -> new/ + wa.msg(A, 'M4', NOW - 10, { body: 'nuevo' }); + wa.chats.get(A).unreadCount = 1; + stats = await run(wa, { onlyChat: A }); + assert.equal(stats.nuevos, 1); + assert.ok(files(dirA).includes(`new/${NOW - 10}.M4.wa`)); + + // 4. read on the phone -> marked read locally without calling WhatsApp + wa.chats.get(A).unreadCount = 0; + await run(wa); + assert.ok(files(dirA).includes(`cur/${NOW - 10}.M4.wa:2,S`)); + assert.equal(wa.called('chats.read').length, 1); + + // 5. star M1, delete M2 (not downloaded again), mark M1 unread + fs.renameSync(path.join(d, 'cur', `${NOW - 300}.M1.wa:2,S`), path.join(d, 'cur', `${NOW - 300}.M1.wa:2,F`)); + fs.rmSync(path.join(d, 'cur', `${NOW - 200}.M2.wa:2,S`)); + stats = await run(wa); + assert.deepEqual(wa.called('messages.star').map(([, p]) => p.messageId), [`false_${A}_M1`]); + assert.equal(wa.called('chats.unread').length, 1); + assert.equal(stats.borrados, 1); + st = loadState('t'); + st.chats[A].lastTs = 0; // force a re-backfill: M2 is in tombstones + fs.writeFileSync(path.join(tmp, 'state', 'wamail', 't.json'), JSON.stringify(st)); + await run(wa); + assert.ok(!files(dirA).some((f) => f.includes('.M2.')), 'deleted message does not come back'); + assert.ok(files(dirA).includes(`cur/${NOW - 300}.M1.wa:2,F`), 'still unread after the remote unread (-1)'); + + // 6. partially read: no push, and the next sync does not overwrite it + wa.msg(A, 'M5', NOW - 5, { body: 'a' }); + wa.msg(A, 'M6', NOW - 4, { body: 'b' }); + wa.chats.get(A).unreadCount = 3; + await run(wa); + const readsBefore = wa.called('chats.read').length; + fs.renameSync(path.join(d, 'new', `${NOW - 5}.M5.wa`), path.join(d, 'cur', `${NOW - 5}.M5.wa:2,S`)); + await run(wa); + await run(wa); + assert.equal(wa.called('chats.read').length, readsBefore); + assert.ok(files(dirA).includes(`cur/${NOW - 5}.M5.wa:2,S`)); +}); + +test('send: quoted reply to a group, attachment as a document, new message with ">"', () => { + const state = { msgs: { '3EB0AAA1': { chat: G, id: `false_${G}_3EB0AAA1_${A}` } }, tombstones: {} }; + const fixture = (n) => fs.readFileSync(new URL(`./fixtures/${n}`, import.meta.url)); + + const reply = planSend({ raw: fixture('neomutt-respuesta.eml'), argv: ['--', A, G], state }); + assert.deepEqual(reply.chatIds, [G], 'group-reply goes to the group only'); + assert.equal(reply.steps.length, 1); + assert.equal(reply.steps[0].quotedMessageId, `false_${G}_3EB0AAA1_${A}`); + assert.doesNotMatch(reply.steps[0].body, /wrote:|>/); + assert.match(reply.steps[0].body, /^Dale, nos vemos mañana/); + + const withFile = planSend({ raw: fixture('neomutt-adjunto.eml'), argv: ['--', A], state }); + assert.equal(withFile.steps.length, 2); + assert.equal(withFile.steps[0].body, 'hola, ÂżquĂ© tal? ñandĂș đ\n\n> lĂnea citada', 'without In-Reply-To it is not a quote; signature left out'); + assert.equal(withFile.steps[1].attachment.filename, 'informe año.pdf'); + + assert.throws(() => planSend({ raw: 'To: juan@gmail.com\n\nhola', argv: [], state }), /is not a WhatsApp recipient/); +}); + +test.after(() => fs.rmSync(tmp, { recursive: true, force: true })); |