- 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>
556 lines
16 KiB
TypeScript
556 lines
16 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|