anonpenguin 64ed9e82a7 Add integration tests for blog workflow and supporting infrastructure
- Implemented comprehensive integration tests for user management, category management, content publishing, comment system, performance, scalability, and network resilience.
- Created DockerNodeManager to manage Docker containers for testing.
- Developed ApiClient for API interactions and health checks.
- Introduced SyncWaiter for synchronizing node states and ensuring data consistency.
- Enhanced test reliability with increased timeouts and detailed logging.
2025-06-21 11:15:33 +03:00

216 lines
7.6 KiB
TypeScript

import { CreateUserRequest, CreateCategoryRequest, CreatePostRequest, CreateCommentRequest, UpdatePostRequest } from './BlogModels';
export class ValidationError extends Error {
constructor(message: string, public field?: string) {
super(message);
this.name = 'ValidationError';
}
}
export class BlogValidation {
static validateUser(data: CreateUserRequest): void {
if (!data.username || data.username.length < 3 || data.username.length > 30) {
throw new ValidationError('Username must be between 3 and 30 characters', 'username');
}
if (!/^[a-zA-Z0-9_]+$/.test(data.username)) {
throw new ValidationError('Username can only contain letters, numbers, and underscores', 'username');
}
if (!data.email || !this.isValidEmail(data.email)) {
throw new ValidationError('Valid email is required', 'email');
}
if (data.displayName && data.displayName.length > 100) {
throw new ValidationError('Display name cannot exceed 100 characters', 'displayName');
}
if (data.avatar && !this.isValidUrl(data.avatar)) {
throw new ValidationError('Avatar must be a valid URL', 'avatar');
}
if (data.roles && !Array.isArray(data.roles)) {
throw new ValidationError('Roles must be an array', 'roles');
}
}
static validateCategory(data: CreateCategoryRequest): void {
if (!data.name || data.name.length < 2 || data.name.length > 50) {
throw new ValidationError('Category name must be between 2 and 50 characters', 'name');
}
if (data.description && data.description.length > 500) {
throw new ValidationError('Description cannot exceed 500 characters', 'description');
}
if (data.color && !/^#[0-9A-Fa-f]{6}$/.test(data.color)) {
throw new ValidationError('Color must be a valid hex color code', 'color');
}
}
static validatePost(data: CreatePostRequest): void {
if (!data.title || data.title.length < 3 || data.title.length > 200) {
throw new ValidationError('Title must be between 3 and 200 characters', 'title');
}
if (!data.content || data.content.length < 10) {
throw new ValidationError('Content must be at least 10 characters long', 'content');
}
if (data.content.length > 50000) {
throw new ValidationError('Content cannot exceed 50,000 characters', 'content');
}
if (!data.authorId) {
throw new ValidationError('Author ID is required', 'authorId');
}
if (data.excerpt && data.excerpt.length > 300) {
throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt');
}
if (data.tags && !Array.isArray(data.tags)) {
throw new ValidationError('Tags must be an array', 'tags');
}
if (data.tags && data.tags.length > 10) {
throw new ValidationError('Cannot have more than 10 tags', 'tags');
}
if (data.tags) {
for (const tag of data.tags) {
if (typeof tag !== 'string' || tag.length > 30) {
throw new ValidationError('Each tag must be a string with max 30 characters', 'tags');
}
}
}
if (data.featuredImage && !this.isValidUrl(data.featuredImage)) {
throw new ValidationError('Featured image must be a valid URL', 'featuredImage');
}
if (data.status && !['draft', 'published'].includes(data.status)) {
throw new ValidationError('Status must be either "draft" or "published"', 'status');
}
}
static validatePostUpdate(data: UpdatePostRequest): void {
if (data.title && (data.title.length < 3 || data.title.length > 200)) {
throw new ValidationError('Title must be between 3 and 200 characters', 'title');
}
if (data.content && (data.content.length < 10 || data.content.length > 50000)) {
throw new ValidationError('Content must be between 10 and 50,000 characters', 'content');
}
if (data.excerpt && data.excerpt.length > 300) {
throw new ValidationError('Excerpt cannot exceed 300 characters', 'excerpt');
}
if (data.tags && !Array.isArray(data.tags)) {
throw new ValidationError('Tags must be an array', 'tags');
}
if (data.tags && data.tags.length > 10) {
throw new ValidationError('Cannot have more than 10 tags', 'tags');
}
if (data.tags) {
for (const tag of data.tags) {
if (typeof tag !== 'string' || tag.length > 30) {
throw new ValidationError('Each tag must be a string with max 30 characters', 'tags');
}
}
}
if (data.featuredImage && !this.isValidUrl(data.featuredImage)) {
throw new ValidationError('Featured image must be a valid URL', 'featuredImage');
}
if (data.isFeatured !== undefined && typeof data.isFeatured !== 'boolean') {
throw new ValidationError('isFeatured must be a boolean', 'isFeatured');
}
}
static validateComment(data: CreateCommentRequest): void {
if (!data.content || data.content.length < 1 || data.content.length > 2000) {
throw new ValidationError('Comment must be between 1 and 2000 characters', 'content');
}
if (!data.postId) {
throw new ValidationError('Post ID is required', 'postId');
}
if (!data.authorId) {
throw new ValidationError('Author ID is required', 'authorId');
}
// parentId is optional, but if provided should be a string
if (data.parentId !== undefined && typeof data.parentId !== 'string') {
throw new ValidationError('Parent ID must be a string', 'parentId');
}
}
private static isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
private static isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
// Sanitization helpers
static sanitizeString(input: string): string {
return input.trim().replace(/[<>]/g, '');
}
static sanitizeArray(input: string[]): string[] {
return input.map(item => this.sanitizeString(item)).filter(item => item.length > 0);
}
static sanitizeUserInput(data: CreateUserRequest): CreateUserRequest {
return {
username: this.sanitizeString(data.username),
email: this.sanitizeString(data.email.toLowerCase()),
displayName: data.displayName ? this.sanitizeString(data.displayName) : undefined,
avatar: data.avatar ? this.sanitizeString(data.avatar) : undefined,
roles: data.roles ? this.sanitizeArray(data.roles) : undefined
};
}
static sanitizeCategoryInput(data: CreateCategoryRequest): CreateCategoryRequest {
return {
name: this.sanitizeString(data.name),
description: data.description ? this.sanitizeString(data.description) : undefined,
color: data.color ? this.sanitizeString(data.color) : undefined
};
}
static sanitizePostInput(data: CreatePostRequest): CreatePostRequest {
return {
title: this.sanitizeString(data.title),
content: data.content.trim(), // Don't sanitize content too aggressively
excerpt: data.excerpt ? this.sanitizeString(data.excerpt) : undefined,
authorId: this.sanitizeString(data.authorId),
categoryId: data.categoryId ? this.sanitizeString(data.categoryId) : undefined,
tags: data.tags ? this.sanitizeArray(data.tags) : undefined,
featuredImage: data.featuredImage ? this.sanitizeString(data.featuredImage) : undefined,
status: data.status
};
}
static sanitizeCommentInput(data: CreateCommentRequest): CreateCommentRequest {
return {
content: data.content.trim(),
postId: this.sanitizeString(data.postId),
authorId: this.sanitizeString(data.authorId),
parentId: data.parentId ? this.sanitizeString(data.parentId) : undefined
};
}
}