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:
Sergei 2026-01-29 11:06:35 -08:00
parent 2b2bd88726
commit 0dd06be8f2
3 changed files with 324 additions and 4 deletions

View File

@ -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"
]
}
}

View 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');
});
});
});

View File

@ -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