Files
flyer-crawler.projectium.com/src/hooks/queries/useUserProfileDataQuery.test.tsx
Torben Sorensen 75406cd924
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m7s
typescript fix
2026-01-29 17:21:55 -08:00

414 lines
14 KiB
TypeScript

// src/hooks/queries/useUserProfileDataQuery.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { useUserProfileDataQuery } from './useUserProfileDataQuery';
import * as apiClient from '../../services/apiClient';
import type { UserProfile, Achievement, UserAchievement } from '../../types';
vi.mock('../../services/apiClient');
const mockedApiClient = vi.mocked(apiClient);
describe('useUserProfileDataQuery', () => {
let queryClient: QueryClient;
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
beforeEach(() => {
vi.resetAllMocks();
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
});
const mockProfile: UserProfile = {
full_name: 'Test User',
avatar_url: 'https://example.com/avatar.png',
address_id: 1,
points: 100,
role: 'user',
preferences: { darkMode: false, unitSystem: 'metric' },
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
user: {
user_id: 'user-123',
email: 'test@example.com',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
address: {
address_id: 1,
address_line_1: '123 Main St',
city: 'Test City',
province_state: 'ON',
postal_code: 'A1B 2C3',
country: 'Canada',
latitude: null,
longitude: null,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
};
const mockAchievements: (UserAchievement & Achievement)[] = [
{
user_id: 'user-123',
achievement_id: 1,
achieved_at: '2025-01-15T10:00:00Z',
name: 'First Upload',
description: 'Uploaded your first flyer',
icon: 'trophy',
points_value: 10,
created_at: '2025-01-01T00:00:00Z',
},
{
user_id: 'user-123',
achievement_id: 2,
achieved_at: '2025-01-20T15:30:00Z',
name: 'Deal Hunter',
description: 'Found 10 deals',
icon: 'star',
points_value: 25,
created_at: '2025-01-01T00:00:00Z',
},
];
it('should fetch user profile and achievements successfully', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalled();
expect(mockedApiClient.getUserAchievements).toHaveBeenCalled();
expect(result.current.data).toEqual({
profile: mockProfile,
achievements: mockAchievements,
});
});
it('should handle profile API error with error message', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: false,
status: 401,
json: () => Promise.resolve({ message: 'Authentication required' }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Authentication required');
});
it('should handle profile API error without message', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.reject(new Error('Parse error')),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Request failed with status 500');
});
it('should use fallback message when profile error.message is empty', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.resolve({ message: '' }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Failed to fetch user profile');
});
it('should handle achievements API error with error message', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: false,
status: 403,
json: () => Promise.resolve({ message: 'Access denied' }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Access denied');
});
it('should handle achievements API error without message', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.reject(new Error('Parse error')),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Request failed with status 500');
});
it('should use fallback message when achievements error.message is empty', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: false,
status: 500,
json: () => Promise.resolve({ message: '' }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe('Failed to fetch user achievements');
});
it('should return empty array for no achievements', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: [] }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({
profile: mockProfile,
achievements: [],
});
});
it('should handle undefined achievements data gracefully', async () => {
// When API returns response without data wrapper (legacy format)
// achievementsJson.data will be undefined, falling back to achievementsJson itself
// Then achievements || [] will convert falsy value to empty array
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
// Return empty array directly (no wrapper) - this tests the fallback path
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve([]),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({
profile: mockProfile,
achievements: [],
});
});
it('should handle response without data wrapper (direct response)', async () => {
// Some APIs may return data directly without the { success, data } wrapper
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockProfile),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockAchievements),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({
profile: mockProfile,
achievements: mockAchievements,
});
});
it('should not fetch when disabled', () => {
renderHook(() => useUserProfileDataQuery(false), { wrapper });
expect(mockedApiClient.getAuthenticatedUserProfile).not.toHaveBeenCalled();
expect(mockedApiClient.getUserAchievements).not.toHaveBeenCalled();
});
it('should fetch when enabled is explicitly true', async () => {
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(true), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalled();
expect(mockedApiClient.getUserAchievements).toHaveBeenCalled();
});
it('should handle profile with minimal data', async () => {
const minimalProfile: UserProfile = {
full_name: null,
avatar_url: null,
address_id: null,
points: 0,
role: 'user',
preferences: null,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
user: {
user_id: 'user-456',
email: 'minimal@example.com',
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
address: null,
};
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: minimalProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: [] }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({
profile: minimalProfile,
achievements: [],
});
});
it('should handle admin user profile', async () => {
const adminProfile: UserProfile = {
...mockProfile,
role: 'admin',
points: 500,
};
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: adminProfile }),
} as Response);
mockedApiClient.getUserAchievements.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data?.profile.role).toBe('admin');
expect(result.current.data?.profile.points).toBe(500);
});
it('should call both APIs in parallel', async () => {
let profileCallTime: number | null = null;
let achievementsCallTime: number | null = null;
mockedApiClient.getAuthenticatedUserProfile.mockImplementation(() => {
profileCallTime = Date.now();
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: true, data: mockProfile }),
} as Response);
});
mockedApiClient.getUserAchievements.mockImplementation(() => {
achievementsCallTime = Date.now();
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ success: true, data: mockAchievements }),
} as Response);
});
const { result } = renderHook(() => useUserProfileDataQuery(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
// Both calls should have happened
expect(profileCallTime).not.toBeNull();
expect(achievementsCallTime).not.toBeNull();
// Both calls should have been made nearly simultaneously (within 50ms)
// This verifies Promise.all is being used for parallel execution
expect(
Math.abs(
(profileCallTime as unknown as number) - (achievementsCallTime as unknown as number),
),
).toBeLessThan(50);
});
});