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",
|
||||
"scripts": {
|
||||
"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": {
|
||||
"@aws-sdk/client-s3": "^3.966.0",
|
||||
@ -27,6 +30,19 @@
|
||||
"stripe": "^20.1.0"
|
||||
},
|
||||
"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' });
|
||||
}
|
||||
|
||||
// If no legacy_deployment_id, return empty list
|
||||
// If no legacy_deployment_id, throw error
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user