const mockPaymentFindOne = jest.fn(); const mockOrderFindByPk = jest.fn(); const mockOrderFindOne = jest.fn(); jest.mock('../../../../models/migration', () => ({ Payment: { findOne: mockPaymentFindOne, update: jest.fn() }, Order: { findByPk: mockOrderFindByPk, findOne: mockOrderFindOne, update: jest.fn() } })); jest.mock('../../models/migration', () => ({ Payment: { findOne: mockPaymentFindOne, update: jest.fn() }, Order: { findByPk: mockOrderFindByPk, findOne: mockOrderFindOne, update: jest.fn() } })); const notificationService = require('../../app/modules/notification/services/notification.service'); const crypto = require('crypto'); describe('NotificationService', () => { const serverKey = 'SB-Mid-server-test'; beforeEach(() => { jest.clearAllMocks(); process.env.MIDTRANS_SERVER_KEY = serverKey; }); describe('verifySignature', () => { it('should return true for a valid signature', () => { const payload = { order_id: 'I-123', status_code: '200', gross_amount: '10.00', signature_key: '' }; const data = payload.order_id + payload.status_code + payload.gross_amount + serverKey; payload.signature_key = crypto.createHash('sha512').update(data).digest('hex'); expect(notificationService.verifySignature(payload)).toBe(true); }); it('should return false for invalid signature', () => { const payload = { order_id: 'I-123', status_code: '200', gross_amount: '10.00', signature_key: 'wrong' }; expect(notificationService.verifySignature(payload)).toBe(false); }); }); describe('processNotification', () => { it('should update order to COMPLETED on settlement', async () => { const payload = { order_id: 'I-123', transaction_status: 'settlement', fraud_status: 'accept' }; const mockUpdateOrder = jest.fn(); const mockUpdatePayment = jest.fn(); const mockPayment = { order_id: 'u-123', update: mockUpdatePayment }; const mockOrder = { id: 'u-123', status: 'PENDING', payment_status: 'PENDING', update: mockUpdateOrder }; mockPaymentFindOne.mockResolvedValue(mockPayment); mockOrderFindByPk.mockResolvedValue(mockOrder); await notificationService.processNotification(payload); expect(mockUpdateOrder).toHaveBeenCalledWith(expect.objectContaining({ status: 'COMPLETED', payment_status: 'PAID' })); expect(mockUpdatePayment).toHaveBeenCalledWith(expect.objectContaining({ status: 'SUCCESS' })); }); it('should update order to FAILED on deny/cancel/expire', async () => { const payload = { order_id: 'I-123', transaction_status: 'expire' }; const mockUpdate = jest.fn(); const mockOrder = { id: 'u-123', status: 'PENDING', update: mockUpdate }; mockPaymentFindOne.mockResolvedValue(null); mockOrderFindOne.mockResolvedValue(mockOrder); await notificationService.processNotification(payload); expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ status: 'FAILED', payment_status: 'REJECT' })); }); it('should update to PENDING on pending status', async () => { const payload = { order_id: 'I-123', transaction_status: 'pending' }; const mockUpdate = jest.fn(); const mockOrder = { id: 'u-123', status: 'PENDING', payment_status: 'PENDING', update: mockUpdate }; mockPaymentFindOne.mockResolvedValue(null); mockOrderFindOne.mockResolvedValue(mockOrder); await notificationService.processNotification(payload); // If already pending, it should skip update expect(mockUpdate).not.toHaveBeenCalled(); }); it('should warn if order not found', async () => { const payload = { order_id: 'unknown' }; mockPaymentFindOne.mockResolvedValue(null); mockOrderFindOne.mockResolvedValue(null); mockOrderFindByPk.mockResolvedValue(null); const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); await notificationService.processNotification(payload); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('tidak ditemukan')); consoleSpy.mockRestore(); }); }); });