mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
342 lines
8.9 KiB
TypeScript
342 lines
8.9 KiB
TypeScript
/**
|
|
* Example React Native Service Test
|
|
*
|
|
* This demonstrates best practices for testing services:
|
|
* - Mock fetch/API calls
|
|
* - Test async operations
|
|
* - Test error handling
|
|
* - Test storage operations
|
|
* - Use MSW for API mocking (optional)
|
|
*/
|
|
|
|
import { authService } from '../authService';
|
|
import { tokenManager } from '../tokenManager';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
|
|
// Mock dependencies
|
|
jest.mock('expo-secure-store');
|
|
jest.mock('../tokenManager');
|
|
|
|
// Mock data
|
|
const mockTokens = {
|
|
appToken: 'mock-app-token-12345',
|
|
refreshToken: 'mock-refresh-token-12345',
|
|
manaToken: 'mock-mana-token-12345',
|
|
};
|
|
|
|
const mockUser = {
|
|
id: 'user-123',
|
|
email: 'test@example.com',
|
|
};
|
|
|
|
describe('authService', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
global.fetch = jest.fn();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('signIn', () => {
|
|
it('should sign in successfully with valid credentials', async () => {
|
|
// Arrange
|
|
const mockResponse = {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
success: true,
|
|
...mockTokens,
|
|
user: mockUser,
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
// Act
|
|
const result = await authService.signIn('test@example.com', 'password123');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(result.user).toEqual(mockUser);
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/auth/signin'),
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: expect.stringContaining('test@example.com'),
|
|
})
|
|
);
|
|
|
|
// Verify tokens were stored
|
|
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/appToken', mockTokens.appToken);
|
|
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/refreshToken', mockTokens.refreshToken);
|
|
});
|
|
|
|
it('should handle invalid credentials error', async () => {
|
|
// Arrange
|
|
const mockResponse = {
|
|
ok: false,
|
|
status: 401,
|
|
json: async () => ({
|
|
success: false,
|
|
error: 'INVALID_CREDENTIALS',
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
|
|
// Act
|
|
const result = await authService.signIn('test@example.com', 'wrongpassword');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('INVALID_CREDENTIALS');
|
|
expect(SecureStore.setItemAsync).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle network errors', async () => {
|
|
// Arrange
|
|
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network request failed'));
|
|
|
|
// Act
|
|
const result = await authService.signIn('test@example.com', 'password123');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Network');
|
|
});
|
|
|
|
it('should handle storage errors', async () => {
|
|
// Arrange
|
|
const mockResponse = {
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
...mockTokens,
|
|
user: mockUser,
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
(SecureStore.setItemAsync as jest.Mock).mockRejectedValue(new Error('Storage unavailable'));
|
|
|
|
// Act
|
|
const result = await authService.signIn('test@example.com', 'password123');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Storage');
|
|
});
|
|
|
|
it('should validate email format', async () => {
|
|
// Act
|
|
const result = await authService.signIn('invalid-email', 'password123');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('email');
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should validate password is not empty', async () => {
|
|
// Act
|
|
const result = await authService.signIn('test@example.com', '');
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('password');
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle timeout errors', async () => {
|
|
jest.useFakeTimers();
|
|
|
|
// Arrange
|
|
(global.fetch as jest.Mock).mockImplementation(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
setTimeout(() => resolve({ ok: true, json: async () => ({}) }), 60000);
|
|
})
|
|
);
|
|
|
|
// Act
|
|
const resultPromise = authService.signIn('test@example.com', 'password123');
|
|
|
|
jest.advanceTimersByTime(30000); // Advance 30s (timeout threshold)
|
|
const result = await resultPromise;
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('timeout');
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe('signOut', () => {
|
|
it('should sign out successfully', async () => {
|
|
// Arrange
|
|
(SecureStore.deleteItemAsync as jest.Mock).mockResolvedValue(undefined);
|
|
(tokenManager.clearTokens as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
// Act
|
|
const result = await authService.signOut();
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('@auth/appToken');
|
|
expect(SecureStore.deleteItemAsync).toHaveBeenCalledWith('@auth/refreshToken');
|
|
expect(tokenManager.clearTokens).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle storage errors during sign out', async () => {
|
|
// Arrange
|
|
(SecureStore.deleteItemAsync as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
|
|
|
// Act
|
|
const result = await authService.signOut();
|
|
|
|
// Assert
|
|
// Should succeed even if storage fails (user intent matters)
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('refreshToken', () => {
|
|
it('should refresh token successfully', async () => {
|
|
// Arrange
|
|
const oldRefreshToken = 'old-refresh-token';
|
|
const newTokens = {
|
|
appToken: 'new-app-token',
|
|
refreshToken: 'new-refresh-token',
|
|
};
|
|
|
|
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue(oldRefreshToken);
|
|
|
|
const mockResponse = {
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
...newTokens,
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
// Act
|
|
const result = await authService.refreshToken();
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(result.appToken).toBe(newTokens.appToken);
|
|
expect(SecureStore.setItemAsync).toHaveBeenCalledWith('@auth/appToken', newTokens.appToken);
|
|
});
|
|
|
|
it('should handle missing refresh token', async () => {
|
|
// Arrange
|
|
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue(null);
|
|
|
|
// Act
|
|
const result = await authService.refreshToken();
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('No refresh token');
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle expired refresh token', async () => {
|
|
// Arrange
|
|
(SecureStore.getItemAsync as jest.Mock).mockResolvedValue('expired-refresh-token');
|
|
|
|
const mockResponse = {
|
|
ok: false,
|
|
status: 401,
|
|
json: async () => ({
|
|
success: false,
|
|
error: 'REFRESH_TOKEN_EXPIRED',
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
|
|
// Act
|
|
const result = await authService.refreshToken();
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('REFRESH_TOKEN_EXPIRED');
|
|
});
|
|
});
|
|
|
|
describe('checkAuthStatus', () => {
|
|
it('should return true when valid token exists', async () => {
|
|
// Arrange
|
|
(tokenManager.getValidToken as jest.Mock).mockResolvedValue('valid-token');
|
|
|
|
// Act
|
|
const result = await authService.checkAuthStatus();
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when no token exists', async () => {
|
|
// Arrange
|
|
(tokenManager.getValidToken as jest.Mock).mockResolvedValue(null);
|
|
|
|
// Act
|
|
const result = await authService.checkAuthStatus();
|
|
|
|
// Assert
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should refresh expired token automatically', async () => {
|
|
// Arrange
|
|
(tokenManager.getValidToken as jest.Mock)
|
|
.mockResolvedValueOnce(null) // First call: no valid token
|
|
.mockResolvedValueOnce('new-valid-token'); // After refresh
|
|
|
|
(authService.refreshToken as jest.Mock) = jest.fn().mockResolvedValue({
|
|
success: true,
|
|
appToken: 'new-valid-token',
|
|
});
|
|
|
|
// Act
|
|
const result = await authService.checkAuthStatus();
|
|
|
|
// Assert
|
|
expect(result).toBe(true);
|
|
expect(authService.refreshToken).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Integration with TokenManager', () => {
|
|
it('should notify TokenManager of new tokens', async () => {
|
|
// Arrange
|
|
const mockResponse = {
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
...mockTokens,
|
|
user: mockUser,
|
|
}),
|
|
};
|
|
|
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
(SecureStore.setItemAsync as jest.Mock).mockResolvedValue(undefined);
|
|
(tokenManager.setTokens as jest.Mock).mockResolvedValue(undefined);
|
|
|
|
// Act
|
|
await authService.signIn('test@example.com', 'password123');
|
|
|
|
// Assert
|
|
expect(tokenManager.setTokens).toHaveBeenCalledWith(mockTokens);
|
|
});
|
|
});
|
|
});
|