qmemory
Version:
A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli
138 lines (118 loc) • 5.39 kB
JavaScript
/**
* Integration tests for the demo Express application
* Uses supertest to verify REST endpoints behave as expected.
*/
const request = require('supertest');
const { testHelpers } = require('qtests/lib/envUtils.js');
let server; // will hold HTTP server instance across tests
let agent; // supertest agent for making requests
beforeAll(async () => { // start demo app once for all tests
process.env.NODE_ENV = 'test'; // ensure development behavior without demo data
jest.resetModules(); // clear module cache before mocking
jest.doMock('../../index', () => { // mock missing HTTP utilities for demo app
const actual = jest.requireActual('../../index'); // retain existing exports
return {
...actual,
sendSuccess: (res, msg, data) => res.status(200).json({ message: msg, data, timestamp: new Date().toISOString() }), // simplified success helper for tests
sendBadRequest: (res, msg) => res.status(400).json({ message: msg, timestamp: new Date().toISOString() }), // simplified 400 helper for validation errors
logInfo: jest.fn(), // stub logging to keep test output clean
logError: jest.fn() // stub logging to keep test output clean
};
});
const { app } = require('../../demo-app'); // import app after mocks applied
server = app.listen(0); // random available port prevents conflicts
agent = request.agent(server); // reuse agent for all tests
});
afterAll(done => { // close server after all tests complete
server.close(done); // ensures port is released
});
beforeEach(async () => { // reset demo storage between tests
await agent.post('/users/clear');
});
describe('Demo App API', () => {
test('GET / returns API index information', async () => {
const res = await agent.get('/');
expect(res.status).toBe(200);
expect(res.body.title).toBe('QMemory Library Demo');
expect(res.body.endpoints).toBeDefined();
});
test('GET /health returns service status', async () => {
const res = await agent.get('/health');
expect(res.status).toBe(200);
expect(res.body.message).toBe('Service is healthy');
expect(res.body.data.status).toBe('healthy');
});
test('GET /users initially returns empty list', async () => {
const res = await agent.get('/users');
expect(res.status).toBe(200);
expect(res.body.data).toEqual([]);
});
test('POST /users succeeds with valid data', async () => {
const res = await agent.post('/users').send({ username: 'alice', displayName: 'Alice' }); // displayName replaces email
expect(res.status).toBe(200);
expect(res.body.data.username).toBe('alice');
expect(res.body.data.id).toBeDefined();
});
test('POST /users sanitizes malicious input', async () => {
const res = await agent
.post('/users')
.send({ username: ' <script>mallory</script> ', displayName: '<b>Mallory</b>' }); // ensure name sanitized without email
expect(res.status).toBe(200);
expect(res.body.data.username).toBe('mallory');
const list = await agent.get('/users');
expect(list.body.data[0].username).toBe('mallory');
});
test('POST /users fails validation when username missing', async () => {
const res = await agent.post('/users').send({});
expect(res.status).toBe(400);
});
test('creating a user twice returns 400 with duplicate message', async () => {
await agent.post('/users').send({ username: 'alice' });
const res = await agent.post('/users').send({ username: 'alice' });
expect(res.status).toBe(400);
expect(res.body.message).toMatch(/already exists/);
});
test('GET /users/:id succeeds for existing user', async () => {
const createRes = await agent.post('/users').send({ username: 'bob' });
const id = createRes.body.data.id;
const res = await agent.get(`/users/${id}`);
expect(res.status).toBe(200);
expect(res.body.data.username).toBe('bob');
});
test('GET /users/:id returns 404 when not found', async () => {
const res = await agent.get('/users/9999');
expect(res.status).toBe(404);
});
test('GET /users/:id returns 400 when id is not numeric', async () => {
const res = await agent.get('/users/xyz');
expect(res.status).toBe(400);
});
test('DELETE /users/:id succeeds for existing user', async () => {
const createRes = await agent.post('/users').send({ username: 'carol' });
const id = createRes.body.data.id;
const res = await agent.delete(`/users/${id}`);
expect(res.status).toBe(200);
});
test('DELETE /users/:id returns 404 when missing', async () => {
const res = await agent.delete('/users/9999');
expect(res.status).toBe(404);
});
test('DELETE /users/:id returns 400 when id is not numeric', async () => {
const res = await agent.delete('/users/10abc');
expect(res.status).toBe(400);
});
test('POST /users/clear resets storage when not in production', async () => {
await agent.post('/users').send({ username: 'dave' });
const res = await agent.post('/users/clear');
expect(res.status).toBe(200);
const list = await agent.get('/users');
expect(list.body.data).toEqual([]);
});
test('/users/clear responds 400 in production', async () => {
await testHelpers.withSavedEnv(async () => {
process.env.NODE_ENV = 'production';
const res = await agent.post('/users/clear');
expect(res.status).toBe(400);
});
});
});