imageFilenames and imageFilename, backend integration tests, frontend tests, removed username references
This commit is contained in:
461
frontend/src/__tests__/contexts/AuthContext.test.tsx
Normal file
461
frontend/src/__tests__/contexts/AuthContext.test.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor, act, fireEvent } from '@testing-library/react';
|
||||
import { AuthProvider, useAuth } from '../../contexts/AuthContext';
|
||||
import { mockUser } from '../../mocks/handlers';
|
||||
|
||||
// Mock the API module
|
||||
jest.mock('../../services/api', () => {
|
||||
const mockAuthAPI = {
|
||||
login: jest.fn(),
|
||||
register: jest.fn(),
|
||||
googleLogin: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
getStatus: jest.fn(),
|
||||
};
|
||||
|
||||
const mockFetchCSRFToken = jest.fn().mockResolvedValue('test-csrf-token');
|
||||
const mockResetCSRFToken = jest.fn();
|
||||
|
||||
return {
|
||||
authAPI: mockAuthAPI,
|
||||
fetchCSRFToken: mockFetchCSRFToken,
|
||||
resetCSRFToken: mockResetCSRFToken,
|
||||
};
|
||||
});
|
||||
|
||||
// Get mocked modules
|
||||
import { authAPI, fetchCSRFToken, resetCSRFToken } from '../../services/api';
|
||||
|
||||
const mockAuthAPI = authAPI as jest.Mocked<typeof authAPI>;
|
||||
const mockFetchCSRFToken = fetchCSRFToken as jest.MockedFunction<typeof fetchCSRFToken>;
|
||||
const mockResetCSRFToken = resetCSRFToken as jest.MockedFunction<typeof resetCSRFToken>;
|
||||
|
||||
// Test component that uses the auth context
|
||||
const TestComponent: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
|
||||
<div data-testid="verified">{auth.user?.isVerified ? 'verified' : 'not-verified'}</div>
|
||||
<div data-testid="modal-open">{auth.showAuthModal ? 'open' : 'closed'}</div>
|
||||
<div data-testid="modal-mode">{auth.authModalMode}</div>
|
||||
<button onClick={() => auth.login('test@example.com', 'password123')}>Login</button>
|
||||
<button onClick={() => auth.register({ email: 'new@example.com', username: 'newuser', password: 'password123' })}>Register</button>
|
||||
<button onClick={() => auth.googleLogin('valid-google-code')}>Google Login</button>
|
||||
<button onClick={() => auth.logout()}>Logout</button>
|
||||
<button onClick={() => auth.openAuthModal('login')}>Open Login Modal</button>
|
||||
<button onClick={() => auth.openAuthModal('signup')}>Open Signup Modal</button>
|
||||
<button onClick={() => auth.closeAuthModal()}>Close Modal</button>
|
||||
<button onClick={() => auth.checkAuth()}>Check Auth</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Wrapper component for testing
|
||||
const renderWithAuth = (ui: React.ReactElement = <TestComponent />) => {
|
||||
return render(
|
||||
<AuthProvider>
|
||||
{ui}
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('AuthContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Default: user is authenticated
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: true, user: mockUser },
|
||||
});
|
||||
mockFetchCSRFToken.mockResolvedValue('test-csrf-token');
|
||||
});
|
||||
|
||||
describe('useAuth hook', () => {
|
||||
it('throws error when used outside AuthProvider', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => render(<TestComponent />)).toThrow('useAuth must be used within an AuthProvider');
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('starts with loading state', () => {
|
||||
renderWithAuth();
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('loading');
|
||||
});
|
||||
|
||||
it('checks authentication status on mount', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
expect(mockAuthAPI.getStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets user to null when not authenticated', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
|
||||
it('handles network errors gracefully', async () => {
|
||||
mockAuthAPI.getStatus.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Login', () => {
|
||||
it('logs in successfully with valid credentials', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.login.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
expect(mockAuthAPI.login).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps user as null when login fails', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.login.mockRejectedValue(new Error('Invalid credentials'));
|
||||
|
||||
// Create a test component that captures login errors
|
||||
const LoginErrorTestComponent: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
await auth.login('test@example.com', 'wrongpassword');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user">{auth.user ? auth.user.email : 'no-user'}</div>
|
||||
<div data-testid="error">{error || 'no-error'}</div>
|
||||
<button onClick={handleLogin}>Login</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginErrorTestComponent />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('error')).toHaveTextContent('Invalid credentials');
|
||||
});
|
||||
|
||||
// User should still be null after failed login
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Registration', () => {
|
||||
it('registers a new user successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.register.mockResolvedValue({
|
||||
data: { user: { ...mockUser, email: 'new@example.com', isVerified: false } },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Register'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('new@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Login', () => {
|
||||
it('logs in with Google successfully', async () => {
|
||||
mockAuthAPI.getStatus.mockResolvedValue({
|
||||
data: { authenticated: false, user: null },
|
||||
});
|
||||
|
||||
mockAuthAPI.googleLogin.mockResolvedValue({
|
||||
data: { user: mockUser },
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Google Login'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
expect(mockAuthAPI.googleLogin).toHaveBeenCalledWith('valid-google-code');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logout', () => {
|
||||
it('logs out successfully', async () => {
|
||||
mockAuthAPI.logout.mockResolvedValue({ data: { message: 'Logged out' } });
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Logout'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
|
||||
expect(mockResetCSRFToken).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears user state even if logout API fails', async () => {
|
||||
mockAuthAPI.logout.mockRejectedValue(new Error('Server error'));
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Logout'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Auth Modal', () => {
|
||||
it('opens login modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Login Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
expect(screen.getByTestId('modal-mode')).toHaveTextContent('login');
|
||||
});
|
||||
|
||||
it('opens signup modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Signup Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
expect(screen.getByTestId('modal-mode')).toHaveTextContent('signup');
|
||||
});
|
||||
|
||||
it('closes modal', async () => {
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Open Login Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('open');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Close Modal'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('modal-open')).toHaveTextContent('closed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('updates user state', async () => {
|
||||
const TestComponentWithUpdate: React.FC = () => {
|
||||
const auth = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">{auth.loading ? 'loading' : 'not-loading'}</div>
|
||||
<div data-testid="user-email">{auth.user?.email || 'no-user'}</div>
|
||||
<div data-testid="user-name">{auth.user?.firstName || 'no-name'}</div>
|
||||
<button onClick={() => auth.updateUser({
|
||||
...auth.user!,
|
||||
firstName: 'Updated',
|
||||
lastName: 'Name',
|
||||
})}>
|
||||
Update User
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderWithAuth(<TestComponentWithUpdate />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user-name')).toHaveTextContent('Test');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Update User'));
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user-name')).toHaveTextContent('Updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAuth', () => {
|
||||
it('refreshes authentication status', async () => {
|
||||
let callCount = 0;
|
||||
|
||||
mockAuthAPI.getStatus.mockImplementation(() => {
|
||||
callCount++;
|
||||
if (callCount === 1) {
|
||||
return Promise.resolve({ data: { authenticated: false, user: null } });
|
||||
}
|
||||
return Promise.resolve({ data: { authenticated: true, user: mockUser } });
|
||||
});
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('no-user');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Check Auth'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('user')).toHaveTextContent('test@example.com');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth Callback Handling', () => {
|
||||
it('skips auth check on OAuth callback page', async () => {
|
||||
// Mock being on the OAuth callback page
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...window.location,
|
||||
pathname: '/auth/google/callback',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
mockAuthAPI.getStatus.mockClear();
|
||||
|
||||
renderWithAuth();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading')).toHaveTextContent('not-loading');
|
||||
});
|
||||
|
||||
// Status should not be called on OAuth callback page
|
||||
expect(mockAuthAPI.getStatus).not.toHaveBeenCalled();
|
||||
|
||||
// Reset location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
...window.location,
|
||||
pathname: '/',
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
346
frontend/src/__tests__/hooks/useAddressAutocomplete.test.ts
Normal file
346
frontend/src/__tests__/hooks/useAddressAutocomplete.test.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useAddressAutocomplete, usStates } from '../../hooks/useAddressAutocomplete';
|
||||
import { PlaceDetails } from '../../services/placesService';
|
||||
|
||||
describe('useAddressAutocomplete', () => {
|
||||
describe('usStates', () => {
|
||||
it('contains all 50 US states', () => {
|
||||
expect(usStates).toHaveLength(50);
|
||||
});
|
||||
|
||||
it('includes common states', () => {
|
||||
expect(usStates).toContain('California');
|
||||
expect(usStates).toContain('New York');
|
||||
expect(usStates).toContain('Texas');
|
||||
expect(usStates).toContain('Florida');
|
||||
});
|
||||
|
||||
it('states are in alphabetical order', () => {
|
||||
const sorted = [...usStates].sort();
|
||||
expect(usStates).toEqual(sorted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePlace', () => {
|
||||
const { result } = renderHook(() => useAddressAutocomplete());
|
||||
|
||||
it('parses a complete place correctly', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '123 Main Street, Los Angeles, CA 90210, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '123',
|
||||
route: 'Main Street',
|
||||
locality: 'Los Angeles',
|
||||
administrativeAreaLevel1: 'CA',
|
||||
administrativeAreaLevel1Long: 'California',
|
||||
postalCode: '90210',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 34.0522,
|
||||
longitude: -118.2437,
|
||||
},
|
||||
placeId: 'test-place-id',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('123 Main Street');
|
||||
expect(parsed!.city).toBe('Los Angeles');
|
||||
expect(parsed!.state).toBe('California');
|
||||
expect(parsed!.zipCode).toBe('90210');
|
||||
expect(parsed!.country).toBe('US');
|
||||
expect(parsed!.latitude).toBe(34.0522);
|
||||
expect(parsed!.longitude).toBe(-118.2437);
|
||||
});
|
||||
|
||||
it('converts state codes to full names', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '456 Oak Ave, New York, NY 10001, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '456',
|
||||
route: 'Oak Ave',
|
||||
locality: 'New York',
|
||||
administrativeAreaLevel1: 'NY',
|
||||
administrativeAreaLevel1Long: 'New York',
|
||||
postalCode: '10001',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 40.7128,
|
||||
longitude: -74.006,
|
||||
},
|
||||
placeId: 'test-place-id-2',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('New York');
|
||||
});
|
||||
|
||||
it('uses formatted address when street number and route are missing', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: 'Some Place, Austin, TX 78701, USA',
|
||||
addressComponents: {
|
||||
locality: 'Austin',
|
||||
administrativeAreaLevel1: 'TX',
|
||||
administrativeAreaLevel1Long: 'Texas',
|
||||
postalCode: '78701',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 30.2672,
|
||||
longitude: -97.7431,
|
||||
},
|
||||
placeId: 'test-place-id-3',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('Some Place, Austin, TX 78701, USA');
|
||||
});
|
||||
|
||||
it('handles missing postal code gracefully', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '789 Pine St, Seattle, WA, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '789',
|
||||
route: 'Pine St',
|
||||
locality: 'Seattle',
|
||||
administrativeAreaLevel1: 'WA',
|
||||
administrativeAreaLevel1Long: 'Washington',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 47.6062,
|
||||
longitude: -122.3321,
|
||||
},
|
||||
placeId: 'test-place-id-4',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.zipCode).toBe('');
|
||||
expect(parsed!.state).toBe('Washington');
|
||||
});
|
||||
|
||||
it('handles missing city gracefully', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '100 Rural Road, CO 80000, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '100',
|
||||
route: 'Rural Road',
|
||||
administrativeAreaLevel1: 'CO',
|
||||
administrativeAreaLevel1Long: 'Colorado',
|
||||
postalCode: '80000',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 39.5501,
|
||||
longitude: -105.7821,
|
||||
},
|
||||
placeId: 'test-place-id-5',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.city).toBe('');
|
||||
});
|
||||
|
||||
it('sets state to empty string for unknown states', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '123 Street, City, XX 12345, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '123',
|
||||
route: 'Street',
|
||||
locality: 'City',
|
||||
administrativeAreaLevel1: 'XX',
|
||||
administrativeAreaLevel1Long: 'Unknown State',
|
||||
postalCode: '12345',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
},
|
||||
placeId: 'test-place-id-6',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('State not found in dropdown options')
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles DC (District of Columbia) correctly', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '1600 Pennsylvania Ave, Washington, DC 20500, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '1600',
|
||||
route: 'Pennsylvania Ave',
|
||||
locality: 'Washington',
|
||||
administrativeAreaLevel1: 'DC',
|
||||
administrativeAreaLevel1Long: 'District of Columbia',
|
||||
postalCode: '20500',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 38.8977,
|
||||
longitude: -77.0365,
|
||||
},
|
||||
placeId: 'test-place-id-dc',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
// DC is not in the 50 states list, so state should be empty
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.city).toBe('Washington');
|
||||
});
|
||||
|
||||
it('uses long state name from administrativeAreaLevel1Long when available', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '500 Beach Blvd, Miami, FL 33101, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '500',
|
||||
route: 'Beach Blvd',
|
||||
locality: 'Miami',
|
||||
administrativeAreaLevel1: 'FL',
|
||||
administrativeAreaLevel1Long: 'Florida',
|
||||
postalCode: '33101',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 25.7617,
|
||||
longitude: -80.1918,
|
||||
},
|
||||
placeId: 'test-place-id-fl',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.state).toBe('Florida');
|
||||
});
|
||||
|
||||
it('returns null and logs error when parsing fails', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
// Pass an object that will cause an error when accessing nested properties
|
||||
const invalidPlace = {
|
||||
formattedAddress: 'Test',
|
||||
addressComponents: null,
|
||||
geometry: { latitude: 0, longitude: 0 },
|
||||
placeId: 'test',
|
||||
} as unknown as PlaceDetails;
|
||||
|
||||
const parsed = result.current.parsePlace(invalidPlace);
|
||||
|
||||
expect(parsed).toBeNull();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Error parsing place details:',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles only street number without route', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '42, Chicago, IL 60601, USA',
|
||||
addressComponents: {
|
||||
streetNumber: '42',
|
||||
locality: 'Chicago',
|
||||
administrativeAreaLevel1: 'IL',
|
||||
administrativeAreaLevel1Long: 'Illinois',
|
||||
postalCode: '60601',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 41.8781,
|
||||
longitude: -87.6298,
|
||||
},
|
||||
placeId: 'test-place-id-number-only',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('42');
|
||||
});
|
||||
|
||||
it('handles only route without street number', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: 'Main Street, Boston, MA 02101, USA',
|
||||
addressComponents: {
|
||||
route: 'Main Street',
|
||||
locality: 'Boston',
|
||||
administrativeAreaLevel1: 'MA',
|
||||
administrativeAreaLevel1Long: 'Massachusetts',
|
||||
postalCode: '02101',
|
||||
country: 'US',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 42.3601,
|
||||
longitude: -71.0589,
|
||||
},
|
||||
placeId: 'test-place-id-route-only',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.address1).toBe('Main Street');
|
||||
});
|
||||
|
||||
it('defaults country to US when not provided', () => {
|
||||
const place: PlaceDetails = {
|
||||
formattedAddress: '999 Test St, Denver, CO 80202',
|
||||
addressComponents: {
|
||||
streetNumber: '999',
|
||||
route: 'Test St',
|
||||
locality: 'Denver',
|
||||
administrativeAreaLevel1: 'CO',
|
||||
administrativeAreaLevel1Long: 'Colorado',
|
||||
postalCode: '80202',
|
||||
},
|
||||
geometry: {
|
||||
latitude: 39.7392,
|
||||
longitude: -104.9903,
|
||||
},
|
||||
placeId: 'test-place-id-no-country',
|
||||
};
|
||||
|
||||
const parsed = result.current.parsePlace(place);
|
||||
|
||||
expect(parsed).not.toBeNull();
|
||||
expect(parsed!.country).toBe('US');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook stability', () => {
|
||||
it('returns stable parsePlace function', () => {
|
||||
const { result, rerender } = renderHook(() => useAddressAutocomplete());
|
||||
|
||||
const firstParsePlace = result.current.parsePlace;
|
||||
|
||||
rerender();
|
||||
|
||||
const secondParsePlace = result.current.parsePlace;
|
||||
|
||||
expect(firstParsePlace).toBe(secondParsePlace);
|
||||
});
|
||||
});
|
||||
});
|
||||
211
frontend/src/__tests__/services/api.test.ts
Normal file
211
frontend/src/__tests__/services/api.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* API Service Tests
|
||||
*
|
||||
* Tests the API service module structure and exported functions.
|
||||
* API interceptor behavior is tested in integration with AuthContext.
|
||||
*/
|
||||
|
||||
import {
|
||||
authAPI,
|
||||
userAPI,
|
||||
itemAPI,
|
||||
rentalAPI,
|
||||
messageAPI,
|
||||
mapsAPI,
|
||||
stripeAPI,
|
||||
addressAPI,
|
||||
conditionCheckAPI,
|
||||
forumAPI,
|
||||
feedbackAPI,
|
||||
fetchCSRFToken,
|
||||
resetCSRFToken,
|
||||
getMessageImageUrl,
|
||||
getForumImageUrl,
|
||||
} from '../../services/api';
|
||||
import api from '../../services/api';
|
||||
|
||||
describe('API Service', () => {
|
||||
describe('Module Exports', () => {
|
||||
it('exports authAPI with correct methods', () => {
|
||||
expect(authAPI).toBeDefined();
|
||||
expect(typeof authAPI.login).toBe('function');
|
||||
expect(typeof authAPI.register).toBe('function');
|
||||
expect(typeof authAPI.logout).toBe('function');
|
||||
expect(typeof authAPI.googleLogin).toBe('function');
|
||||
expect(typeof authAPI.getStatus).toBe('function');
|
||||
expect(typeof authAPI.verifyEmail).toBe('function');
|
||||
expect(typeof authAPI.forgotPassword).toBe('function');
|
||||
expect(typeof authAPI.resetPassword).toBe('function');
|
||||
});
|
||||
|
||||
it('exports userAPI with correct methods', () => {
|
||||
expect(userAPI).toBeDefined();
|
||||
expect(typeof userAPI.getProfile).toBe('function');
|
||||
expect(typeof userAPI.updateProfile).toBe('function');
|
||||
expect(typeof userAPI.uploadProfileImage).toBe('function');
|
||||
});
|
||||
|
||||
it('exports itemAPI with correct methods', () => {
|
||||
expect(itemAPI).toBeDefined();
|
||||
expect(typeof itemAPI.getItems).toBe('function');
|
||||
expect(typeof itemAPI.getItem).toBe('function');
|
||||
expect(typeof itemAPI.createItem).toBe('function');
|
||||
expect(typeof itemAPI.updateItem).toBe('function');
|
||||
expect(typeof itemAPI.deleteItem).toBe('function');
|
||||
});
|
||||
|
||||
it('exports rentalAPI with correct methods', () => {
|
||||
expect(rentalAPI).toBeDefined();
|
||||
expect(typeof rentalAPI.createRental).toBe('function');
|
||||
expect(typeof rentalAPI.getRentals).toBe('function');
|
||||
expect(typeof rentalAPI.getListings).toBe('function');
|
||||
expect(typeof rentalAPI.updateRentalStatus).toBe('function');
|
||||
expect(typeof rentalAPI.cancelRental).toBe('function');
|
||||
});
|
||||
|
||||
it('exports messageAPI with correct methods', () => {
|
||||
expect(messageAPI).toBeDefined();
|
||||
expect(typeof messageAPI.getMessages).toBe('function');
|
||||
expect(typeof messageAPI.getConversations).toBe('function');
|
||||
expect(typeof messageAPI.sendMessage).toBe('function');
|
||||
expect(typeof messageAPI.getUnreadCount).toBe('function');
|
||||
});
|
||||
|
||||
it('exports mapsAPI with correct methods', () => {
|
||||
expect(mapsAPI).toBeDefined();
|
||||
expect(typeof mapsAPI.placesAutocomplete).toBe('function');
|
||||
expect(typeof mapsAPI.placeDetails).toBe('function');
|
||||
expect(typeof mapsAPI.geocode).toBe('function');
|
||||
});
|
||||
|
||||
it('exports stripeAPI with correct methods', () => {
|
||||
expect(stripeAPI).toBeDefined();
|
||||
expect(typeof stripeAPI.getCheckoutSession).toBe('function');
|
||||
expect(typeof stripeAPI.createConnectedAccount).toBe('function');
|
||||
expect(typeof stripeAPI.createAccountLink).toBe('function');
|
||||
expect(typeof stripeAPI.getAccountStatus).toBe('function');
|
||||
});
|
||||
|
||||
it('exports CSRF token management functions', () => {
|
||||
expect(typeof fetchCSRFToken).toBe('function');
|
||||
expect(typeof resetCSRFToken).toBe('function');
|
||||
});
|
||||
|
||||
it('exports helper functions for image URLs', () => {
|
||||
expect(typeof getMessageImageUrl).toBe('function');
|
||||
expect(typeof getForumImageUrl).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper Functions', () => {
|
||||
it('getMessageImageUrl constructs correct URL', () => {
|
||||
const url = getMessageImageUrl('test-image.jpg');
|
||||
expect(url).toContain('/messages/images/test-image.jpg');
|
||||
});
|
||||
|
||||
it('getForumImageUrl constructs correct URL', () => {
|
||||
const url = getForumImageUrl('forum-image.jpg');
|
||||
expect(url).toContain('/uploads/forum/forum-image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSRF Token Management', () => {
|
||||
it('resetCSRFToken clears the token', () => {
|
||||
// Should not throw
|
||||
expect(() => resetCSRFToken()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Configuration', () => {
|
||||
it('creates axios instance with correct base URL', () => {
|
||||
expect(api).toBeDefined();
|
||||
expect(api.defaults).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('API Namespaces', () => {
|
||||
describe('authAPI', () => {
|
||||
it('has all authentication methods', () => {
|
||||
const expectedMethods = [
|
||||
'register',
|
||||
'login',
|
||||
'googleLogin',
|
||||
'logout',
|
||||
'refresh',
|
||||
'getCSRFToken',
|
||||
'getStatus',
|
||||
'verifyEmail',
|
||||
'resendVerification',
|
||||
'forgotPassword',
|
||||
'verifyResetToken',
|
||||
'resetPassword',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((authAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (authAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addressAPI', () => {
|
||||
it('has all address management methods', () => {
|
||||
const expectedMethods = [
|
||||
'getAddresses',
|
||||
'createAddress',
|
||||
'updateAddress',
|
||||
'deleteAddress',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((addressAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (addressAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('conditionCheckAPI', () => {
|
||||
it('has all condition check methods', () => {
|
||||
const expectedMethods = [
|
||||
'submitConditionCheck',
|
||||
'getConditionChecks',
|
||||
'getConditionCheckTimeline',
|
||||
'getAvailableChecks',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((conditionCheckAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (conditionCheckAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forumAPI', () => {
|
||||
it('has all forum methods', () => {
|
||||
const expectedMethods = [
|
||||
'getPosts',
|
||||
'getPost',
|
||||
'createPost',
|
||||
'updatePost',
|
||||
'deletePost',
|
||||
'createComment',
|
||||
'updateComment',
|
||||
'deleteComment',
|
||||
'getTags',
|
||||
];
|
||||
|
||||
expectedMethods.forEach((method) => {
|
||||
expect((forumAPI as any)[method]).toBeDefined();
|
||||
expect(typeof (forumAPI as any)[method]).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedbackAPI', () => {
|
||||
it('has feedback submission method', () => {
|
||||
expect(feedbackAPI.submitFeedback).toBeDefined();
|
||||
expect(typeof feedbackAPI.submitFeedback).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user