UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

324 lines (274 loc) 9.46 kB
const express = require('express') const fs = require('fs') const path = require('path') /** * Internal API test server to replace json-server dependency * Provides REST API endpoints for testing CodeceptJS helpers */ class TestServer { constructor(config = {}) { this.app = express() this.server = null this.port = config.port || 8010 this.host = config.host || 'localhost' this.dbFile = config.dbFile || path.join(__dirname, '../test/data/rest/db.json') this.lastModified = null this.data = this.loadData() this.setupMiddleware() this.setupRoutes() this.setupFileWatcher() } loadData() { try { const content = fs.readFileSync(this.dbFile, 'utf8') const data = JSON.parse(content) // Update lastModified time when loading data if (fs.existsSync(this.dbFile)) { this.lastModified = fs.statSync(this.dbFile).mtime } console.log('[Data Load] Loaded data from file:', JSON.stringify(data)) return data } catch (err) { console.warn(`[Data Load] Could not load data file ${this.dbFile}:`, err.message) console.log('[Data Load] Using fallback default data') return { posts: [{ id: 1, title: 'json-server', author: 'davert' }], user: { name: 'john', password: '123456' }, } } } reloadData() { console.log('[Reload] Reloading data from file...') this.data = this.loadData() console.log('[Reload] Data reloaded successfully') return this.data } saveData() { try { fs.writeFileSync(this.dbFile, JSON.stringify(this.data, null, 2)) console.log('[Save] Data saved to file') // Force update modification time to ensure auto-reload works const now = new Date() fs.utimesSync(this.dbFile, now, now) this.lastModified = now console.log('[Save] File modification time updated') } catch (err) { console.warn(`[Save] Could not save data file ${this.dbFile}:`, err.message) } } setupMiddleware() { // Parse JSON bodies this.app.use(express.json()) // Parse URL-encoded bodies this.app.use(express.urlencoded({ extended: true })) // CORS support this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Test') if (req.method === 'OPTIONS') { res.status(200).end() return } next() }) // Auto-reload middleware - check if file changed before each request this.app.use((req, res, next) => { try { if (fs.existsSync(this.dbFile)) { const stats = fs.statSync(this.dbFile) if (!this.lastModified || stats.mtime > this.lastModified) { console.log(`[Auto-reload] Database file changed (${this.dbFile}), reloading data...`) console.log(`[Auto-reload] Old mtime: ${this.lastModified}, New mtime: ${stats.mtime}`) this.reloadData() this.lastModified = stats.mtime console.log(`[Auto-reload] Data reloaded, user name is now: ${this.data.user?.name}`) } } } catch (err) { console.warn('[Auto-reload] Error checking file modification time:', err.message) } next() }) // Logging middleware this.app.use((req, res, next) => { console.log(`${req.method} ${req.path}`) next() }) } setupRoutes() { // Reload endpoint (for testing) this.app.post('/_reload', (req, res) => { this.reloadData() res.json({ message: 'Data reloaded', data: this.data }) }) // Headers endpoint (for header testing) this.app.get('/headers', (req, res) => { res.json(req.headers) }) this.app.post('/headers', (req, res) => { res.json(req.headers) }) // User endpoints this.app.get('/user', (req, res) => { console.log(`[GET /user] Serving user data: ${JSON.stringify(this.data.user)}`) res.json(this.data.user) }) this.app.post('/user', (req, res) => { this.data.user = { ...this.data.user, ...req.body } this.saveData() res.status(201).json(this.data.user) }) this.app.patch('/user', (req, res) => { this.data.user = { ...this.data.user, ...req.body } this.saveData() res.json(this.data.user) }) this.app.put('/user', (req, res) => { this.data.user = req.body this.saveData() res.json(this.data.user) }) // Posts endpoints this.app.get('/posts', (req, res) => { res.json(this.data.posts) }) this.app.get('/posts/:id', (req, res) => { const id = parseInt(req.params.id) const post = this.data.posts.find(p => p.id === id) if (!post) { // Return empty object instead of 404 for json-server compatibility return res.json({}) } res.json(post) }) this.app.post('/posts', (req, res) => { const newId = Math.max(...this.data.posts.map(p => p.id || 0)) + 1 const newPost = { id: newId, ...req.body } this.data.posts.push(newPost) this.saveData() res.status(201).json(newPost) }) this.app.put('/posts/:id', (req, res) => { const id = parseInt(req.params.id) const postIndex = this.data.posts.findIndex(p => p.id === id) if (postIndex === -1) { return res.status(404).json({ error: 'Post not found' }) } this.data.posts[postIndex] = { id, ...req.body } this.saveData() res.json(this.data.posts[postIndex]) }) this.app.patch('/posts/:id', (req, res) => { const id = parseInt(req.params.id) const postIndex = this.data.posts.findIndex(p => p.id === id) if (postIndex === -1) { return res.status(404).json({ error: 'Post not found' }) } this.data.posts[postIndex] = { ...this.data.posts[postIndex], ...req.body } this.saveData() res.json(this.data.posts[postIndex]) }) this.app.delete('/posts/:id', (req, res) => { const id = parseInt(req.params.id) const postIndex = this.data.posts.findIndex(p => p.id === id) if (postIndex === -1) { return res.status(404).json({ error: 'Post not found' }) } const deletedPost = this.data.posts.splice(postIndex, 1)[0] this.saveData() res.json(deletedPost) }) // File upload endpoint (basic implementation) this.app.post('/upload', (req, res) => { // Simple upload simulation - for more complex file uploads, // multer would be needed but basic tests should work res.json({ message: 'File upload endpoint available', headers: req.headers, body: req.body, }) }) // Comments endpoints (for ApiDataFactory tests) this.app.get('/comments', (req, res) => { res.json(this.data.comments || []) }) this.app.post('/comments', (req, res) => { if (!this.data.comments) this.data.comments = [] const newId = Math.max(...this.data.comments.map(c => c.id || 0), 0) + 1 const newComment = { id: newId, ...req.body } this.data.comments.push(newComment) this.saveData() res.status(201).json(newComment) }) this.app.delete('/comments/:id', (req, res) => { if (!this.data.comments) this.data.comments = [] const id = parseInt(req.params.id) const commentIndex = this.data.comments.findIndex(c => c.id === id) if (commentIndex === -1) { return res.status(404).json({ error: 'Comment not found' }) } const deletedComment = this.data.comments.splice(commentIndex, 1)[0] this.saveData() res.json(deletedComment) }) // Generic catch-all for other endpoints this.app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found' }) }) } setupFileWatcher() { if (fs.existsSync(this.dbFile)) { fs.watchFile(this.dbFile, (current, previous) => { if (current.mtime !== previous.mtime) { console.log('Database file changed, reloading data...') this.reloadData() } }) } } start() { return new Promise((resolve, reject) => { this.server = this.app.listen(this.port, this.host, err => { if (err) { reject(err) } else { console.log(`Test server running on http://${this.host}:${this.port}`) resolve(this.server) } }) }) } stop() { return new Promise(resolve => { if (this.server) { this.server.close(() => { console.log('Test server stopped') resolve() }) } else { resolve() } }) } } module.exports = TestServer // CLI usage if (require.main === module) { const config = { port: process.env.PORT || 8010, host: process.env.HOST || '0.0.0.0', dbFile: process.argv[2] || path.join(__dirname, '../test/data/rest/db.json'), } const server = new TestServer(config) server.start().catch(console.error) // Graceful shutdown process.on('SIGINT', () => { console.log('\nShutting down test server...') server.stop().then(() => process.exit(0)) }) process.on('SIGTERM', () => { console.log('\nShutting down test server...') server.stop().then(() => process.exit(0)) }) }