diff --git a/__tests__/services/api-sensor-attachment-errors.test.ts b/__tests__/services/api-sensor-attachment-errors.test.ts index 79d7e5c..4a6fe73 100644 --- a/__tests__/services/api-sensor-attachment-errors.test.ts +++ b/__tests__/services/api-sensor-attachment-errors.test.ts @@ -382,5 +382,175 @@ describe('API - Sensor Attachment Error Handling', () => { // Token will be URL encoded in the body, so just check it contains the token expect(bodyString).toContain('token='); }); + + it('should handle 403 Forbidden from Legacy API', async () => { + // Mock successful beneficiary lookup + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 123, + firstName: 'Maria', + deploymentId: mockDeploymentId + }) + }) + // Mock 403 response from Legacy API + .mockResolvedValueOnce({ + ok: false, + status: 403, + json: async () => ({ message: 'Access denied' }) + }); + + const result = await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + mockDeviceMac + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe('UNAUTHORIZED'); + expect(result.error?.message).toMatch(/Authentication expired/); + expect(result.error?.status).toBe(403); + }); + + it('should handle 429 Rate Limit from Legacy API', async () => { + // Mock successful beneficiary lookup + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 123, + firstName: 'Maria', + deploymentId: mockDeploymentId + }) + }) + // Mock 429 response from Legacy API + .mockResolvedValueOnce({ + ok: false, + status: 429, + json: async () => ({ message: 'Too many requests' }) + }); + + const result = await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + mockDeviceMac + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe('RATE_LIMITED'); + expect(result.error?.message).toContain('Too many requests'); + expect(result.error?.status).toBe(429); + }); + + it('should handle JSON parse errors gracefully', async () => { + // Mock successful beneficiary lookup + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 123, + firstName: 'Maria', + deploymentId: mockDeploymentId + }) + }) + // Mock response with invalid JSON + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => { throw new SyntaxError('Unexpected token'); } + }); + + const result = await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + mockDeviceMac + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe('EXCEPTION'); + }); + + it('should handle WellNuo API failure when getting beneficiary', async () => { + // Mock failed beneficiary lookup from WellNuo API + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: false, + status: 500, + json: async () => ({ message: 'Internal error' }) + }); + + const result = await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + mockDeviceMac + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe('DEPLOYMENT_NOT_FOUND'); + expect(result.error?.status).toBe(404); + }); + + it('should handle timeout errors', async () => { + // Mock successful beneficiary lookup + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 123, + firstName: 'Maria', + deploymentId: mockDeploymentId + }) + }) + // Mock timeout error + .mockRejectedValueOnce( + new Error('network request failed: timeout') + ); + + const result = await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + mockDeviceMac + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe('NETWORK_ERROR'); + expect(result.error?.message).toContain('No internet connection'); + }); + + it('should uppercase device MAC address in request', async () => { + const lowercaseMac = '142b2f81a14c'; + + // Mock successful beneficiary lookup + (global.fetch as jest.Mock) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: 123, + firstName: 'Maria', + deploymentId: mockDeploymentId + }) + }) + // Mock successful device_form response + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + status: '200 OK' + }) + }); + + await api.attachDeviceToBeneficiary( + mockBeneficiaryId, + mockWellId, + lowercaseMac + ); + + // Verify MAC is uppercased + const secondCall = (global.fetch as jest.Mock).mock.calls[1]; + const bodyString = secondCall[1].body; + expect(bodyString).toContain(`device_mac=${lowercaseMac.toUpperCase()}`); + expect(bodyString).not.toContain(`device_mac=${lowercaseMac}`); + }); }); }); diff --git a/services/api.ts b/services/api.ts index f09131f..61153bd 100644 --- a/services/api.ts +++ b/services/api.ts @@ -2048,6 +2048,9 @@ class ApiService { } else if (attachResponse.status === 404) { errorMessage = 'Sensor or deployment not found.'; errorCode = 'NOT_FOUND'; + } else if (attachResponse.status === 429) { + errorMessage = 'Too many requests. Please wait a moment and try again.'; + errorCode = 'RATE_LIMITED'; } else if (attachResponse.status === 500) { errorMessage = 'Server error. Please try again later.'; errorCode = 'SERVER_ERROR';