import { api } from '@/services/api'; import * as SecureStore from 'expo-secure-store'; // Mock dependencies jest.mock('expo-secure-store'); jest.mock('expo-file-system'); // Mock fetch globally global.fetch = jest.fn(); describe('API - Sensor Attachment Error Handling', () => { const mockBeneficiaryId = '123'; const mockWellId = 497; const mockDeviceMac = '142B2F81A14C'; const mockDeploymentId = 22; // Use 'robster' to match DEMO_LEGACY_USER in api.ts const mockUserName = 'robster'; // Create a mock JWT with expiration far in the future const createMockJWT = () => { const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); const payload = btoa(JSON.stringify({ sub: mockUserName, exp: Math.floor(Date.now() / 1000) + 3600, // expires in 1 hour })); const signature = 'mock-signature'; return `${header}.${payload}.${signature}`; }; const mockToken = createMockJWT(); beforeEach(() => { jest.clearAllMocks(); // Reset SecureStore mock to default implementation // legacyAccessToken needs to be a valid JWT format with valid expiration (SecureStore.getItemAsync as jest.Mock).mockImplementation((key: string) => { if (key === 'accessToken') return Promise.resolve('mock-jwt-token'); if (key === 'userId') return Promise.resolve('user-123'); if (key === 'userEmail') return Promise.resolve('test@example.com'); if (key === 'legacyAccessToken') return Promise.resolve(mockToken); if (key === 'legacyUserName') return Promise.resolve(mockUserName); if (key === 'legacyUserId') return Promise.resolve('legacy-user-123'); return Promise.resolve(null); }); }); describe('attachDeviceToBeneficiary', () => { it('should handle deployment not found error', async () => { // Mock getDeploymentForBeneficiary to return beneficiary without deployment (global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 123, firstName: 'Maria', lastName: 'Garcia', // No deploymentId }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('DEPLOYMENT_NOT_FOUND'); expect(result.error?.message).toContain('Could not find beneficiary deployment'); expect(result.error?.status).toBe(404); }); it('should handle missing authentication credentials', async () => { // Mock successful beneficiary lookup (global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 123, firstName: 'Maria', deploymentId: mockDeploymentId }) }); // Mock missing legacy credentials (SecureStore.getItemAsync as jest.Mock).mockImplementation((key: string) => { if (key === 'accessToken') return Promise.resolve('mock-jwt-token'); // Return null for legacy credentials return Promise.resolve(null); }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('UNAUTHORIZED'); expect(result.error?.message).toContain('Not authenticated'); expect(result.error?.status).toBe(401); }); it('should handle 401 Unauthorized 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 401 response from Legacy API .mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ message: 'Invalid credentials' }) }); 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|Not authenticated/); expect(result.error?.status).toBe(401); }); it('should handle 404 Not Found 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 404 response from Legacy API .mockResolvedValueOnce({ ok: false, status: 404, json: async () => ({ message: 'Device not found' }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('NOT_FOUND'); expect(result.error?.message).toContain('Sensor or deployment not found'); expect(result.error?.status).toBe(404); }); it('should handle 500 Server Error 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 500 response from Legacy API .mockResolvedValueOnce({ ok: false, status: 500, json: async () => ({ message: 'Internal server error' }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('SERVER_ERROR'); expect(result.error?.message).toContain('Server error'); expect(result.error?.status).toBe(500); }); it('should handle 503 Service Unavailable 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 503 response from Legacy API .mockResolvedValueOnce({ ok: false, status: 503, json: async () => ({ message: 'Service unavailable' }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('SERVICE_UNAVAILABLE'); expect(result.error?.message).toContain('Service unavailable'); expect(result.error?.status).toBe(503); }); it('should handle Legacy API error status in response body', async () => { // Mock successful beneficiary lookup (global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 123, firstName: 'Maria', deploymentId: mockDeploymentId }) }) // Mock successful HTTP but error status in body .mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ status: '400 Bad Request', message: 'Invalid device MAC address format' }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('LEGACY_API_ERROR'); expect(result.error?.message).toBe('Invalid device MAC address format'); }); it('should handle network errors', async () => { // Mock successful beneficiary lookup first (global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 123, firstName: 'Maria', deploymentId: mockDeploymentId }) }) // Then mock network error on Legacy API call .mockRejectedValueOnce( new Error('fetch failed') ); 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 handle unexpected exceptions', async () => { // Mock successful beneficiary lookup first (global.fetch as jest.Mock) .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 123, firstName: 'Maria', deploymentId: mockDeploymentId }) }) // Then mock unexpected error .mockRejectedValueOnce( new Error('Unexpected error occurred') ); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(false); expect(result.error?.code).toBe('EXCEPTION'); expect(result.error?.message).toContain('unexpected error'); }); it('should successfully attach device when all is well', async () => { // 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', message: 'Device attached successfully' }) }); const result = await api.attachDeviceToBeneficiary( mockBeneficiaryId, mockWellId, mockDeviceMac ); expect(result.ok).toBe(true); expect(result.data).toEqual({ success: true }); expect(result.error).toBeUndefined(); }); it('should send correct request parameters to Legacy API', async () => { // 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, mockDeviceMac ); // Verify the second fetch call (device_form) const secondCall = (global.fetch as jest.Mock).mock.calls[1]; expect(secondCall[0]).toBe('https://eluxnetworks.net/function/well-api/api'); expect(secondCall[1].method).toBe('POST'); expect(secondCall[1].headers['Content-Type']).toBe('application/x-www-form-urlencoded'); // Verify request body contains correct parameters const bodyString = secondCall[1].body; expect(bodyString).toContain('function=device_form'); expect(bodyString).toContain(`well_id=${mockWellId}`); expect(bodyString).toContain(`device_mac=${mockDeviceMac.toUpperCase()}`); expect(bodyString).toContain(`deployment_id=${mockDeploymentId}`); expect(bodyString).toContain(`user_name=${mockUserName}`); // 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}`); }); }); });