Handle missing deploymentId with proper error response
- Change GET /api/me/deployments/:id/devices to return 400 error when legacy_deployment_id is missing - Add error response with code 'MISSING_DEPLOYMENT_ID' and descriptive message - Add comprehensive Jest tests for missing deployment scenarios - Install Jest and Supertest for backend testing - Add test scripts to package.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2b2bd88726
commit
0dd06be8f2
@ -5,7 +5,10 @@
|
|||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "nodemon src/index.js"
|
"dev": "nodemon src/index.js",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.966.0",
|
"@aws-sdk/client-s3": "^3.966.0",
|
||||||
@ -27,6 +30,19 @@
|
|||||||
"stripe": "^20.1.0"
|
"stripe": "^20.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.2"
|
"@types/jest": "^30.0.0",
|
||||||
|
"jest": "^30.2.0",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"coveragePathIgnorePatterns": [
|
||||||
|
"/node_modules/"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"**/__tests__/**/*.js",
|
||||||
|
"**/?(*.)+(spec|test).js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
300
backend/src/routes/__tests__/deployments.test.js
Normal file
300
backend/src/routes/__tests__/deployments.test.js
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
/**
|
||||||
|
* Tests for deployments routes
|
||||||
|
* Focus: Missing deploymentId error handling
|
||||||
|
*/
|
||||||
|
|
||||||
|
const request = require('supertest');
|
||||||
|
const express = require('express');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const deploymentsRouter = require('../deployments');
|
||||||
|
const { supabase } = require('../../config/supabase');
|
||||||
|
|
||||||
|
// Mock Supabase
|
||||||
|
jest.mock('../../config/supabase', () => ({
|
||||||
|
supabase: {
|
||||||
|
from: jest.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock environment
|
||||||
|
process.env.JWT_SECRET = 'test-secret-key';
|
||||||
|
|
||||||
|
describe('Deployments Routes - Missing Deployment ID', () => {
|
||||||
|
let app;
|
||||||
|
let testToken;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
// Create test app
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use('/api/me', deploymentsRouter);
|
||||||
|
|
||||||
|
// Create test JWT token
|
||||||
|
testToken = jwt.sign(
|
||||||
|
{ userId: 1, email: 'test@example.com' },
|
||||||
|
process.env.JWT_SECRET
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/me/deployments/:id/devices', () => {
|
||||||
|
it('should return 400 error when legacy_deployment_id is null', async () => {
|
||||||
|
// Mock user access check - user has access
|
||||||
|
const mockAccessSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq1 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq2 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: { role: 'custodian' },
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock deployment fetch - deployment exists but no legacy_deployment_id
|
||||||
|
const mockDeploymentSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentEq = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
beneficiary_id: 42,
|
||||||
|
legacy_deployment_id: null // This is the key condition
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
supabase.from.mockImplementation((table) => {
|
||||||
|
if (table === 'user_access') {
|
||||||
|
return {
|
||||||
|
select: mockAccessSelect,
|
||||||
|
eq: mockAccessEq1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === 'beneficiary_deployments') {
|
||||||
|
return {
|
||||||
|
select: mockDeploymentSelect,
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockAccessSelect.mockReturnValue({
|
||||||
|
eq: mockAccessEq1
|
||||||
|
});
|
||||||
|
mockAccessEq1.mockReturnValue({
|
||||||
|
eq: mockAccessEq2
|
||||||
|
});
|
||||||
|
mockAccessEq2.mockReturnValue({
|
||||||
|
single: mockAccessSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDeploymentSelect.mockReturnValue({
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
});
|
||||||
|
mockDeploymentEq.mockReturnValue({
|
||||||
|
single: mockDeploymentSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/me/deployments/1/devices')
|
||||||
|
.set('Authorization', `Bearer ${testToken}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body).toHaveProperty('error', 'No deployment configured for user');
|
||||||
|
expect(response.body).toHaveProperty('code', 'MISSING_DEPLOYMENT_ID');
|
||||||
|
expect(response.body).toHaveProperty('message');
|
||||||
|
expect(response.body.message).toContain('does not have a deployment configured');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 error when legacy_deployment_id is undefined', async () => {
|
||||||
|
// Mock similar setup but with undefined legacy_deployment_id
|
||||||
|
const mockAccessSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq1 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq2 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: { role: 'custodian' },
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockDeploymentSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentEq = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
beneficiary_id: 42
|
||||||
|
// legacy_deployment_id is undefined
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
supabase.from.mockImplementation((table) => {
|
||||||
|
if (table === 'user_access') {
|
||||||
|
return {
|
||||||
|
select: mockAccessSelect,
|
||||||
|
eq: mockAccessEq1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === 'beneficiary_deployments') {
|
||||||
|
return {
|
||||||
|
select: mockDeploymentSelect,
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockAccessSelect.mockReturnValue({
|
||||||
|
eq: mockAccessEq1
|
||||||
|
});
|
||||||
|
mockAccessEq1.mockReturnValue({
|
||||||
|
eq: mockAccessEq2
|
||||||
|
});
|
||||||
|
mockAccessEq2.mockReturnValue({
|
||||||
|
single: mockAccessSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDeploymentSelect.mockReturnValue({
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
});
|
||||||
|
mockDeploymentEq.mockReturnValue({
|
||||||
|
single: mockDeploymentSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/me/deployments/1/devices')
|
||||||
|
.set('Authorization', `Bearer ${testToken}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(response.body).toHaveProperty('code', 'MISSING_DEPLOYMENT_ID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proceed normally when legacy_deployment_id exists', async () => {
|
||||||
|
// Mock user access check
|
||||||
|
const mockAccessSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq1 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq2 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: { role: 'custodian' },
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock deployment with valid legacy_deployment_id
|
||||||
|
const mockDeploymentSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentEq = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
beneficiary_id: 42,
|
||||||
|
legacy_deployment_id: 123 // Valid deployment ID
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
supabase.from.mockImplementation((table) => {
|
||||||
|
if (table === 'user_access') {
|
||||||
|
return {
|
||||||
|
select: mockAccessSelect,
|
||||||
|
eq: mockAccessEq1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === 'beneficiary_deployments') {
|
||||||
|
return {
|
||||||
|
select: mockDeploymentSelect,
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockAccessSelect.mockReturnValue({
|
||||||
|
eq: mockAccessEq1
|
||||||
|
});
|
||||||
|
mockAccessEq1.mockReturnValue({
|
||||||
|
eq: mockAccessEq2
|
||||||
|
});
|
||||||
|
mockAccessEq2.mockReturnValue({
|
||||||
|
single: mockAccessSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDeploymentSelect.mockReturnValue({
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
});
|
||||||
|
mockDeploymentEq.mockReturnValue({
|
||||||
|
single: mockDeploymentSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/me/deployments/1/devices')
|
||||||
|
.set('Authorization', `Bearer ${testToken}`);
|
||||||
|
|
||||||
|
// Should NOT return error - should proceed to device fetch
|
||||||
|
// (currently returns empty array as TODO is not implemented)
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toHaveProperty('devices');
|
||||||
|
expect(Array.isArray(response.body.devices)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 when no auth token provided', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/me/deployments/1/devices');
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
expect(response.body).toHaveProperty('error', 'No token provided');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 403 when user has no access to beneficiary', async () => {
|
||||||
|
// Mock user access check - no access
|
||||||
|
const mockAccessSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq1 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessEq2 = jest.fn().mockReturnThis();
|
||||||
|
const mockAccessSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: null,
|
||||||
|
error: { message: 'No access' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockDeploymentSelect = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentEq = jest.fn().mockReturnThis();
|
||||||
|
const mockDeploymentSingle = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
beneficiary_id: 42,
|
||||||
|
legacy_deployment_id: 123
|
||||||
|
},
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
supabase.from.mockImplementation((table) => {
|
||||||
|
if (table === 'user_access') {
|
||||||
|
return {
|
||||||
|
select: mockAccessSelect,
|
||||||
|
eq: mockAccessEq1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (table === 'beneficiary_deployments') {
|
||||||
|
return {
|
||||||
|
select: mockDeploymentSelect,
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mockAccessSelect.mockReturnValue({
|
||||||
|
eq: mockAccessEq1
|
||||||
|
});
|
||||||
|
mockAccessEq1.mockReturnValue({
|
||||||
|
eq: mockAccessEq2
|
||||||
|
});
|
||||||
|
mockAccessEq2.mockReturnValue({
|
||||||
|
single: mockAccessSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDeploymentSelect.mockReturnValue({
|
||||||
|
eq: mockDeploymentEq
|
||||||
|
});
|
||||||
|
mockDeploymentEq.mockReturnValue({
|
||||||
|
single: mockDeploymentSingle
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/me/deployments/1/devices')
|
||||||
|
.set('Authorization', `Bearer ${testToken}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body).toHaveProperty('error', 'Access denied');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -291,9 +291,13 @@ router.get('/deployments/:id/devices', async (req, res) => {
|
|||||||
return res.status(403).json({ error: 'Access denied' });
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no legacy_deployment_id, return empty list
|
// If no legacy_deployment_id, throw error
|
||||||
if (!deployment.legacy_deployment_id) {
|
if (!deployment.legacy_deployment_id) {
|
||||||
return res.json({ devices: [] });
|
return res.status(400).json({
|
||||||
|
error: 'No deployment configured for user',
|
||||||
|
code: 'MISSING_DEPLOYMENT_ID',
|
||||||
|
message: 'This beneficiary does not have a deployment configured yet. Please set up the deployment first.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get devices from Legacy API using legacy_deployment_id
|
// TODO: Get devices from Legacy API using legacy_deployment_id
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user