Skip to content

Commit 5699dff

Browse files
committed
Fix REST tests - add missing setup.js import for chai.should()
- REST_test.js was failing with 'Cannot read properties of undefined (reading 'eql')' - Added import '../support/setup.js' which initializes chai.should() - Restored test-server.js file which was accidentally emptied - All REST tests now pass successfully
1 parent c4b3090 commit 5699dff

File tree

2 files changed

+338
-4
lines changed

2 files changed

+338
-4
lines changed

lib/test-server.js

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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+
}

test/rest/REST_test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import path from 'path'
2-
import { expect } from 'expect'
32
import fs from 'fs'
43
import { fileURLToPath } from 'url'
54

5+
const __filename = fileURLToPath(import.meta.url)
6+
const __dirname = path.dirname(__filename)
7+
8+
import '../support/setup.js'
69
import TestHelper from '../support/TestHelper.js'
710
import REST from '../../lib/helper/REST.js'
811
import Container from '../../lib/container.js'
912
import Secret from '../../lib/secret.js'
1013

11-
const __filename = fileURLToPath(import.meta.url)
12-
const __dirname = path.dirname(__filename)
13-
1414
const api_url = TestHelper.jsonServerUrl()
1515
import * as codeceptjs from '../../lib/index.js'
1616
global.codeceptjs = codeceptjs.default || codeceptjs

0 commit comments

Comments
 (0)