Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
1024 lines
40 KiB
JavaScript
1024 lines
40 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { EventEmitter } from 'node:events';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { createWhatsAppManager, WHATSAPP_STATES } from '../src/whatsapp/manager.mjs';
|
|
|
|
async function fixture(options = {}) {
|
|
const authDir = await fs.mkdtemp(path.join(os.tmpdir(), 'whatsapp-manager-'));
|
|
const sockets = [];
|
|
let loads = 0;
|
|
const module = {
|
|
DisconnectReason: { restartRequired: 515, loggedOut: 401, badSession: 500, forbidden: 403, multideviceMismatch: 411, connectionReplaced: 440 },
|
|
useMultiFileAuthState: async () => ({ state: { creds: {} }, saveCreds: async () => {} }),
|
|
default: (config) => {
|
|
const ev = new EventEmitter();
|
|
const socket = {
|
|
ev,
|
|
config,
|
|
sent: [],
|
|
onWhatsApp: async (jid) => [{ jid: `${String(jid).replace(/@s\.whatsapp\.net$/, '')}@s.whatsapp.net`, exists: true }],
|
|
sendMessage: async (...args) => {
|
|
socket.sent.push(args);
|
|
return { key: { id: `message-${socket.sent.length}` } };
|
|
},
|
|
end: () => {},
|
|
logout: async () => {}
|
|
};
|
|
sockets.push(socket);
|
|
return socket;
|
|
}
|
|
};
|
|
const manager = createWhatsAppManager({
|
|
authDir,
|
|
loadBaileys: async () => { loads += 1; return module; },
|
|
qrTtlMs: 20,
|
|
reconnectDelayMs: 0,
|
|
...options
|
|
});
|
|
return { authDir, manager, module, sockets, get loads() { return loads; } };
|
|
}
|
|
|
|
const tick = () => new Promise((resolve) => setTimeout(resolve, 5));
|
|
|
|
async function waitFor(predicate, timeoutMs = 500) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (!await predicate()) {
|
|
if (Date.now() >= deadline) throw new Error('condicao nao atendida dentro do prazo');
|
|
await tick();
|
|
}
|
|
}
|
|
|
|
test('manager carrega Baileys somente ao conectar e evita conexoes duplicadas', async () => {
|
|
const f = await fixture();
|
|
assert.equal(f.loads, 0);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.STOPPED);
|
|
const first = f.manager.connect();
|
|
const second = f.manager.connect();
|
|
assert.strictEqual(first, second);
|
|
await first;
|
|
assert.equal(f.loads, 1);
|
|
assert.equal(f.sockets.length, 1);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.STARTING);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('manager bloqueia dump criptografico do libsignal sem silenciar outros logs', async () => {
|
|
const originalInfo = console.info;
|
|
const forwarded = [];
|
|
console.info = (...args) => forwarded.push(args);
|
|
try {
|
|
const f = await fixture();
|
|
console.info('Closing session:', { currentRatchet: { rootKey: Buffer.from('segredo') } });
|
|
console.info('log seguro', 1);
|
|
assert.deepEqual(forwarded, [['log seguro', 1]]);
|
|
await f.manager.close();
|
|
assert.notStrictEqual(console.info, originalInfo);
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
} finally {
|
|
console.info = originalInfo;
|
|
}
|
|
});
|
|
|
|
test('QR fica somente em memoria, altera estado e expira', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { qr: 'segredo-qr' });
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.QR_REQUIRED);
|
|
assert.equal(f.manager.getQr(), 'segredo-qr');
|
|
assert.equal(JSON.stringify(f.manager.getStatus()).includes(f.authDir), false);
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
assert.equal(f.manager.getQr(), null);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('connection.update abre sessao e sendText serializa envios para JID de usuario', async () => {
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.sendMessage = async (...args) => {
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
active -= 1;
|
|
socket.sent.push(args);
|
|
return { key: { id: `id-${socket.sent.length}` } };
|
|
};
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.READY);
|
|
|
|
const results = await Promise.all([
|
|
f.manager.sendText({ number: '+55 (11) 99999-0001', message: 'um', externalKey: 'ext-1' }),
|
|
f.manager.sendText({ number: '5511999990002', message: 'dois', externalKey: 'ext-2' })
|
|
]);
|
|
assert.equal(maxActive, 1);
|
|
assert.deepEqual(results, [
|
|
{ messageId: 'id-1', resolvedNumber: '5511999990001', variant: 'exata' },
|
|
{ messageId: 'id-2', resolvedNumber: '5511999990002', variant: 'exata' }
|
|
]);
|
|
assert.deepEqual(socket.sent[0], ['5511999990001@s.whatsapp.net', { text: 'um' }]);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText rejeita numero que nao existe no WhatsApp antes de reservar ou enviar', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.onWhatsApp = async () => [];
|
|
let reserved = false;
|
|
|
|
await assert.rejects(f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
beforeSend: () => { reserved = true; }
|
|
}), /nao esta cadastrado no WhatsApp/i);
|
|
assert.equal(reserved, false);
|
|
assert.equal(socket.sent.length, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText consulta numero movel brasileiro com e sem nono digito', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
const queries = [];
|
|
socket.onWhatsApp = async (...numbers) => {
|
|
queries.push(numbers);
|
|
return [{ jid: '551198765432@s.whatsapp.net', exists: true }];
|
|
};
|
|
|
|
await f.manager.sendText({ number: '5511998765432', message: 'oi' });
|
|
assert.deepEqual(queries[0], ['5511998765432', '551198765432']);
|
|
assert.equal(socket.sent[0][0], '551198765432@s.whatsapp.net');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText tenta adicionar nono digito apenas em movel brasileiro legado', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
const queries = [];
|
|
socket.onWhatsApp = async (...numbers) => {
|
|
queries.push(numbers);
|
|
return numbers.length === 2 ? [{ jid: `${numbers[1]}@s.whatsapp.net`, exists: true }] : [];
|
|
};
|
|
|
|
await f.manager.sendText({ number: '551198765432', message: 'movel' });
|
|
assert.deepEqual(queries[0], ['551198765432', '5511998765432']);
|
|
await assert.rejects(f.manager.sendText({ number: '551134567890', message: 'fixo' }), /nao esta cadastrado/i);
|
|
assert.deepEqual(queries[1], ['551134567890']);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText prefere correspondencia exata quando as duas variantes existem', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.onWhatsApp = async () => [
|
|
{ jid: '551198765432@s.whatsapp.net', exists: true },
|
|
{ jid: '5511998765432@s.whatsapp.net', exists: true }
|
|
];
|
|
await f.manager.sendText({ number: '5511998765432', message: 'oi' });
|
|
assert.equal(socket.sent[0][0], '5511998765432@s.whatsapp.net');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('checkNumber diagnostica variantes sem enviar mensagem', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.onWhatsApp = async (...numbers) => [{ jid: `${numbers[1]}@s.whatsapp.net`, exists: true }];
|
|
const result = await f.manager.checkNumber('5575999091780');
|
|
assert.deepEqual(result, {
|
|
numero: '5575999091780',
|
|
status: 'confirmado',
|
|
registered: true,
|
|
resolvedNumber: '557599091780',
|
|
variant: 'sem_nono_digito',
|
|
candidatesChecked: 2
|
|
});
|
|
assert.equal(socket.sent.length, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('checkNumber retorna metadados solicitados quando numero nao esta cadastrado', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.onWhatsApp = async () => [];
|
|
|
|
assert.deepEqual(await f.manager.checkNumber('+55 (11) 99999-0001'), {
|
|
numero: '5511999990001',
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
});
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('manager ignora JID existente fora das variantes solicitadas e nao envia', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.onWhatsApp = async () => [{ jid: '5511888880000@s.whatsapp.net', exists: true }];
|
|
|
|
const checked = await f.manager.checkNumber('5511999990001');
|
|
let reserved = false;
|
|
await assert.rejects(f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
beforeSend: () => { reserved = true; }
|
|
}), /nao esta cadastrado/i);
|
|
|
|
assert.deepEqual(checked, {
|
|
numero: '5511999990001',
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
});
|
|
assert.equal(reserved, false);
|
|
assert.equal(socket.sent.length, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('checkNumber limita lookup e observa rejeicao tardia do provedor', async () => {
|
|
const f = await fixture({ lookupTimeoutMs: 10 });
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
let rejectLookup;
|
|
socket.onWhatsApp = () => new Promise((resolve, reject) => { rejectLookup = reject; });
|
|
|
|
await assert.rejects(f.manager.checkNumber('5511999990001'), (error) => {
|
|
assert.equal(error.statusCode, 504);
|
|
assert.equal(error.code, 'WHATSAPP_LOOKUP_TIMEOUT');
|
|
assert.equal(error.numero, '5511999990001');
|
|
assert.equal(error.status, 'erro_temporario');
|
|
assert.equal(error.resolvedNumber, null);
|
|
assert.equal(error.variant, null);
|
|
assert.equal(error.candidatesChecked, 2);
|
|
return true;
|
|
});
|
|
rejectLookup(new Error('lookup rejeitado depois do timeout'));
|
|
await tick();
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('messages.update publica ACK, entrega e leitura para persistencia', async () => {
|
|
const updates = [];
|
|
const f = await fixture({ onMessageStatus: (event) => updates.push(event) });
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.ev.emit('messages.update', [
|
|
{ key: { id: 'msg-1', remoteJid: '5511999990001@s.whatsapp.net', fromMe: true }, update: { status: 2 } },
|
|
{ key: { id: 'msg-2', remoteJid: '5511999990002@s.whatsapp.net', fromMe: true }, update: { status: 3 } },
|
|
{ key: { id: 'msg-3', remoteJid: '5511999990003@s.whatsapp.net', fromMe: true }, update: { status: 4 } }
|
|
]);
|
|
await waitFor(() => updates.length === 3);
|
|
assert.deepEqual(updates.map(({ messageId, status }) => ({ messageId, status })), [
|
|
{ messageId: 'msg-1', status: 'enviado' },
|
|
{ messageId: 'msg-2', status: 'entregue' },
|
|
{ messageId: 'msg-3', status: 'lido' }
|
|
]);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('messages.upsert publica uma vez somente mensagens inbound novas de conversa individual', async () => {
|
|
const inbound = [];
|
|
const f = await fixture({ onInboundMessage: (event) => inbound.push(event) });
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
|
|
socket.ev.emit('messages.upsert', {
|
|
type: 'append',
|
|
messages: [{ key: { id: 'history-1', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }]
|
|
});
|
|
socket.ev.emit('messages.upsert', {
|
|
type: 'notify',
|
|
messages: [
|
|
{ key: { id: 'inbound-1', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 },
|
|
{ key: { id: 'self-1', remoteJid: '5575999091780@s.whatsapp.net', fromMe: true }, messageTimestamp: 1785326400 },
|
|
{ key: { id: 'group-1', remoteJid: '120363000000000000@g.us', fromMe: false }, messageTimestamp: 1785326400 },
|
|
{ key: { id: 'broadcast-1', remoteJid: 'newsletter@broadcast', fromMe: false }, messageTimestamp: 1785326400 },
|
|
{ key: { id: 'status-1', remoteJid: 'status@broadcast', fromMe: false }, messageTimestamp: 1785326400 },
|
|
{ key: { remoteJid: '5575999091781@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }
|
|
]
|
|
});
|
|
socket.ev.emit('messages.upsert', {
|
|
type: 'notify',
|
|
messages: [{ key: { id: 'inbound-1', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326460 }]
|
|
});
|
|
|
|
await waitFor(() => inbound.length === 1);
|
|
assert.deepEqual(inbound, [{
|
|
messageId: 'inbound-1',
|
|
jid: '5575999091780@s.whatsapp.net',
|
|
number: '5575999091780',
|
|
receivedAt: '2026-07-29T12:00:00.000Z'
|
|
}]);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
// O Set de dedup so era esvaziado inteiro no disconnect: sem um teto,
|
|
// crescia sem limite proporcional ao volume total de mensagens recebidas
|
|
// durante a vida da conexao. Envia 1 mensagem a mais que o teto interno
|
|
// (5000) e confirma que a mais antiga foi descartada (tratada como nova de
|
|
// novo), provando que o Set nao cresce indefinidamente.
|
|
test('dedup de mensagens inbound tem teto e descarta a mais antiga', async () => {
|
|
const inbound = [];
|
|
const f = await fixture({ onInboundMessage: (event) => inbound.push(event) });
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
|
|
const TOTAL = 5001;
|
|
for (let i = 0; i < TOTAL; i++) {
|
|
socket.ev.emit('messages.upsert', {
|
|
type: 'notify',
|
|
messages: [{ key: { id: `msg-${i}`, remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }]
|
|
});
|
|
}
|
|
await waitFor(() => inbound.length === TOTAL, 5000);
|
|
|
|
socket.ev.emit('messages.upsert', {
|
|
type: 'notify',
|
|
messages: [{ key: { id: 'msg-0', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }]
|
|
});
|
|
await waitFor(() => inbound.length === TOTAL + 1, 2000);
|
|
assert.equal(inbound.filter((e) => e.messageId === 'msg-0').length, 2);
|
|
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('falha ao persistir inbound nao escapa do EventEmitter nem gera resposta automatica', async () => {
|
|
const logs = [];
|
|
const f = await fixture({
|
|
onInboundMessage: () => { throw new Error('inbound sqlite fechado'); },
|
|
log: (message) => logs.push(message)
|
|
});
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
|
|
assert.doesNotThrow(() => socket.ev.emit('messages.upsert', {
|
|
type: 'notify',
|
|
messages: [{ key: { id: 'inbound-error', remoteJid: '5575999091780@s.whatsapp.net', fromMe: false }, messageTimestamp: 1785326400 }]
|
|
}));
|
|
await waitFor(() => logs.some((message) => message.includes('inbound sqlite fechado')));
|
|
assert.equal(socket.sent.length, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('erro sincrono ao persistir ACK nao escapa do EventEmitter', async () => {
|
|
const logs = [];
|
|
const f = await fixture({
|
|
onMessageStatus: () => { throw new Error('sqlite fechado'); },
|
|
log: (message) => logs.push(message)
|
|
});
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
assert.doesNotThrow(() => socket.ev.emit('messages.update', [
|
|
{ key: { id: 'sync-error', remoteJid: '5511999990001@s.whatsapp.net', fromMe: true }, update: { status: 2 } }
|
|
]));
|
|
await waitFor(() => logs.some((message) => message.includes('sqlite fechado')));
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('ACK recebido antes da persistencia do messageId e reaplicado depois', async () => {
|
|
let persisted = false;
|
|
const calls = [];
|
|
const f = await fixture({
|
|
onMessageStatus: (event) => {
|
|
calls.push(event);
|
|
return persisted;
|
|
}
|
|
});
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
socket.sendMessage = async () => {
|
|
socket.ev.emit('messages.update', [
|
|
{ key: { id: 'early-1', remoteJid: '5511999990001@s.whatsapp.net', fromMe: true }, update: { status: 2 } }
|
|
]);
|
|
return { key: { id: 'early-1' } };
|
|
};
|
|
await f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
afterSend: () => { persisted = true; return true; }
|
|
});
|
|
assert.equal(calls.length, 2);
|
|
assert.equal(calls[1].messageId, 'early-1');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('badSession generico preserva credenciais para recuperacao explicita', async () => {
|
|
const f = await fixture();
|
|
await fs.writeFile(path.join(f.authDir, 'creds.json'), '{}');
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: f.module.DisconnectReason.badSession } } }
|
|
});
|
|
await waitFor(() => f.manager.getStatus().state === WHATSAPP_STATES.ERROR);
|
|
assert.equal((await fs.stat(path.join(f.authDir, 'creds.json'))).isFile(), true);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('connect aguarda limpeza fatal da sessao anterior', async () => {
|
|
const f = await fixture();
|
|
await fs.writeFile(path.join(f.authDir, 'creds.json'), '{}');
|
|
await f.manager.connect();
|
|
let releaseRemoval;
|
|
const originalRm = fs.rm;
|
|
fs.rm = async (target, options) => {
|
|
if (path.resolve(target) !== path.resolve(f.authDir)) return originalRm(target, options);
|
|
await new Promise((resolve) => { releaseRemoval = resolve; });
|
|
return originalRm(target, options);
|
|
};
|
|
try {
|
|
f.sockets[0].ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: f.module.DisconnectReason.loggedOut } } }
|
|
});
|
|
await waitFor(() => Boolean(releaseRemoval));
|
|
const reconnecting = f.manager.connect();
|
|
await tick();
|
|
assert.equal(f.sockets.length, 1);
|
|
releaseRemoval();
|
|
await reconnecting;
|
|
assert.equal(f.sockets.length, 2);
|
|
} finally {
|
|
fs.rm = originalRm;
|
|
}
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('refreshQr encerra socket preso e inicia novo pareamento', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { qr: 'qr-antigo' });
|
|
await f.manager.refreshQr();
|
|
assert.equal(f.sockets.length, 2);
|
|
f.sockets[1].ev.emit('connection.update', { qr: 'qr-novo' });
|
|
assert.equal(f.manager.getQr(), 'qr-novo');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('falha transitoria ao iniciar agenda nova tentativa', async () => {
|
|
let attempts = 0;
|
|
const f = await fixture({
|
|
loadBaileys: async () => {
|
|
attempts += 1;
|
|
if (attempts === 1) throw new Error('rede indisponivel');
|
|
return f.module;
|
|
}
|
|
});
|
|
await assert.rejects(f.manager.connect(), /rede indisponivel/);
|
|
await waitFor(() => f.sockets.length === 1);
|
|
assert.equal(attempts, 2);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('nova conexao manual reinicia o orçamento de reconexoes', async () => {
|
|
const f = await fixture({ maxReconnectAttempts: 1 });
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'close', lastDisconnect: { error: { output: { statusCode: 408 } } } });
|
|
await waitFor(() => f.sockets.length === 2);
|
|
f.sockets[1].ev.emit('connection.update', { connection: 'close', lastDisconnect: { error: { output: { statusCode: 408 } } } });
|
|
await waitFor(() => f.manager.getStatus().state === WHATSAPP_STATES.ERROR);
|
|
await f.manager.connect();
|
|
f.sockets[2].ev.emit('connection.update', { connection: 'close', lastDisconnect: { error: { output: { statusCode: 408 } } } });
|
|
await waitFor(() => f.sockets.length === 4);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('restartRequired reconecta e loggedOut remove somente o authDir', async () => {
|
|
const f = await fixture();
|
|
await fs.writeFile(path.join(f.authDir, 'creds.json'), '{}');
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: f.module.DisconnectReason.restartRequired } } }
|
|
});
|
|
await tick();
|
|
assert.equal(f.sockets.length, 2);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.STARTING);
|
|
|
|
f.sockets[1].ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: f.module.DisconnectReason.loggedOut } } }
|
|
});
|
|
await waitFor(() => f.manager.getStatus().state === WHATSAPP_STATES.DISCONNECTED);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.DISCONNECTED);
|
|
await assert.rejects(fs.stat(f.authDir), { code: 'ENOENT' });
|
|
await f.manager.close();
|
|
});
|
|
|
|
test('disconnect com logout encerra socket, limpa auth e sendText exige READY', async () => {
|
|
const f = await fixture();
|
|
await fs.writeFile(path.join(f.authDir, 'creds.json'), '{}');
|
|
await assert.rejects(
|
|
f.manager.sendText({ number: '5511999990001', message: 'oi' }),
|
|
/WhatsApp nao esta conectado/
|
|
);
|
|
await f.manager.connect();
|
|
let loggedOut = false;
|
|
f.sockets[0].logout = async () => { loggedOut = true; };
|
|
await f.manager.disconnect({ logout: true });
|
|
assert.equal(loggedOut, true);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.DISCONNECTED);
|
|
await assert.rejects(fs.stat(f.authDir), { code: 'ENOENT' });
|
|
await f.manager.close();
|
|
});
|
|
|
|
test('falha no logout remoto ainda limpa sessao local e libera lock', async () => {
|
|
const f = await fixture();
|
|
await fs.writeFile(path.join(f.authDir, 'creds.json'), '{}');
|
|
await f.manager.connect();
|
|
f.sockets[0].logout = async () => { throw new Error('logout remoto falhou'); };
|
|
await assert.rejects(f.manager.disconnect({ logout: true }), /logout remoto falhou/);
|
|
await assert.rejects(fs.stat(f.authDir), { code: 'ENOENT' });
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.DISCONNECTED);
|
|
const second = createWhatsAppManager({ authDir: f.authDir, loadBaileys: async () => f.module });
|
|
await second.connect();
|
|
await second.close();
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText executa callbacks dentro da fila e persiste afterSend antes do proximo envio', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
const events = [];
|
|
await Promise.all([
|
|
f.manager.sendText({ number: '5511999990001', message: 'um', beforeSend: () => events.push('before-1'), afterSend: () => events.push('after-1') }),
|
|
f.manager.sendText({ number: '5511999990002', message: 'dois', beforeSend: () => events.push('before-2'), afterSend: () => events.push('after-2') })
|
|
]);
|
|
assert.deepEqual(events, ['before-1', 'after-1', 'before-2', 'after-2']);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('pendingSends inclui operacoes enfileiradas e decrementa no finally', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
let release;
|
|
f.sockets[0].sendMessage = () => new Promise((resolve) => { release = () => resolve({ key: { id: 'queued-1' } }); });
|
|
const sending = f.manager.sendText({ number: '5511999990001', message: 'oi' });
|
|
assert.equal(f.manager.getStatus().pendingSends, 1);
|
|
await waitFor(() => Boolean(release));
|
|
release();
|
|
await sending;
|
|
assert.equal(f.manager.getStatus().pendingSends, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('disconnect aguarda envio em andamento antes de encerrar socket', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
let releaseSend;
|
|
let ended = false;
|
|
socket.sendMessage = () => new Promise((resolve) => { releaseSend = () => resolve({ key: { id: 'drain-1' } }); });
|
|
socket.end = () => { ended = true; };
|
|
const sending = f.manager.sendText({ number: '5511999990001', message: 'oi' });
|
|
await waitFor(() => Boolean(releaseSend));
|
|
const disconnecting = f.manager.disconnect();
|
|
await tick();
|
|
assert.equal(ended, false);
|
|
releaseSend();
|
|
await sending;
|
|
await disconnecting;
|
|
assert.equal(ended, true);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('disconnect limita espera por sendMessage travado', async () => {
|
|
const f = await fixture({ sendDrainTimeoutMs: 15 });
|
|
await f.manager.connect();
|
|
const socket = f.sockets[0];
|
|
socket.ev.emit('connection.update', { connection: 'open' });
|
|
let releaseSend;
|
|
socket.sendMessage = () => new Promise((resolve) => { releaseSend = () => resolve({ key: { id: 'late-1' } }); });
|
|
const sending = f.manager.sendText({ number: '5511999990001', message: 'oi' });
|
|
await waitFor(() => Boolean(releaseSend));
|
|
await assert.rejects(f.manager.disconnect(), /fila de envios.*prazo/i);
|
|
releaseSend();
|
|
await sending;
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText chama afterSend uma vez e preserva messageId quando a persistencia falha', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
let calls = 0;
|
|
|
|
await assert.rejects(
|
|
f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
afterSend: () => {
|
|
calls += 1;
|
|
throw new Error('banco indisponivel');
|
|
}
|
|
}),
|
|
(error) => {
|
|
assert.match(error.message, /persistir.*message-1/i);
|
|
assert.equal(error.messageId, 'message-1');
|
|
assert.equal(error.deliverySucceeded, true);
|
|
assert.match(error.cause.message, /banco indisponivel/);
|
|
return true;
|
|
}
|
|
);
|
|
assert.equal(calls, 1);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText trata retorno false do afterSend como falha de persistencia', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
await assert.rejects(f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
afterSend: () => false
|
|
}), /persistir.*message-1/i);
|
|
assert.equal(f.manager.getStatus().pendingSends, 0);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText informa falha do provedor ao afterSend exatamente uma vez', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
const providerError = new Error('provedor fora');
|
|
f.sockets[0].sendMessage = async () => { throw providerError; };
|
|
const calls = [];
|
|
|
|
await assert.rejects(f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'oi',
|
|
afterSend: (...args) => calls.push(args)
|
|
}), providerError);
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0][0], null);
|
|
assert.equal(calls[0][1], providerError);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText trata resposta resolvida sem messageId como entrega desconhecida', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'open' });
|
|
f.sockets[0].sendMessage = async () => ({ key: {} });
|
|
const finalized = [];
|
|
|
|
await assert.rejects(f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'pode ter sido enviada',
|
|
beforeSend: () => 'reservation-without-id',
|
|
afterSend: (...args) => finalized.push(args)
|
|
}), (error) => {
|
|
assert.equal(error.code, 'WHATSAPP_MESSAGE_ID_MISSING');
|
|
assert.equal(error.deliveryUnknown, true);
|
|
return true;
|
|
});
|
|
|
|
assert.equal(finalized.length, 1);
|
|
assert.equal(finalized[0][0], null);
|
|
assert.equal(finalized[0][1].deliveryUnknown, true);
|
|
assert.equal(finalized[0][2], 'reservation-without-id');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText nao usa socket capturado se conexao muda durante beforeSend', async () => {
|
|
const f = await fixture();
|
|
await f.manager.connect();
|
|
const staleSocket = f.sockets[0];
|
|
staleSocket.ev.emit('connection.update', { connection: 'open' });
|
|
let staleSendAttempts = 0;
|
|
staleSocket.sendMessage = async () => {
|
|
staleSendAttempts += 1;
|
|
return { key: { id: 'stale-send' } };
|
|
};
|
|
let releaseBeforeSend;
|
|
let notifyBeforeSend;
|
|
const beforeSendStarted = new Promise((resolve) => { notifyBeforeSend = resolve; });
|
|
const beforeSendGate = new Promise((resolve) => { releaseBeforeSend = resolve; });
|
|
const finalized = [];
|
|
|
|
const sending = f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'nao enviar em socket obsoleto',
|
|
beforeSend: async () => {
|
|
notifyBeforeSend();
|
|
await beforeSendGate;
|
|
return 'reservation-stale';
|
|
},
|
|
afterSend: (...args) => finalized.push(args)
|
|
});
|
|
await beforeSendStarted;
|
|
staleSocket.ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: 408 } } }
|
|
});
|
|
await waitFor(() => f.sockets.length === 2);
|
|
const replacementSocket = f.sockets[1];
|
|
replacementSocket.ev.emit('connection.update', { connection: 'open' });
|
|
releaseBeforeSend();
|
|
|
|
await assert.rejects(sending, (error) => {
|
|
assert.equal(error.code, 'WHATSAPP_SOCKET_STALE');
|
|
assert.equal(error.numero, '5511999990001');
|
|
assert.equal(error.resolvedNumber, '5511999990001');
|
|
assert.equal(error.variant, 'exata');
|
|
return true;
|
|
});
|
|
assert.equal(staleSendAttempts, 0);
|
|
assert.equal(replacementSocket.sent.length, 0);
|
|
assert.equal(finalized.length, 1);
|
|
assert.equal(finalized[0][0], null);
|
|
assert.equal(finalized[0][1].code, 'WHATSAPP_SOCKET_STALE');
|
|
assert.equal(finalized[0][2], 'reservation-stale');
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sendText limita envio incerto, finaliza uma vez e invalida socket antes de liberar fila', async () => {
|
|
const events = [];
|
|
const f = await fixture({ messageTimeoutMs: 10 });
|
|
await f.manager.connect();
|
|
const uncertainSocket = f.sockets[0];
|
|
uncertainSocket.ev.emit('connection.update', { connection: 'open' });
|
|
let rejectLateSend;
|
|
let ended = false;
|
|
let lookupAttempts = 0;
|
|
let sendAttempts = 0;
|
|
uncertainSocket.onWhatsApp = async (jid) => {
|
|
lookupAttempts += 1;
|
|
return [{ jid: `${jid}@s.whatsapp.net`, exists: true }];
|
|
};
|
|
uncertainSocket.sendMessage = () => {
|
|
sendAttempts += 1;
|
|
return new Promise((resolve, reject) => { rejectLateSend = reject; });
|
|
};
|
|
uncertainSocket.end = () => { ended = true; };
|
|
|
|
const first = f.manager.sendText({
|
|
number: '5511999990001',
|
|
message: 'primeira',
|
|
beforeSend: () => { events.push('before'); return 'reservation-1'; },
|
|
afterSend: (result, error, context) => events.push(['after', result, error, context])
|
|
});
|
|
const second = f.manager.sendText({ number: '5511999990002', message: 'segunda' });
|
|
const secondOutcome = second.catch((error) => error);
|
|
|
|
await assert.rejects(first, (error) => {
|
|
assert.equal(error.statusCode, 504);
|
|
assert.equal(error.code, 'WHATSAPP_SEND_TIMEOUT');
|
|
assert.equal(error.deliveryUnknown, true);
|
|
assert.equal(error.numero, '5511999990001');
|
|
assert.equal(error.resolvedNumber, '5511999990001');
|
|
assert.equal(error.variant, 'exata');
|
|
return true;
|
|
});
|
|
const secondError = await secondOutcome;
|
|
assert.match(secondError.message, /nao esta conectado/i);
|
|
assert.equal(events[0], 'before');
|
|
assert.equal(events.length, 2);
|
|
assert.equal(events[1][0], 'after');
|
|
assert.equal(events[1][1], null);
|
|
assert.equal(events[1][2].code, 'WHATSAPP_SEND_TIMEOUT');
|
|
assert.equal(events[1][3], 'reservation-1');
|
|
assert.equal(lookupAttempts, 1);
|
|
assert.equal(sendAttempts, 1);
|
|
assert.equal(ended, true);
|
|
assert.equal(f.manager.getStatus().pendingSends, 0);
|
|
await waitFor(() => f.sockets.length === 2);
|
|
assert.notStrictEqual(f.sockets[1], uncertainSocket);
|
|
|
|
rejectLateSend(new Error('envio rejeitado depois do timeout'));
|
|
await tick();
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('timeout tardio do socket anterior nao invalida socket substituto', async () => {
|
|
const f = await fixture({ messageTimeoutMs: 100 });
|
|
await f.manager.connect();
|
|
const staleSocket = f.sockets[0];
|
|
staleSocket.ev.emit('connection.update', { connection: 'open' });
|
|
let rejectLateSend;
|
|
let notifySendStarted;
|
|
let staleSendAttempts = 0;
|
|
const sendStarted = new Promise((resolve) => { notifySendStarted = resolve; });
|
|
staleSocket.sendMessage = () => {
|
|
staleSendAttempts += 1;
|
|
notifySendStarted();
|
|
return new Promise((resolve, reject) => { rejectLateSend = reject; });
|
|
};
|
|
|
|
const uncertainSend = f.manager.sendText({ number: '5511999990001', message: 'incerta' });
|
|
await sendStarted;
|
|
staleSocket.ev.emit('connection.update', {
|
|
connection: 'close',
|
|
lastDisconnect: { error: { output: { statusCode: 408 } } }
|
|
});
|
|
await waitFor(() => f.sockets.length === 2);
|
|
const replacementSocket = f.sockets[1];
|
|
replacementSocket.ev.emit('connection.update', { connection: 'open' });
|
|
|
|
await assert.rejects(uncertainSend, (error) => {
|
|
assert.equal(error.code, 'WHATSAPP_SEND_TIMEOUT');
|
|
assert.equal(error.deliveryUnknown, true);
|
|
return true;
|
|
});
|
|
assert.equal(staleSendAttempts, 1);
|
|
assert.equal(f.sockets.length, 2);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.READY);
|
|
|
|
const result = await f.manager.sendText({ number: '5511999990002', message: 'somente no socket B' });
|
|
assert.equal(result.messageId, 'message-1');
|
|
assert.deepEqual(replacementSocket.sent, [
|
|
['5511999990002@s.whatsapp.net', { text: 'somente no socket B' }]
|
|
]);
|
|
assert.equal(staleSendAttempts, 1);
|
|
|
|
rejectLateSend(new Error('socket A rejeitou tarde'));
|
|
await tick();
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('dois managers nao adquirem o mesmo authDir e lock stale e recuperado', async () => {
|
|
const first = await fixture();
|
|
await first.manager.connect();
|
|
const second = createWhatsAppManager({ authDir: first.authDir, loadBaileys: async () => first.module });
|
|
await assert.rejects(second.connect(), /em uso por outro processo/);
|
|
await first.manager.close();
|
|
await second.connect();
|
|
await second.close();
|
|
|
|
const lockPath = path.join(path.dirname(first.authDir), `${path.basename(first.authDir)}.lock`);
|
|
await fs.writeFile(lockPath, JSON.stringify({ pid: 2147483647 }));
|
|
const stale = createWhatsAppManager({ authDir: first.authDir, loadBaileys: async () => first.module });
|
|
await stale.connect();
|
|
await stale.close();
|
|
await fs.rm(first.authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('duas aquisicoes concorrentes isolam lock stale sem remover lock do vencedor', async () => {
|
|
const authDir = await fs.mkdtemp(path.join(os.tmpdir(), 'whatsapp-lock-race-'));
|
|
const lockPath = path.join(path.dirname(authDir), `${path.basename(authDir)}.lock`);
|
|
await fs.writeFile(lockPath, JSON.stringify({ pid: 2147483647, token: 'stale' }));
|
|
const sockets = [];
|
|
const module = {
|
|
DisconnectReason: {},
|
|
useMultiFileAuthState: async () => ({ state: {}, saveCreds: async () => {} }),
|
|
default: () => {
|
|
const socket = { ev: new EventEmitter(), end() {} };
|
|
sockets.push(socket);
|
|
return socket;
|
|
}
|
|
};
|
|
const managers = [
|
|
createWhatsAppManager({ authDir, loadBaileys: async () => module }),
|
|
createWhatsAppManager({ authDir, loadBaileys: async () => module })
|
|
];
|
|
|
|
const originalRename = fs.rename;
|
|
let authRenames = 0;
|
|
fs.rename = async (from, to) => {
|
|
if (from !== lockPath) return originalRename(from, to);
|
|
authRenames += 1;
|
|
if (authRenames === 2) {
|
|
await waitFor(async () => {
|
|
try {
|
|
const holder = JSON.parse(await fs.readFile(lockPath, 'utf8'));
|
|
return holder.pid === process.pid;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
return originalRename(from, to);
|
|
};
|
|
|
|
let results;
|
|
try {
|
|
results = await Promise.allSettled(managers.map((manager) => manager.connect()));
|
|
} finally {
|
|
fs.rename = originalRename;
|
|
}
|
|
assert.equal(results.filter(({ status }) => status === 'fulfilled').length, 1);
|
|
const rejected = results.find(({ status }) => status === 'rejected');
|
|
assert.match(rejected.reason.message, /(?:em uso por outro processo|recuperacao.*(?:andamento|bloqueada))/i);
|
|
assert.equal(authRenames, 1);
|
|
assert.equal(sockets.length, 1);
|
|
const holder = JSON.parse(await fs.readFile(lockPath, 'utf8'));
|
|
assert.equal(holder.pid, process.pid);
|
|
|
|
const loser = managers[results.findIndex(({ status }) => status === 'rejected')];
|
|
await loser.close();
|
|
assert.equal((await fs.stat(lockPath)).isFile(), true);
|
|
const winner = managers[results.findIndex(({ status }) => status === 'fulfilled')];
|
|
await winner.close();
|
|
await assert.rejects(fs.stat(lockPath), { code: 'ENOENT' });
|
|
await fs.rm(authDir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('recovery lock de processo morto e removido com seguranca', async () => {
|
|
const f = await fixture();
|
|
const lockPath = path.join(path.dirname(f.authDir), `${path.basename(f.authDir)}.lock`);
|
|
const recoveryPath = `${lockPath}.recovery`;
|
|
await fs.writeFile(lockPath, JSON.stringify({ pid: 2147483647, token: 'stale' }));
|
|
await fs.writeFile(recoveryPath, JSON.stringify({ pid: 2147483646, token: 'recovery-owner' }));
|
|
|
|
await f.manager.connect();
|
|
assert.equal(f.sockets.length, 1);
|
|
await assert.rejects(fs.stat(recoveryPath), { code: 'ENOENT' });
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
await fs.rm(lockPath, { force: true });
|
|
await fs.rm(recoveryPath, { force: true });
|
|
});
|
|
|
|
test('recuperacao de lock stale usa quarantine por rename atomico', async () => {
|
|
const source = await fs.readFile(new URL('../src/whatsapp/manager.mjs', import.meta.url), 'utf8');
|
|
const acquireLock = source.slice(source.indexOf('const acquireLock'), source.indexOf('const releaseLock'));
|
|
assert.match(acquireLock, /fs\.rename\(lockPath,/);
|
|
assert.doesNotMatch(acquireLock, /fs\.rm\(lockPath/);
|
|
});
|
|
|
|
test('disconnect durante await de connect nao publica socket e fatal nao reconecta', async () => {
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
const f = await fixture({ loadBaileys: async () => { await gate; return f.module; } });
|
|
const connecting = f.manager.connect();
|
|
await f.manager.disconnect();
|
|
release();
|
|
await connecting;
|
|
assert.equal(f.sockets.length, 0);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.STOPPED);
|
|
|
|
await f.manager.connect();
|
|
f.sockets[0].ev.emit('connection.update', { connection: 'close', lastDisconnect: { error: { output: { statusCode: f.module.DisconnectReason.connectionReplaced } } } });
|
|
await tick();
|
|
assert.equal(f.sockets.length, 1);
|
|
assert.equal(f.manager.getStatus().state, WHATSAPP_STATES.ERROR);
|
|
await f.manager.close();
|
|
await fs.rm(f.authDir, { recursive: true, force: true });
|
|
});
|