Story 2.4: Test Backend Chat Route Handlers
Sprint Key: 2-4-test-backend-chat-route-handlers
Epic: 2
PRD: testing-framework
Story 2.4: Test Backend Chat Route Handlers
Status: ready-for-dev
Story
As a developer, I want tests for the chat route group, so that conversation and folder endpoints are validated against the API contract.
Acceptance Criteria
-
AC1: GET /api/chat/conversations (200) — Test returns conversation list with pagination.
chatHistoryService.getUserConversations(userId, options)is mocked. Response shape:{ conversations: [...], pagination: { total, limit, offset, pages, currentPage } }. Verifiesreq.user.iss_subis passed as userId andreq.queryparams (limit, offset, includeArchived, filterStarred, searchTerm) are forwarded. -
AC2: GET /api/chat/conversations without userId (400) — When
req.user.iss_subis missing, returns 400 with{ success: false, message: 'User ID is required but not found in request' }. -
AC3: POST /api/chat/conversations (201) — Test creates a new conversation.
chatHistoryService.createConversation(data)is mocked. Request body:{ title, categoryId?, initialMessage?, tags? }. VerifieschatHistoryService.createConversationis called withuserId,userKey, title, timestamps. IfinitialMessageis provided, also verifieschatHistoryService.addMessageis called. -
AC4: GET /api/chat/conversations/:conversationId (200) — Returns conversation details with messages.
chatHistoryService.getConversation(id)is mocked. -
AC5: GET /api/chat/conversations/:conversationId (404) — When
chatHistoryService.getConversation(id)returns null, returns 404 with{ message: 'Conversation not found' }. -
AC6: GET /api/chat/conversations/:conversationId/messages (200) — Returns messages with pagination.
chatHistoryService.getConversationMessages(id, options)is mocked. Query params:limit,offset,newestFirst. -
AC7: POST /api/chat/conversations/:conversationId/messages (201) — Adds a message. Request body:
{ content, sender, queryId?, metadata? }. Validates sender must be "user" or "assistant". Missing content returns 400. Invalid sender returns 400. -
AC8: POST /api/chat/conversations/:conversationId/messages without body (400) — Returns 400 with
{ message: 'Request body is required' }. -
AC9: 401 on all routes — All chat routes use
keycloakAuthMiddleware.authenticateviarouter.use(). Requests without valid token return 401. -
AC10: Supertest + createApp pattern — All tests use the same mock setup as
auth.test.js:require('../setup-env'), shared-lib mock, keycloak service mocks, swagger mocks, service mocks. App created viacreateApp(). -
AC11: AAA structure — All tests follow Arrange-Act-Assert with "should" naming convention.
-
AC12: Existing tests pass — All existing tests (200+) continue to pass unchanged.
Tasks / Subtasks
-
Task 1: Set up test file with mocks (AC: #10 (closed), #11 (closed)) -
1.1 Create __tests__/routes/chat.test.jswith mock setup identical toauth.test.jspattern -
1.2 Mock chat-history-servicewith all methods used by routes:getUserConversations,createConversation,getConversation,updateConversation,deleteConversation,getConversationMessages,addMessage,markMessagesAsRead,findMessagesForQuery,findOriginatingQuery,linkQueryToConversation,createConversationFromQuery,searchConversations,getRecentConversations,getUserConversationStats,getUserFolders,createFolder,getFolder,updateFolder,deleteFolder,addConversationToFolder,removeConversationFromFolder,searchFolders,reorderFolders,getFolderPath,moveConversation,findConversationFolder -
1.3 Mock query-service(used for route registration alongside chat-history-service)
-
-
Task 2: Write conversation CRUD tests (AC: #1 (closed), #2 (closed), #3 (closed), #4 (closed), #5 (closed)) -
2.1 GET /conversationswith valid token → 200, returns conversation list with pagination -
2.2 GET /conversationswith missing userId → 400 -
2.3 POST /conversationswith valid data → 201, verify createConversation called -
2.4 POST /conversationswith initialMessage → verify addMessage also called -
2.5 GET /conversations/:id→ 200, returns conversation with messages -
2.6 GET /conversations/:idnot found → 404
-
-
Task 3: Write message tests (AC: #6 (closed), #7 (closed), #8 (closed)) -
3.1 GET /conversations/:id/messages→ 200, returns messages with pagination -
3.2 POST /conversations/:id/messagesvalid → 201 -
3.3 POST /conversations/:id/messageswithout body → 400 -
3.4 POST /conversations/:id/messageswithout content → 400 -
3.5 POST /conversations/:id/messageswith invalid sender → 400
-
-
Task 4: Write auth guard tests (AC: #9 (closed)) -
4.1 GET /conversationswithout token → 401 -
4.2 POST /conversationswithout token → 401 -
4.3 GET /conversations/:id/messageswithout token → 401
-
-
Task 5: Verify existing tests pass (AC: #12 (closed)) -
5.1 Run cd components/gov-chat-backend && npm test— all tests pass -
5.2 Run cd components/gov-chat-backend && npm run lint— no lint errors
-
Dev Notes
Critical Discovery: Chat Routes Are FAR More Than Conversations + Messages
The epics file (Story 2.4 definition) lists only 5 endpoints (conversations CRUD + messages). The actual chat-history-routes.js (1,697 lines) has 22 routes organized into 4 groups:
Group 1 — Conversations (6 routes):
GET /conversations — list with pagination/filters
GET /conversations/:conversationId — details with messages
POST /conversations — create new
PATCH /conversations/:conversationId — update (title, starred, archived, tags)
DELETE /conversations/:conversationId — delete with permission check
GET /conversations/:conversationId/folder — find which folder contains it
POST /conversations/:conversationId/move — move between folders
Group 2 — Messages (4 routes):
GET /conversations/:conversationId/messages — list with pagination
POST /conversations/:conversationId/messages — add message
POST /conversations/:conversationId/messages/read — mark as read
GET /messages/:messageId/query — find originating query
Group 3 — Query linking (2 routes):
GET /query/:queryId/messages — find messages for a query
POST /query/:queryId/conversation — create conversation from query
Group 4 — Utility (3 routes):
GET /search — search conversations
GET /recent — recent conversations
GET /stats — conversation statistics
Group 5 — Folders (8 routes):
GET /folders — list user folders
POST /folders — create folder
GET /folders/:folderId — folder details
PATCH /folders/:folderId — update folder
DELETE /folders/:folderId — delete folder
GET /folders/search — search folders
POST /folders/reorder — reorder folders
GET /folders/:folderId/path — folder breadcrumbs
POST /folders/:folderId/conversations/:conversationId — add to folder
DELETE /folders/:folderId/conversations/:conversationId — remove from folder
SCOPE DECISION: This story tests the core conversation and message routes (Groups 1-3 plus search/recent/stats). The folder routes (Group 5) are a substantial separate concern — they are tested in a separate describe block within the same file but with fewer, more targeted tests since they follow the same patterns.
Route Registration Pattern
In index.js:
{ file: 'chat-history-routes', paths: ['/api/chat-history', '/api/chat'], serviceName: 'chatHistoryService', keycloakAuth: true }
Key details:
- Routes are mounted at BOTH
/api/chat-historyand/api/chat -
keycloakAuth: truemeanskeycloakAuthMiddleware.authenticateis applied to ALL routes viarouter.use()at line 17 - Factory pattern:
module.exports = (chatHistoryService) => { ... return router; }
Auth Middleware on Chat Routes
Unlike auth-routes.js where middleware is per-route, chat routes apply auth middleware globally via router.use(keycloakAuthMiddleware.authenticate) at line 17. This means ALL chat endpoints require authentication — no public endpoints exist.
extractUserId Helper
Every route handler uses an inline helper:
const extractUserId = (req) => {
if (!req.user?.iss_sub) {
logger.warn('No user context — JWT middleware should have populated req.user.iss_sub');
return null;
}
return req.user.iss_sub;
};
When extractUserId returns null, handlers return 400 (not 401) with { success: false, message: 'User ID is required but not found in request' }. This is a defense-in-depth check — the middleware should have already set req.user.
chatHistoryService Methods to Mock
The service is a singleton (ChatHistoryService.getInstance()) exported at module level. Mock ALL methods used by the routes:
jest.mock('../../services/chat-history-service', () => ({
getUserConversations: jest.fn(),
createConversation: jest.fn(),
getConversation: jest.fn(),
updateConversation: jest.fn(),
deleteConversation: jest.fn(),
getConversationMessages: jest.fn(),
addMessage: jest.fn(),
markMessagesAsRead: jest.fn(),
findMessagesForQuery: jest.fn(),
findOriginatingQuery: jest.fn(),
linkQueryToConversation: jest.fn(),
createConversationFromQuery: jest.fn(),
searchConversations: jest.fn(),
getRecentConversations: jest.fn(),
getUserConversationStats: jest.fn(),
getUserFolders: jest.fn(),
createFolder: jest.fn(),
getFolder: jest.fn(),
updateFolder: jest.fn(),
deleteFolder: jest.fn(),
addConversationToFolder: jest.fn(),
removeConversationFromFolder: jest.fn(),
searchFolders: jest.fn(),
reorderFolders: jest.fn(),
getFolderPath: jest.fn(),
moveConversation: jest.fn(),
findConversationFolder: jest.fn(),
db: { collection: jest.fn() }
}));
Note: The db mock is needed because POST /conversations/:id/messages calls chatHistoryService.db.collection('queries').document(queryId) directly (line 529) for query linking.
Conversation Response Shapes (from Story 2.2 fixture spec)
// getUserConversations returns:
{
conversations: [
{
_id, _key, title, lastMessage, created, updated,
messageCount, isStarred, isArchived, category, tags,
userRole, lastViewedAt, lastMessagePreview
}
],
pagination: { total, limit, offset, pages, currentPage }
}
// getConversation returns:
{
_id, _key, title, lastMessage, created, updated,
messageCount, isStarred, isArchived, category, tags,
messages: [...],
categories: [...],
owners: [...],
files: [...]
}
// createConversation returns:
{ _id, _key, title, lastMessage, created, updated, messageCount, ... }
Error Handling Pattern
Chat route handlers use try/catch with next(error) — errors are passed to the global Express error handler. They do NOT return structured { error, message, details } format like auth middleware does. Instead, validation errors return { success: false, message: '...' } or { message: '...' }.
POST /messages Query Linking Edge Case
Line 527-537: When sender is 'assistant' AND queryId is provided, the handler tries to:
- Verify the query exists via
chatHistoryService.db.collection('queries').document(queryId) - Link the query to the conversation via
chatHistoryService.linkQueryToConversation(...)
If the queryId is invalid, it catches the error and logs a warning — does NOT fail the request. This is tested with a separate test case.
Test File Location
components/gov-chat-backend/__tests__/routes/chat.test.js
Files to Create
| File | Action |
|---|---|
__tests__/routes/chat.test.js |
NEW |
Files NOT Modified
All existing test files remain unchanged. The new test file is purely additive.
Dependencies to Mock
| Module | Mock Strategy |
|---|---|
../shared-lib |
Use centralized mock: jest.mock('../shared-lib', () => require('./__tests__/mocks/shared-lib'), { virtual: true })
|
../services/keycloak-auth-service |
jest.mock() with verifyToken, checkUserStatusInKeycloak
|
../services/user-provisioning-service |
jest.mock() with provisionUser, initialize, markUserAsDeleted
|
../services/session-service |
jest.mock() with session methods (not directly used but loaded by index.js) |
../services/chat-history-service |
jest.mock() with all methods listed above |
../services/query-service |
jest.mock(() => ({})) — loaded for route registration but not called by chat routes |
| All other services |
jest.mock(() => ({})) — same pattern as auth.test.js |
swagger-jsdoc |
jest.mock() with { virtual: true }
|
swagger-ui-express |
jest.mock() with { virtual: true }
|
Anti-Patterns to Avoid
- Do NOT call
app.listen()—createApp()returns the app without listening - Do NOT modify existing test files
- Do NOT use ES
import/export— CommonJS only (require/module.exports) - Do NOT create real ArangoDB connections — all DB access must be mocked
- Do NOT duplicate
process.envoverrides — use centralized__tests__/setup-env.jsviarequire('../setup-env') - Do NOT test the service layer itself — that's Story 2.7. This story tests the route handler's behavior
- Do NOT write tests for every single folder route — focus on representative patterns (CRUD), not exhaustive coverage of all 22 routes
- Do NOT test
PATCH /conversations/:idorDELETE /conversations/:id— they follow identical patterns to GET/POST and would bloat the file without new patterns - Do NOT forget to mock
chatHistoryService.db.collection()— the POST messages route calls it directly
Project Structure Notes
- Backend files live at
components/gov-chat-backend/root — nosrc/subdirectory - Route tests go in
__tests__/routes/(established by Story 2.3) - CommonJS (
require/module.exports) everywhere in backend - Jest config in
package.json:testMatch: ["**/__tests__/**/*.test.js"]
Downstream Impact
This story validates the createApp() + Supertest pattern for the most complex route module (22 routes). Stories 2.5 (analytics/categories) and 2.6 (admin/files) will follow the same pattern but with simpler route files.
References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.4] — Original story definition (conversations + messages only — corrected above)
- [Source: _bmad-output/implementation-artifacts/2-3-test-backend-auth-route-handlers.md] — Previous story: mock setup pattern, auth.test.js structure
- [Source: components/gov-chat-backend/routes/chat-history-routes.js] — 22 routes: conversations, messages, queries, folders
- [Source: components/gov-chat-backend/services/chat-history-service.js] — Singleton service with ArangoDB queries
- [Source: components/gov-chat-backend/tests/routes/auth.test.js] — Exact mock pattern to replicate
- [Source: components/gov-chat-backend/tests/setup-env.js] — Centralized env setup
- [Source: components/gov-chat-backend/tests/mocks/shared-lib.js] — Shared mock with all 4 exports
- [Source: components/gov-chat-backend/tests/fixtures/tokens.js] — JWT fixture helpers
- [Source: components/gov-chat-backend/tests/fixtures/users.js] — User fixture factory
Dev Agent Record
Agent Model Used
{{agent_model_name_version}}