Files
desa-darmasaba/__tests__/lib/whatsapp.test.ts
nico 6ed2392420 Add comprehensive testing suite and fix QC issues
- Add 115+ unit, component, and E2E tests
- Add Vitest configuration with coverage thresholds
- Add validation schema tests (validations.test.ts)
- Add sanitizer utility tests (sanitizer.test.ts)
- Add WhatsApp service tests (whatsapp.test.ts)
- Add component tests for UnifiedTypography and UnifiedSurface
- Add E2E tests for admin auth and public pages
- Add testing documentation (docs/TESTING.md)
- Add sanitizer and WhatsApp utilities
- Add centralized validation schemas
- Refactor state management (admin/public separation)
- Fix security issues (OTP via POST, session password validation)
- Update AGENTS.md with testing guidelines

Test Coverage: 50%+ target achieved
All tests passing: 115/115

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-09 14:05:03 +08:00

363 lines
10 KiB
TypeScript

/**
* WhatsApp Service Unit Tests
*
* Tests for WhatsApp OTP service in lib/whatsapp
* Note: These tests use direct fetch mocking, not MSW
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
sendWhatsAppOTP,
formatOTPMessage,
formatOTPMessageWithReference,
} from '@/lib/whatsapp';
describe('WhatsApp Service', () => {
// Store original fetch
const originalFetch = global.fetch;
let mockFetch: any;
beforeEach(() => {
mockFetch = vi.fn();
global.fetch = mockFetch;
});
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
// ============================================================================
// formatOTPMessage Tests
// ============================================================================
describe('formatOTPMessage', () => {
it('should format OTP message with numeric code', () => {
const otpCode = 123456;
const message = formatOTPMessage(otpCode);
expect(message).toContain('Website Desa Darmasaba');
expect(message).toContain('RAHASIA');
expect(message).toContain('JANGAN DI BAGIKAN');
expect(message).toContain('123456');
expect(message).toContain('satu kali login');
});
it('should format OTP message with string code', () => {
const otpCode = '654321';
const message = formatOTPMessage(otpCode);
expect(message).toContain('654321');
});
it('should include security warning', () => {
const message = formatOTPMessage(123456);
expect(message).toMatch(/RAHASIA/);
expect(message).toMatch(/JANGAN DI BAGIKAN KEPADA SIAPAPUN/);
});
it('should mention code validity', () => {
const message = formatOTPMessage(123456);
expect(message).toMatch(/hanya berlaku untuk satu kali login/);
});
});
// ============================================================================
// formatOTPMessageWithReference Tests
// ============================================================================
describe('formatOTPMessageWithReference', () => {
it('should format message with reference ID', () => {
const otpId = 'clm5z8z8z000008l4f3qz8z8z';
const message = formatOTPMessageWithReference(otpId);
expect(message).toContain('Website Desa Darmasaba');
expect(message).toContain('RAHASIA');
expect(message).toContain('JANGAN DI BAGIKAN');
expect(message).toContain(otpId);
expect(message).toContain('Reference ID');
});
it('should NOT include actual OTP code', () => {
const message = formatOTPMessageWithReference('test-id');
expect(message).not.toMatch(/\d{6}/);
});
it('should instruct user to enter received OTP', () => {
const message = formatOTPMessageWithReference('test-id');
expect(message).toMatch(/masukkan kode OTP/);
});
it('should include security warning', () => {
const message = formatOTPMessageWithReference('test-id');
expect(message).toMatch(/RAHASIA/);
expect(message).toMatch(/JANGAN DI BAGIKAN KEPADA SIAPAPUN/);
});
});
// ============================================================================
// sendWhatsAppOTP Tests
// ============================================================================
describe('sendWhatsAppOTP', () => {
it('should send OTP successfully with valid parameters', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'clm5z8z8z000008l4f3qz8z8z',
message: 'Test message',
});
expect(result.status).toBe('success');
expect(mockFetch).toHaveBeenCalledWith(
'https://wa.wibudev.com/send',
expect.objectContaining({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
);
});
it('should use POST method (not GET) for security', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-otp-id',
message: 'Test',
});
const callArgs = mockFetch.mock.calls[0];
expect(callArgs[0]).toBe('https://wa.wibudev.com/send');
expect(callArgs[1]?.method).toBe('POST');
});
it('should send otpId reference, not actual OTP code', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-otp-id-123',
message: 'Test message',
});
const callArgs = mockFetch.mock.calls[0];
const body = JSON.parse(callArgs[1]?.body as string);
expect(body.otpId).toBe('test-otp-id-123');
expect(body.nomor).toBe('08123456789');
});
it('should return error for invalid phone number (empty)', async () => {
const result = await sendWhatsAppOTP({
nomor: '',
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Nomor telepon tidak valid');
expect(mockFetch).not.toHaveBeenCalled();
});
it('should return error for invalid phone number (null)', async () => {
const result = await sendWhatsAppOTP({
nomor: null as any,
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Nomor telepon tidak valid');
});
it('should return error for invalid otpId', async () => {
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: '',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('OTP ID tidak valid');
expect(mockFetch).not.toHaveBeenCalled();
});
it('should return error for null otpId', async () => {
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: null as any,
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('OTP ID tidak valid');
});
it('should handle WhatsApp API error response', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
status: 'error',
message: 'Invalid phone number',
}),
clone: function() { return this; }
} as any);
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Invalid phone number');
});
it('should handle HTTP error response', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
clone: function() { return this; }
} as any);
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Gagal mengirim pesan WhatsApp');
});
it('should handle network errors', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Terjadi kesalahan saat mengirim pesan');
});
it('should handle JSON parse errors', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => {
throw new Error('Invalid JSON');
},
clone: function() { return this; }
} as any);
const result = await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Test',
});
expect(result.status).toBe('error');
expect(result.message).toBe('Terjadi kesalahan saat mengirim pesan');
});
it('should send correct request body structure', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
await sendWhatsAppOTP({
nomor: '081234567890',
otpId: 'unique-otp-id',
message: 'Custom message',
});
const callArgs = mockFetch.mock.calls[0];
const body = JSON.parse(callArgs[1]?.body as string);
expect(body).toEqual({
nomor: '081234567890',
otpId: 'unique-otp-id',
message: 'Custom message',
});
});
});
// ============================================================================
// Security Tests
// ============================================================================
describe('Security - OTP not exposed in URL', () => {
it('should NOT include OTP code in URL query string', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Your OTP is 123456',
});
const callArgs = mockFetch.mock.calls[0];
const url = callArgs[0];
// URL should be the endpoint, not containing OTP
expect(url).toBe('https://wa.wibudev.com/send');
expect(url).not.toContain('123456');
expect(url).not.toContain('?');
});
it('should send OTP in request body (POST), not URL', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ status: 'success' }),
clone: function() { return this; }
} as any);
await sendWhatsAppOTP({
nomor: '08123456789',
otpId: 'test-id',
message: 'Test',
});
const callArgs = mockFetch.mock.calls[0];
// Should use POST with body
expect(callArgs[1]?.method).toBe('POST');
expect(callArgs[1]?.body).toBeDefined();
// OTP reference should be in body, not URL
const body = JSON.parse(callArgs[1]?.body as string);
expect(body.otpId).toBe('test-id');
});
});
});