Reputation: 424
I have written a WhatsApp bot using whatsapp-web.js, and it works fine in production. However, when I try to run tests, they fail, and I suspect the issue lies with my mocks, but I am not entirely sure.
Here’s the implementation of the WhatsAppBot class:
import qrcode from 'qrcode';
import { Client, LocalAuth, Message } from 'whatsapp-web.js';
import fs from 'fs';
import {
createOrUpdateUser,
getUserByWhatsAppId,
getFaqAnswer,
logQuery,
} from '../services/dbServices';
import { queryGenerativeAI } from '../services/germini';
const QR_FILE_PATH = './qr-code.json';
const QR_IMAGE_PATH = './qr-code.png';
export class WhatsAppBot {
public client: Client;
private qrCodeGenerated: boolean;
private userSessions: Map<string, string>;
constructor() {
this.client = new Client({
authStrategy: new LocalAuth(),
});
this.qrCodeGenerated = false;
this.userSessions = new Map();
this.setupEventHandlers();
}
private setupEventHandlers() {
this.client.on('qr', async (qrCode: string) => {
if (!this.qrCodeGenerated) {
try {
console.log('Scan this QR code with WhatsApp:');
fs.writeFileSync(QR_FILE_PATH, JSON.stringify({ qrCode }), 'utf-8');
await qrcode.toFile(QR_IMAGE_PATH, qrCode, {
errorCorrectionLevel: 'M',
scale: 8,
margin: 4,
color: { dark: '#000000', light: '#ffffff' },
});
const terminalQR = await qrcode.toString(qrCode, { type: 'terminal', small: true });
console.log(terminalQR);
console.log(`QR code saved to ${QR_FILE_PATH} and ${QR_IMAGE_PATH}`);
this.qrCodeGenerated = true;
} catch (error) {
console.error('Error generating QR code:', error);
}
}
});
this.client.on('authenticated', () => {
console.log('Authenticated successfully!');
});
this.client.on('auth_failure', (msg: string) => {
console.error('Authentication failed:', msg);
});
this.client.on('ready', () => {
console.log('WhatsApp bot is ready!');
});
this.client.on('disconnected', (reason: string) => {
console.log('Client was logged out:', reason);
});
this.client.on('message', async (message: Message) => {
const userId = message.from;
if(!userId) return console.log(`Invalid userId: ${userId}`);
console.log(`Message received from ${userId}:`, message.body);
if (!this.userSessions.has(userId)) {
this.userSessions.set(userId, '');
await message.reply("Hi! I'm Chaty, your support assistant. What's your name?");
return;
}
const userName = this.userSessions.get(userId);
if (!userName) {
this.userSessions.set(userId, message.body);
await createOrUpdateUser(userId, message.body);
await message.reply(`Nice to meet you, ${message.body}! How can I assist you today? you can type 'help' to see a list of commands`);
return;
}
const query = message.body.toLowerCase();
if (query === 'help') {
await message.reply("type: 'exit' to end the session and 'reset' to reset the session");
return;
}
if (query === 'exit') {
this.userSessions.delete(userId);
await message.reply('Your session has been ended. Have a great day!');
return;
}
if (query === 'reset') {
this.userSessions.set(userId, '');
await message.reply("Let's start over. What's your name?");
return;
}
const faqAnswer = await getFaqAnswer(query);
let aiResponse = 'AI generated response';
if (faqAnswer) {
await message.reply(faqAnswer);
} else {
aiResponse = process.env.NODE_ENV === 'test' ? 'AI generated response' : await queryGenerativeAI(query);
await message.reply(aiResponse);
}
const user = await getUserByWhatsAppId(userId);
if (user) {
await logQuery(user.id, query, faqAnswer || aiResponse);
}
});
this.client.on('message_ack', (message: Message, ack: number) => {
const status = ['MESSAGE SENT', 'MESSAGE DELIVERED', 'MESSAGE READ', 'MESSAGE PLAYED'];
console.log(`Message "${message.body}" acknowledgment status: ${status[ack] || 'UNKNOWN'}`);
});
}
public initialize() {
if (fs.existsSync(QR_FILE_PATH)) {
console.log(`QR code already saved at ${QR_FILE_PATH}. Please use it for reconnection.`);
} else {
console.log('No saved QR code found. Generating a new QR code...');
}
this.client.initialize();
}
public async stop() {
try {
await this.client.destroy();
console.log('WhatsApp bot stopped');
} catch (err) {
console.error('Error stopping WhatsApp bot:', err);
}
}
}
I’m using sinon for stubbing the client methods in my tests. Below is a part of my test file:
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as proxyquire from 'proxyquire';
import { Message } from 'whatsapp-web.js';
proxyquire.noCallThru();
describe('WhatsAppBot', () => {
let bot: any;
let sandbox: sinon.SinonSandbox;
let mockClient: {
on: sinon.SinonStub;
initialize: sinon.SinonStub;
destroy: sinon.SinonStub;
sendMessage: sinon.SinonStub;
};
before(() => {
sandbox = sinon.createSandbox();
// Mock Client methods
mockClient = {
on: sandbox.stub(),
initialize: sandbox.stub(),
destroy: sandbox.stub(),
sendMessage: sandbox.stub(),
};
const mockLocalAuth = sandbox.stub();
const WhatsAppBot = proxyquire.load('../src/botHandler/whatsappBot', {
'whatsapp-web.js': {
Client: sandbox.stub().returns(mockClient),
LocalAuth: mockLocalAuth,
},
qrcode: {
toFile: sandbox.stub().resolves(),
toString: sandbox.stub().resolves(),
},
fs: {
writeFileSync: sandbox.stub(),
existsSync: sandbox.stub().returns(false),
},
'../services/dbServices': {
createOrUpdateUser: sandbox.stub().resolves(),
getUserByWhatsAppId: sandbox.stub().resolves(),
getFaqAnswer: sandbox.stub().resolves('The weather today is sunny.'),
logQuery: sandbox.stub().resolves(),
},
'../services/germini': {
queryGenerativeAI: sandbox.stub().resolves('AI generated response'),
},
}).WhatsAppBot;
bot = new WhatsAppBot();
});
it('should generate QR code on initialization', async () => {
const qrCode = 'qrCodeString';
mockClient.on.withArgs('qr').callsFake((event, handler) => {
if (event === 'qr') handler(qrCode);
});
await bot.initialize();
expect(mockClient.on.calledWith('qr')).to.be.true;
expect(mockClient.initialize.called).to.be.true;
});
it('should handle new user message', async () => {
const userId = '123';
const userName = 'John Doe';
const message: Message = { from: userId, body: userName } as Message;
mockClient.on.withArgs('message').callsFake((event, handler) => {
console.log('Event triggered:', event);
if (event === 'message') handler(message);
});
await bot.initialize();
console.log('User sessions:', bot.userSessions);
console.log('SendMessage calls:', mockClient.sendMessage.args);
expect(bot.userSessions.get(userId)).to.equal(userName);
expect(mockClient.sendMessage.calledWith(userId, sinon.match.string)).to.be.true;
});
it('should handle existing user query with FAQ answer', async () => {
const userId = '123';
const query = 'What is the weather today?';
const message: Message = { from: userId, body: query } as Message;
bot.userSessions.set(userId, 'John Doe');
mockClient.on.withArgs('message').callsFake((event, handler) => {
if (event === 'message') handler(message);
});
await bot.initialize();
expect(mockClient.sendMessage.calledWith(userId, 'The weather today is sunny.')).to.be.true;
});
it('should handle existing user query with AI response', async () => {
// Arrange
const userId = '123';
const query = 'Tell me a joke.';
const aiResponse = 'Why don’t scientists trust atoms? Because they make up everything!';
const message: Message = { from: userId, body: query } as Message;
bot.userSessions.set(userId, 'John Doe');
sandbox.stub(bot, 'getFaqAnswer').resolves(null);
sandbox.stub(bot, 'queryGenerativeAI').resolves(aiResponse);
mockClient.on.withArgs('message').callsFake((event, handler) => {
if (event === 'message') handler(message);
});
await bot.initialize();
expect(mockClient.sendMessage.calledWith(userId, aiResponse)).to.be.true;
});
it('should handle "exit" command', async () => {
const userId = '123';
const message: Message = { from: userId, body: 'exit' } as Message;
bot.userSessions.set(userId, 'John Doe');
mockClient.on.withArgs('message').callsFake((event, handler) => {
if (event === 'message') handler(message);
});
await bot.initialize();
expect(mockClient.sendMessage.calledWith(userId, 'Your session has been ended. Have a great day!')).to.be.true;
expect(bot.userSessions.has(userId)).to.be.false;
});
it('should handle "reset" command', async () => {
const userId = '123';
const message: Message = { from: userId, body: 'reset' } as Message;
bot.userSessions.set(userId, 'John Doe');
mockClient.on.withArgs('message').callsFake((event, handler) => {
if (event === 'message') handler(message);
});
await bot.initialize();
expect(mockClient.sendMessage.calledWith(userId, "Let's start over. What's your name?")).to.be.true;
expect(bot.userSessions.get(userId)).to.equal('');
});
});
here's the console output for the tests
WhatsAppBot
No saved QR code found. Generating a new QR code...
✔ should generate QR code on initialization
No saved QR code found. Generating a new QR code...
User sessions: Map(0) {}
SendMessage calls: []
1) should handle new user message
No saved QR code found. Generating a new QR code...
2) should handle existing user query with FAQ answer
3) should handle existing user query with AI response
No saved QR code found. Generating a new QR code...
4) should handle "exit" command
No saved QR code found. Generating a new QR code...
5) should handle "reset" command
5 passing (255ms)
5 failing
1) WhatsAppBot
should handle new user message:
AssertionError: expected undefined to equal 'John Doe'
at Context.<anonymous> (test/whatsappBot.test.ts:91:45)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
2) WhatsAppBot
should handle existing user query with FAQ answer:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.<anonymous> (test/whatsappBot.test.ts:111:91)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
3) WhatsAppBot
should handle existing user query with AI response:
TypeError: Cannot stub non-existent property getFaqAnswer
at Function.stub (node_modules/sinon/lib/sinon/stub.js:82:15)
at Sandbox.stub (node_modules/sinon/lib/sinon/sandbox.js:454:39)
at Context.<anonymous> (test/whatsappBot.test.ts:122:13)
at processImmediate (node:internal/timers:483:21)
4) WhatsAppBot
should handle "exit" command:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.<anonymous> (test/whatsappBot.test.ts:150:110)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
5) WhatsAppBot
should handle "reset" command:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.<anonymous> (test/whatsappBot.test.ts:168:99)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
Upvotes: 1
Views: 52