Austin
Austin

Reputation: 424

Why are my tests failing for a WhatsAppbot using whatsapp-web.js? Potential issue with mocks

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

Answers (0)

Related Questions