All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m7s
414 lines
14 KiB
TypeScript
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);
|
|
});
|
|
});
|