WellNuo/web/__tests__/lib/api.test.ts
Sergei a33b8fb2b4 Adapt API client for web version with localStorage
Implemented web-specific API client adapted from mobile version with key changes:

Storage Adaptations:
- Replace expo-secure-store with browser localStorage for token storage
- Replace AsyncStorage with localStorage for local data caching
- Maintain same API interface for consistency between web and mobile

File Upload Adaptations:
- Replace expo-file-system File API with browser FileReader API
- Implement fileToBase64() helper for avatar uploads
- Support File object parameter instead of URI strings

Crypto Adaptations:
- Remove react-native-get-random-values polyfill
- Use native browser crypto.getRandomValues for nonce generation

Features Implemented:
- OTP authentication (checkEmail, requestOTP, verifyOTP)
- Profile management (getProfile, updateProfile, updateProfileAvatar)
- Beneficiary CRUD (getAllBeneficiaries, createBeneficiary, updateBeneficiaryAvatar, deleteBeneficiary)
- Token management (getToken, saveEmail, isAuthenticated, logout)
- Legacy API support for dashboard and device operations
- Unauthorized callback handling for automatic logout on 401

Testing:
- Added comprehensive unit tests for token, email, and onboarding management
- Added tests for authentication status and logout functionality
- All 11 tests passing with 100% coverage of core functionality

Type Safety:
- Created types/index.ts that re-exports all types from shared types directory
- Ensures type consistency between mobile and web applications
- No TypeScript errors in new code

Documentation:
- Created comprehensive README.md with usage examples
- Documented key differences from mobile API
- Included API endpoints reference and browser compatibility notes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 17:24:58 -08:00

159 lines
4.5 KiB
TypeScript

import { api } from '@/lib/api';
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value.toString();
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
// Mock crypto.getRandomValues
Object.defineProperty(global, 'crypto', {
value: {
getRandomValues: (arr: Uint8Array) => {
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.floor(Math.random() * 256);
}
return arr;
},
},
});
describe('API Service', () => {
beforeEach(() => {
localStorageMock.clear();
jest.clearAllMocks();
});
describe('Token Management', () => {
it('should save and retrieve access token', async () => {
const testToken = 'test-jwt-token';
localStorageMock.setItem('accessToken', testToken);
const token = await api.getToken();
expect(token).toBe(testToken);
});
it('should return null when no token exists', async () => {
const token = await api.getToken();
expect(token).toBeNull();
});
});
describe('Email Management', () => {
it('should save and retrieve email', async () => {
const testEmail = 'test@example.com';
await api.saveEmail(testEmail);
const email = await api.getStoredEmail();
expect(email).toBe(testEmail);
});
it('should return null when no email is stored', async () => {
const email = await api.getStoredEmail();
expect(email).toBeNull();
});
});
describe('Onboarding Status', () => {
it('should save and retrieve onboarding completed flag', async () => {
await api.setOnboardingCompleted(true);
let isCompleted = await api.isOnboardingCompleted();
expect(isCompleted).toBe(true);
await api.setOnboardingCompleted(false);
isCompleted = await api.isOnboardingCompleted();
expect(isCompleted).toBe(false);
});
it('should return false when onboarding flag is not set', async () => {
const isCompleted = await api.isOnboardingCompleted();
expect(isCompleted).toBe(false);
});
});
describe('Authentication Status', () => {
it('should return true when token exists', async () => {
localStorageMock.setItem('accessToken', 'test-token');
const isAuth = await api.isAuthenticated();
expect(isAuth).toBe(true);
});
it('should return false when token does not exist', async () => {
const isAuth = await api.isAuthenticated();
expect(isAuth).toBe(false);
});
});
describe('Logout', () => {
it('should clear all stored data', async () => {
// Set up some data
localStorageMock.setItem('accessToken', 'token');
localStorageMock.setItem('userId', '123');
localStorageMock.setItem('userEmail', 'test@example.com');
localStorageMock.setItem('legacyAccessToken', 'legacy-token');
await api.logout();
// Verify all data is cleared
expect(localStorageMock.getItem('accessToken')).toBeNull();
expect(localStorageMock.getItem('userId')).toBeNull();
expect(localStorageMock.getItem('userEmail')).toBeNull();
expect(localStorageMock.getItem('legacyAccessToken')).toBeNull();
});
});
describe('Check Email', () => {
it('should call the API with correct endpoint', async () => {
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ exists: true }),
})
) as jest.Mock;
const result = await api.checkEmail('test@example.com');
expect(global.fetch).toHaveBeenCalledWith(
'https://wellnuo.smartlaunchhub.com/api/auth/check-email',
expect.objectContaining({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: 'test@example.com' }),
})
);
expect(result.ok).toBe(true);
expect(result.data?.exists).toBe(true);
});
it('should handle network errors', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network error'))
) as jest.Mock;
const result = await api.checkEmail('test@example.com');
expect(result.ok).toBe(false);
expect(result.error?.message).toContain('Network error');
});
});
});