|
1 | | -import express from 'express' |
2 | | -import fs from 'fs' |
3 | | -import path from 'path' |
4 | | - |
5 | | -/** |
6 | | - * Internal API test server to replace json-server dependency |
7 | | - * Provides REST API endpoints for testing CodeceptJS helpers |
8 | | - */ |
9 | | -class TestServer { |
10 | | - constructor(config = {}) { |
11 | | - this.app = express() |
12 | | - this.server = null |
13 | | - this.port = config.port || 8010 |
14 | | - this.host = config.host || 'localhost' |
15 | | - this.dbFile = config.dbFile || path.join(__dirname, '../test/data/rest/db.json') |
16 | | - this.readOnly = config.readOnly || false |
17 | | - this.lastModified = null |
18 | | - this.data = this.loadData() |
19 | | - |
20 | | - this.setupMiddleware() |
21 | | - this.setupRoutes() |
22 | | - this.setupFileWatcher() |
23 | | - } |
24 | | - |
25 | | - loadData() { |
26 | | - try { |
27 | | - const content = fs.readFileSync(this.dbFile, 'utf8') |
28 | | - const data = JSON.parse(content) |
29 | | - // Update lastModified time when loading data |
30 | | - if (fs.existsSync(this.dbFile)) { |
31 | | - this.lastModified = fs.statSync(this.dbFile).mtime |
32 | | - } |
33 | | - console.log('[Data Load] Loaded data from file:', JSON.stringify(data)) |
34 | | - return data |
35 | | - } catch (err) { |
36 | | - console.warn(`[Data Load] Could not load data file ${this.dbFile}:`, err.message) |
37 | | - console.log('[Data Load] Using fallback default data') |
38 | | - return { |
39 | | - posts: [{ id: 1, title: 'json-server', author: 'davert' }], |
40 | | - user: { name: 'john', password: '123456' }, |
41 | | - } |
42 | | - } |
43 | | - } |
44 | | - |
45 | | - reloadData() { |
46 | | - console.log('[Reload] Reloading data from file...') |
47 | | - this.data = this.loadData() |
48 | | - console.log('[Reload] Data reloaded successfully') |
49 | | - return this.data |
50 | | - } |
51 | | - |
52 | | - saveData() { |
53 | | - if (this.readOnly) { |
54 | | - console.log('[Save] Skipping save - running in read-only mode') |
55 | | - return |
56 | | - } |
57 | | - try { |
58 | | - fs.writeFileSync(this.dbFile, JSON.stringify(this.data, null, 2)) |
59 | | - console.log('[Save] Data saved to file') |
60 | | - // Force update modification time to ensure auto-reload works |
61 | | - const now = new Date() |
62 | | - fs.utimesSync(this.dbFile, now, now) |
63 | | - this.lastModified = now |
64 | | - console.log('[Save] File modification time updated') |
65 | | - } catch (err) { |
66 | | - console.warn(`[Save] Could not save data file ${this.dbFile}:`, err.message) |
67 | | - } |
68 | | - } |
69 | | - |
70 | | - setupMiddleware() { |
71 | | - // Parse JSON bodies |
72 | | - this.app.use(express.json()) |
73 | | - |
74 | | - // Parse URL-encoded bodies |
75 | | - this.app.use(express.urlencoded({ extended: true })) |
76 | | - |
77 | | - // CORS support |
78 | | - this.app.use((req, res, next) => { |
79 | | - res.header('Access-Control-Allow-Origin', '*') |
80 | | - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS') |
81 | | - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Test') |
82 | | - |
83 | | - if (req.method === 'OPTIONS') { |
84 | | - res.status(200).end() |
85 | | - return |
86 | | - } |
87 | | - next() |
88 | | - }) |
89 | | - |
90 | | - // Auto-reload middleware - check if file changed before each request |
91 | | - this.app.use((req, res, next) => { |
92 | | - try { |
93 | | - if (fs.existsSync(this.dbFile)) { |
94 | | - const stats = fs.statSync(this.dbFile) |
95 | | - if (!this.lastModified || stats.mtime > this.lastModified) { |
96 | | - console.log(`[Auto-reload] Database file changed (${this.dbFile}), reloading data...`) |
97 | | - console.log(`[Auto-reload] Old mtime: ${this.lastModified}, New mtime: ${stats.mtime}`) |
98 | | - this.reloadData() |
99 | | - this.lastModified = stats.mtime |
100 | | - console.log(`[Auto-reload] Data reloaded, user name is now: ${this.data.user?.name}`) |
101 | | - } |
102 | | - } |
103 | | - } catch (err) { |
104 | | - console.warn('[Auto-reload] Error checking file modification time:', err.message) |
105 | | - } |
106 | | - next() |
107 | | - }) |
108 | | - |
109 | | - // Logging middleware |
110 | | - this.app.use((req, res, next) => { |
111 | | - console.log(`${req.method} ${req.path}`) |
112 | | - next() |
113 | | - }) |
114 | | - } |
115 | | - |
116 | | - setupRoutes() { |
117 | | - // Reload endpoint (for testing) |
118 | | - this.app.post('/_reload', (req, res) => { |
119 | | - this.reloadData() |
120 | | - res.json({ message: 'Data reloaded', data: this.data }) |
121 | | - }) |
122 | | - |
123 | | - // Headers endpoint (for header testing) |
124 | | - this.app.get('/headers', (req, res) => { |
125 | | - res.json(req.headers) |
126 | | - }) |
127 | | - |
128 | | - this.app.post('/headers', (req, res) => { |
129 | | - res.json(req.headers) |
130 | | - }) |
131 | | - |
132 | | - // User endpoints |
133 | | - this.app.get('/user', (req, res) => { |
134 | | - console.log(`[GET /user] Serving user data: ${JSON.stringify(this.data.user)}`) |
135 | | - res.json(this.data.user) |
136 | | - }) |
137 | | - |
138 | | - this.app.post('/user', (req, res) => { |
139 | | - this.data.user = { ...this.data.user, ...req.body } |
140 | | - this.saveData() |
141 | | - res.status(201).json(this.data.user) |
142 | | - }) |
143 | | - |
144 | | - this.app.patch('/user', (req, res) => { |
145 | | - this.data.user = { ...this.data.user, ...req.body } |
146 | | - this.saveData() |
147 | | - res.json(this.data.user) |
148 | | - }) |
149 | | - |
150 | | - this.app.put('/user', (req, res) => { |
151 | | - this.data.user = req.body |
152 | | - this.saveData() |
153 | | - res.json(this.data.user) |
154 | | - }) |
155 | | - |
156 | | - // Posts endpoints |
157 | | - this.app.get('/posts', (req, res) => { |
158 | | - res.json(this.data.posts) |
159 | | - }) |
160 | | - |
161 | | - this.app.get('/posts/:id', (req, res) => { |
162 | | - const id = parseInt(req.params.id) |
163 | | - const post = this.data.posts.find(p => p.id === id) |
164 | | - |
165 | | - if (!post) { |
166 | | - // Return empty object instead of 404 for json-server compatibility |
167 | | - return res.json({}) |
168 | | - } |
169 | | - |
170 | | - res.json(post) |
171 | | - }) |
172 | | - |
173 | | - this.app.post('/posts', (req, res) => { |
174 | | - const newId = Math.max(...this.data.posts.map(p => p.id || 0)) + 1 |
175 | | - const newPost = { id: newId, ...req.body } |
176 | | - |
177 | | - this.data.posts.push(newPost) |
178 | | - this.saveData() |
179 | | - res.status(201).json(newPost) |
180 | | - }) |
181 | | - |
182 | | - this.app.put('/posts/:id', (req, res) => { |
183 | | - const id = parseInt(req.params.id) |
184 | | - const postIndex = this.data.posts.findIndex(p => p.id === id) |
185 | | - |
186 | | - if (postIndex === -1) { |
187 | | - return res.status(404).json({ error: 'Post not found' }) |
188 | | - } |
189 | | - |
190 | | - this.data.posts[postIndex] = { id, ...req.body } |
191 | | - this.saveData() |
192 | | - res.json(this.data.posts[postIndex]) |
193 | | - }) |
194 | | - |
195 | | - this.app.patch('/posts/:id', (req, res) => { |
196 | | - const id = parseInt(req.params.id) |
197 | | - const postIndex = this.data.posts.findIndex(p => p.id === id) |
198 | | - |
199 | | - if (postIndex === -1) { |
200 | | - return res.status(404).json({ error: 'Post not found' }) |
201 | | - } |
202 | | - |
203 | | - this.data.posts[postIndex] = { ...this.data.posts[postIndex], ...req.body } |
204 | | - this.saveData() |
205 | | - res.json(this.data.posts[postIndex]) |
206 | | - }) |
207 | | - |
208 | | - this.app.delete('/posts/:id', (req, res) => { |
209 | | - const id = parseInt(req.params.id) |
210 | | - const postIndex = this.data.posts.findIndex(p => p.id === id) |
211 | | - |
212 | | - if (postIndex === -1) { |
213 | | - return res.status(404).json({ error: 'Post not found' }) |
214 | | - } |
215 | | - |
216 | | - const deletedPost = this.data.posts.splice(postIndex, 1)[0] |
217 | | - this.saveData() |
218 | | - res.json(deletedPost) |
219 | | - }) |
220 | | - |
221 | | - // File upload endpoint (basic implementation) |
222 | | - this.app.post('/upload', (req, res) => { |
223 | | - // Simple upload simulation - for more complex file uploads, |
224 | | - // multer would be needed but basic tests should work |
225 | | - res.json({ |
226 | | - message: 'File upload endpoint available', |
227 | | - headers: req.headers, |
228 | | - body: req.body, |
229 | | - }) |
230 | | - }) |
231 | | - |
232 | | - // Comments endpoints (for ApiDataFactory tests) |
233 | | - this.app.get('/comments', (req, res) => { |
234 | | - res.json(this.data.comments || []) |
235 | | - }) |
236 | | - |
237 | | - this.app.post('/comments', (req, res) => { |
238 | | - if (!this.data.comments) this.data.comments = [] |
239 | | - const newId = Math.max(...this.data.comments.map(c => c.id || 0), 0) + 1 |
240 | | - const newComment = { id: newId, ...req.body } |
241 | | - |
242 | | - this.data.comments.push(newComment) |
243 | | - this.saveData() |
244 | | - res.status(201).json(newComment) |
245 | | - }) |
246 | | - |
247 | | - this.app.delete('/comments/:id', (req, res) => { |
248 | | - if (!this.data.comments) this.data.comments = [] |
249 | | - const id = parseInt(req.params.id) |
250 | | - const commentIndex = this.data.comments.findIndex(c => c.id === id) |
251 | | - |
252 | | - if (commentIndex === -1) { |
253 | | - return res.status(404).json({ error: 'Comment not found' }) |
254 | | - } |
255 | | - |
256 | | - const deletedComment = this.data.comments.splice(commentIndex, 1)[0] |
257 | | - this.saveData() |
258 | | - res.json(deletedComment) |
259 | | - }) |
260 | | - |
261 | | - // Generic catch-all for other endpoints |
262 | | - this.app.use((req, res) => { |
263 | | - res.status(404).json({ error: 'Endpoint not found' }) |
264 | | - }) |
265 | | - } |
266 | | - |
267 | | - setupFileWatcher() { |
268 | | - if (fs.existsSync(this.dbFile)) { |
269 | | - fs.watchFile(this.dbFile, (current, previous) => { |
270 | | - if (current.mtime !== previous.mtime) { |
271 | | - console.log('Database file changed, reloading data...') |
272 | | - this.reloadData() |
273 | | - } |
274 | | - }) |
275 | | - } |
276 | | - } |
277 | | - |
278 | | - start() { |
279 | | - return new Promise((resolve, reject) => { |
280 | | - this.server = this.app.listen(this.port, this.host, err => { |
281 | | - if (err) { |
282 | | - reject(err) |
283 | | - } else { |
284 | | - console.log(`Test server running on http://${this.host}:${this.port}`) |
285 | | - resolve(this.server) |
286 | | - } |
287 | | - }) |
288 | | - }) |
289 | | - } |
290 | | - |
291 | | - stop() { |
292 | | - return new Promise(resolve => { |
293 | | - if (this.server) { |
294 | | - this.server.close(() => { |
295 | | - console.log('Test server stopped') |
296 | | - resolve() |
297 | | - }) |
298 | | - } else { |
299 | | - resolve() |
300 | | - } |
301 | | - }) |
302 | | - } |
303 | | -} |
304 | | - |
305 | | -export default TestServer |
306 | | - |
307 | | -// CLI usage - Import meta for ESM |
308 | | -import { fileURLToPath } from 'url' |
309 | | -import { dirname } from 'path' |
310 | | - |
311 | | -const __filename = fileURLToPath(import.meta.url) |
312 | | -const __dirname = dirname(__filename) |
313 | | - |
314 | | -if (import.meta.url === `file://${process.argv[1]}`) { |
315 | | - const config = { |
316 | | - port: process.env.PORT || 8010, |
317 | | - host: process.env.HOST || '0.0.0.0', |
318 | | - dbFile: process.argv[2] || path.join(__dirname, '../test/data/rest/db.json'), |
319 | | - } |
320 | | - |
321 | | - const server = new TestServer(config) |
322 | | - server.start().catch(console.error) |
323 | | - |
324 | | - // Graceful shutdown |
325 | | - process.on('SIGINT', () => { |
326 | | - console.log('\nShutting down test server...') |
327 | | - server.stop().then(() => process.exit(0)) |
328 | | - }) |
329 | | - |
330 | | - process.on('SIGTERM', () => { |
331 | | - console.log('\nShutting down test server...') |
332 | | - server.stop().then(() => process.exit(0)) |
333 | | - }) |
334 | | -} |
0 commit comments