- 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>
363 lines
10 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|