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>
This commit is contained in:
555
__tests__/lib/validations.test.ts
Normal file
555
__tests__/lib/validations.test.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* Validation Schemas Unit Tests
|
||||
*
|
||||
* Tests for Zod validation schemas in lib/validations
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
createBeritaSchema,
|
||||
updateBeritaSchema,
|
||||
loginRequestSchema,
|
||||
otpVerificationSchema,
|
||||
uploadFileSchema,
|
||||
registerUserSchema,
|
||||
paginationSchema,
|
||||
} from '@/lib/validations';
|
||||
|
||||
// ============================================================================
|
||||
// Berita Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('createBeritaSchema', () => {
|
||||
const validData = {
|
||||
judul: 'Judul Berita Valid',
|
||||
deskripsi: 'Deskripsi yang cukup panjang untuk berita',
|
||||
content: 'Konten berita yang lengkap dengan minimal 50 karakter',
|
||||
kategoriBeritaId: 'clm5z8z8z000008l4f3qz8z8z',
|
||||
imageId: 'clm5z8z8z000008l4f3qz8z8z',
|
||||
};
|
||||
|
||||
it('should accept valid berita data', () => {
|
||||
const result = createBeritaSchema.safeParse(validData);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject short titles (less than 5 characters)', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
judul: 'abc',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('judul');
|
||||
expect(result.error.errors[0].message).toContain('minimal 5 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject long titles (more than 255 characters)', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
judul: 'a'.repeat(256),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('judul');
|
||||
expect(result.error.errors[0].message).toContain('maksimal 255 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject short descriptions (less than 10 characters)', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
deskripsi: 'short',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('deskripsi');
|
||||
expect(result.error.errors[0].message).toContain('minimal 10 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject long descriptions (more than 500 characters)', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
deskripsi: 'a'.repeat(501),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('deskripsi');
|
||||
expect(result.error.errors[0].message).toContain('maksimal 500 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject short content (less than 50 characters)', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
content: 'short',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('content');
|
||||
expect(result.error.errors[0].message).toContain('minimal 50 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid cuid for kategoriBeritaId', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
kategoriBeritaId: 'invalid-id',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('kategoriBeritaId');
|
||||
expect(result.error.errors[0].message).toContain('tidak valid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid cuid for imageId', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
imageId: 'invalid-id',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('imageId');
|
||||
expect(result.error.errors[0].message).toContain('tidak valid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept valid YouTube URL for linkVideo', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
linkVideo: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid URL for linkVideo', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
linkVideo: 'not-a-url',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].path).toContain('linkVideo');
|
||||
expect(result.error.errors[0].message).toContain('tidak valid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept empty string for linkVideo', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
linkVideo: '',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept optional imageIds array with valid cuids', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
imageIds: ['clm5z8z8z000008l4f3qz8z8z', 'clm5z8z8z000008l4f3qz8z8y'],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject imageIds array with invalid cuid', () => {
|
||||
const result = createBeritaSchema.safeParse({
|
||||
...validData,
|
||||
imageIds: ['invalid-id'],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBeritaSchema', () => {
|
||||
it('should accept partial data for updates', () => {
|
||||
const result = updateBeritaSchema.safeParse({
|
||||
judul: 'Updated Title',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept empty object', () => {
|
||||
const result = updateBeritaSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should still validate provided fields', () => {
|
||||
const result = updateBeritaSchema.safeParse({
|
||||
judul: 'abc', // too short
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// OTP/Login Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('loginRequestSchema', () => {
|
||||
it('should accept valid phone number', () => {
|
||||
const result = loginRequestSchema.safeParse({
|
||||
nomor: '08123456789',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject phone number with less than 10 digits', () => {
|
||||
const result = loginRequestSchema.safeParse({
|
||||
nomor: '08123456',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('minimal 10 digit');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject phone number with more than 15 digits', () => {
|
||||
const result = loginRequestSchema.safeParse({
|
||||
nomor: '081234567890123456',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('maksimal 15 digit');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject phone number with non-numeric characters', () => {
|
||||
const result = loginRequestSchema.safeParse({
|
||||
nomor: '0812-3456-789',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('harus berupa angka');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject empty phone number', () => {
|
||||
const result = loginRequestSchema.safeParse({
|
||||
nomor: '',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('otpVerificationSchema', () => {
|
||||
it('should accept valid OTP verification data', () => {
|
||||
const result = otpVerificationSchema.safeParse({
|
||||
nomor: '08123456789',
|
||||
kodeId: 'clm5z8z8z000008l4f3qz8z8z',
|
||||
otp: '123456',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject OTP with wrong length', () => {
|
||||
const result = otpVerificationSchema.safeParse({
|
||||
nomor: '08123456789',
|
||||
kodeId: 'clm5z8z8z000008l4f3qz8z8z',
|
||||
otp: '12345',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('harus 6 digit');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject OTP with non-numeric characters', () => {
|
||||
const result = otpVerificationSchema.safeParse({
|
||||
nomor: '08123456789',
|
||||
kodeId: 'clm5z8z8z000008l4f3qz8z8z',
|
||||
otp: '12345a',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('harus berupa angka');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid kodeId', () => {
|
||||
const result = otpVerificationSchema.safeParse({
|
||||
nomor: '08123456789',
|
||||
kodeId: 'invalid-id',
|
||||
otp: '123456',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('tidak valid');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// File Upload Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('uploadFileSchema', () => {
|
||||
it('should accept valid file upload data', () => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'document.pdf',
|
||||
type: 'application/pdf',
|
||||
size: 1024 * 1024, // 1MB
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty file name', () => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: '',
|
||||
type: 'application/pdf',
|
||||
size: 1024 * 1024,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('wajib diisi');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept allowed image types', () => {
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
];
|
||||
|
||||
allowedTypes.forEach((type) => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'file.jpg',
|
||||
type,
|
||||
size: 1024 * 1024,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept allowed document types', () => {
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
];
|
||||
|
||||
allowedTypes.forEach((type) => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'document.doc',
|
||||
type,
|
||||
size: 1024 * 1024,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject disallowed file types', () => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'file.exe',
|
||||
type: 'application/x-executable',
|
||||
size: 1024 * 1024,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('tidak diizinkan');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject files larger than 5MB', () => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'largefile.pdf',
|
||||
type: 'application/pdf',
|
||||
size: 6 * 1024 * 1024, // 6MB
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('maksimal 5MB');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept files exactly 5MB', () => {
|
||||
const result = uploadFileSchema.safeParse({
|
||||
name: 'file.pdf',
|
||||
type: 'application/pdf',
|
||||
size: 5 * 1024 * 1024, // 5MB
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// User Registration Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('registerUserSchema', () => {
|
||||
it('should accept valid user registration data', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: '08123456789',
|
||||
email: 'john@example.com',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject short names (less than 3 characters)', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'Jo',
|
||||
nomor: '08123456789',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('minimal 3 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject long names (more than 100 characters)', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'a'.repeat(101),
|
||||
nomor: '08123456789',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('maksimal 100 karakter');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid phone numbers', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: 'invalid',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept empty email', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: '08123456789',
|
||||
email: '',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid email format', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: '08123456789',
|
||||
email: 'not-an-email',
|
||||
roleId: 1,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('tidak valid');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject non-integer roleId', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: '08123456789',
|
||||
email: 'john@example.com',
|
||||
roleId: 1.5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('angka bulat');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject non-positive roleId', () => {
|
||||
const result = registerUserSchema.safeParse({
|
||||
name: 'John Doe',
|
||||
nomor: '08123456789',
|
||||
email: 'john@example.com',
|
||||
roleId: 0,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('lebih dari 0');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Pagination Validation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('paginationSchema', () => {
|
||||
it('should accept default pagination values', () => {
|
||||
const result = paginationSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.page).toBe(1);
|
||||
expect(result.data.limit).toBe(10);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept custom page and limit', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
page: '5',
|
||||
limit: '25',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.page).toBe(5);
|
||||
expect(result.data.limit).toBe(25);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject page less than 1', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
page: '0',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('lebih dari 0');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject limit less than 1', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
limit: '0',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('antara 1-100');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject limit greater than 100', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
limit: '101',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.errors[0].message).toContain('antara 1-100');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept limit exactly 100', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
limit: '100',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept optional search parameter', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
search: 'test query',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.search).toBe('test query');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle invalid page numbers gracefully', () => {
|
||||
const result = paginationSchema.safeParse({
|
||||
page: 'abc',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user