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

663 lines
18 KiB
JavaScript

#!/usr/bin/env node
import express from 'express';
import { DebrosFramework } from '../../../../src/framework/DebrosFramework';
import { User, UserProfile, Category, Post, Comment } from '../models/BlogModels';
import { BlogValidation, ValidationError } from '../models/BlogValidation';
class BlogAPIServer {
private app: express.Application;
private framework: DebrosFramework;
private nodeId: string;
constructor() {
this.app = express();
this.nodeId = process.env.NODE_ID || 'blog-node';
this.setupMiddleware();
this.setupRoutes();
}
private setupMiddleware() {
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true }));
// CORS
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// Logging
this.app.use((req, res, next) => {
console.log(`[${this.nodeId}] ${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
// Error handling
this.app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
nodeId: this.nodeId
});
}
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId
});
});
}
private setupRoutes() {
// Health check
this.app.get('/health', async (req, res) => {
try {
const peers = await this.getConnectedPeerCount();
res.json({
status: 'healthy',
nodeId: this.nodeId,
peers,
timestamp: Date.now()
});
} catch (error) {
res.status(500).json({
status: 'unhealthy',
nodeId: this.nodeId,
error: error.message
});
}
});
// API routes
this.setupUserRoutes();
this.setupCategoryRoutes();
this.setupPostRoutes();
this.setupCommentRoutes();
this.setupMetricsRoutes();
}
private setupUserRoutes() {
// Create user
this.app.post('/api/users', async (req, res, next) => {
try {
const sanitizedData = BlogValidation.sanitizeUserInput(req.body);
BlogValidation.validateUser(sanitizedData);
const user = await User.create(sanitizedData);
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
res.status(201).json(user.toJSON());
} catch (error) {
next(error);
}
});
// Get user by ID
this.app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
});
}
res.json(user.toJSON());
} catch (error) {
next(error);
}
});
// Get all users
this.app.get('/api/users', async (req, res, next) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
const search = req.query.search as string;
let query = User.query();
if (search) {
query = query.where('username', 'like', `%${search}%`)
.orWhere('displayName', 'like', `%${search}%`);
}
const users = await query
.orderBy('createdAt', 'desc')
.limit(limit)
.offset((page - 1) * limit)
.find();
res.json({
users: users.map(u => u.toJSON()),
page,
limit,
nodeId: this.nodeId
});
} catch (error) {
next(error);
}
});
// Update user
this.app.put('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
});
}
// Only allow updating certain fields
const allowedFields = ['displayName', 'avatar', 'roles'];
const updateData: any = {};
allowedFields.forEach(field => {
if (req.body[field] !== undefined) {
updateData[field] = req.body[field];
}
});
Object.assign(user, updateData);
await user.save();
console.log(`[${this.nodeId}] Updated user: ${user.username}`);
res.json(user.toJSON());
} catch (error) {
next(error);
}
});
// User login (update last login)
this.app.post('/api/users/:id/login', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
});
}
await user.updateLastLogin();
res.json({ message: 'Login recorded', lastLoginAt: user.lastLoginAt });
} catch (error) {
next(error);
}
});
}
private setupCategoryRoutes() {
// Create category
this.app.post('/api/categories', async (req, res, next) => {
try {
const sanitizedData = BlogValidation.sanitizeCategoryInput(req.body);
BlogValidation.validateCategory(sanitizedData);
const category = await Category.create(sanitizedData);
console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`);
res.status(201).json(category);
} catch (error) {
next(error);
}
});
// Get all categories
this.app.get('/api/categories', async (req, res, next) => {
try {
const categories = await Category.query()
.where('isActive', true)
.orderBy('name', 'asc')
.find();
res.json({
categories,
nodeId: this.nodeId
});
} catch (error) {
next(error);
}
});
// Get category by ID
this.app.get('/api/categories/:id', async (req, res, next) => {
try {
const category = await Category.findById(req.params.id);
if (!category) {
return res.status(404).json({
error: 'Category not found',
nodeId: this.nodeId
});
}
res.json(category);
} catch (error) {
next(error);
}
});
}
private setupPostRoutes() {
// Create post
this.app.post('/api/posts', async (req, res, next) => {
try {
const sanitizedData = BlogValidation.sanitizePostInput(req.body);
BlogValidation.validatePost(sanitizedData);
const post = await Post.create(sanitizedData);
console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`);
res.status(201).json(post);
} catch (error) {
next(error);
}
});
// Get post by ID with relationships
this.app.get('/api/posts/:id', async (req, res, next) => {
try {
const post = await Post.query()
.where('id', req.params.id)
.with(['author', 'category', 'comments'])
.first();
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
res.json(post);
} catch (error) {
next(error);
}
});
// Get all posts with pagination and filters
this.app.get('/api/posts', async (req, res, next) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50);
const status = req.query.status as string;
const authorId = req.query.authorId as string;
const categoryId = req.query.categoryId as string;
const tag = req.query.tag as string;
let query = Post.query().with(['author', 'category']);
if (status) {
query = query.where('status', status);
}
if (authorId) {
query = query.where('authorId', authorId);
}
if (categoryId) {
query = query.where('categoryId', categoryId);
}
if (tag) {
query = query.where('tags', 'includes', tag);
}
const posts = await query
.orderBy('createdAt', 'desc')
.limit(limit)
.offset((page - 1) * limit)
.find();
res.json({
posts,
page,
limit,
nodeId: this.nodeId
});
} catch (error) {
next(error);
}
});
// Update post
this.app.put('/api/posts/:id', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
BlogValidation.validatePostUpdate(req.body);
Object.assign(post, req.body);
post.updatedAt = Date.now();
await post.save();
console.log(`[${this.nodeId}] Updated post: ${post.title}`);
res.json(post);
} catch (error) {
next(error);
}
});
// Publish post
this.app.post('/api/posts/:id/publish', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
await post.publish();
console.log(`[${this.nodeId}] Published post: ${post.title}`);
res.json(post);
} catch (error) {
next(error);
}
});
// Unpublish post
this.app.post('/api/posts/:id/unpublish', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
await post.unpublish();
console.log(`[${this.nodeId}] Unpublished post: ${post.title}`);
res.json(post);
} catch (error) {
next(error);
}
});
// Like post
this.app.post('/api/posts/:id/like', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
await post.like();
res.json({ likeCount: post.likeCount });
} catch (error) {
next(error);
}
});
// View post (increment view count)
this.app.post('/api/posts/:id/view', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
});
}
await post.incrementViews();
res.json({ viewCount: post.viewCount });
} catch (error) {
next(error);
}
});
}
private setupCommentRoutes() {
// Create comment
this.app.post('/api/comments', async (req, res, next) => {
try {
const sanitizedData = BlogValidation.sanitizeCommentInput(req.body);
BlogValidation.validateComment(sanitizedData);
const comment = await Comment.create(sanitizedData);
console.log(`[${this.nodeId}] Created comment on post ${comment.postId} by ${comment.authorId}`);
res.status(201).json(comment);
} catch (error) {
next(error);
}
});
// Get comments for a post
this.app.get('/api/posts/:postId/comments', async (req, res, next) => {
try {
const comments = await Comment.query()
.where('postId', req.params.postId)
.where('isApproved', true)
.with(['author'])
.orderBy('createdAt', 'asc')
.find();
res.json({
comments,
nodeId: this.nodeId
});
} catch (error) {
next(error);
}
});
// Approve comment
this.app.post('/api/comments/:id/approve', async (req, res, next) => {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({
error: 'Comment not found',
nodeId: this.nodeId
});
}
await comment.approve();
console.log(`[${this.nodeId}] Approved comment ${comment.id}`);
res.json(comment);
} catch (error) {
next(error);
}
});
// Like comment
this.app.post('/api/comments/:id/like', async (req, res, next) => {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({
error: 'Comment not found',
nodeId: this.nodeId
});
}
await comment.like();
res.json({ likeCount: comment.likeCount });
} catch (error) {
next(error);
}
});
}
private setupMetricsRoutes() {
// Network metrics
this.app.get('/api/metrics/network', async (req, res, next) => {
try {
const peers = await this.getConnectedPeerCount();
res.json({
nodeId: this.nodeId,
peers,
timestamp: Date.now()
});
} catch (error) {
next(error);
}
});
// Data metrics
this.app.get('/api/metrics/data', async (req, res, next) => {
try {
const [userCount, postCount, commentCount, categoryCount] = await Promise.all([
User.count(),
Post.count(),
Comment.count(),
Category.count()
]);
res.json({
nodeId: this.nodeId,
counts: {
users: userCount,
posts: postCount,
comments: commentCount,
categories: categoryCount
},
timestamp: Date.now()
});
} catch (error) {
next(error);
}
});
// Framework metrics
this.app.get('/api/metrics/framework', async (req, res, next) => {
try {
const metrics = this.framework.getMetrics();
res.json({
nodeId: this.nodeId,
...metrics,
timestamp: Date.now()
});
} catch (error) {
next(error);
}
});
}
private async getConnectedPeerCount(): Promise<number> {
try {
if (this.framework) {
const ipfsService = this.framework.getIPFSService();
if (ipfsService) {
const peers = await ipfsService.getConnectedPeers();
return peers.size;
}
}
return 0;
} catch (error) {
console.warn(`[${this.nodeId}] Failed to get peer count:`, error.message);
return 0;
}
}
async start() {
try {
console.log(`[${this.nodeId}] Starting Blog API Server...`);
// Wait for dependencies
await this.waitForDependencies();
// Initialize framework
await this.initializeFramework();
// Start HTTP server
const port = process.env.NODE_PORT || 3000;
this.app.listen(port, () => {
console.log(`[${this.nodeId}] Blog API server listening on port ${port}`);
console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`);
});
} catch (error) {
console.error(`[${this.nodeId}] Failed to start:`, error);
process.exit(1);
}
}
private async waitForDependencies(): Promise<void> {
// In a real deployment, you might wait for database connections, etc.
console.log(`[${this.nodeId}] Dependencies ready`);
}
private async initializeFramework(): Promise<void> {
// Import services - adjust paths based on your actual service locations
// Note: You'll need to implement these services or use existing ones
const IPFSService = (await import('../../../../src/framework/services/IPFSService')).IPFSService;
const OrbitDBService = (await import('../../../../src/framework/services/OrbitDBService')).OrbitDBService;
// Initialize IPFS service
const ipfsService = new IPFSService({
swarmKeyFile: process.env.SWARM_KEY_FILE,
bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [],
ports: {
swarm: parseInt(process.env.IPFS_PORT) || 4001
}
});
await ipfsService.init();
console.log(`[${this.nodeId}] IPFS service initialized`);
// Initialize OrbitDB service
const orbitDBService = new OrbitDBService(ipfsService);
await orbitDBService.init();
console.log(`[${this.nodeId}] OrbitDB service initialized`);
// Initialize framework
this.framework = new DebrosFramework({
environment: 'test',
features: {
autoMigration: true,
automaticPinning: true,
pubsub: true,
queryCache: true,
relationshipCache: true
},
performance: {
queryTimeout: 10000,
maxConcurrentOperations: 20,
batchSize: 50
}
});
await this.framework.initialize(orbitDBService, ipfsService);
console.log(`[${this.nodeId}] DebrosFramework initialized successfully`);
}
}
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down gracefully...');
process.exit(0);
});
// Start the server
const server = new BlogAPIServer();
server.start();