Add user store edge case tests - TEST-016 complete (20 tests)
Quick win #3: Test coverage for user store edge cases Tests cover: - fetchProfile success and error paths - fetchProfile with/without authentication - fetchProfile with missing/null fields - fetchProfile API errors (404, network, generic) - fetchProfile concurrent calls - updateDisplayName success and error paths - updateDisplayName validation errors (too short, profanity) - updateDisplayName with/without authentication - Loading state management during operations - Error clearing on successful operations - Auth store synchronization Results: - 20 new tests, all passing - User store coverage: 52% → ~90%+ (estimated) - Complete edge case coverage for profile operations - All authentication state transitions tested All quick wins complete! Total: 65 new tests across 3 files Test count: 1000 → 1065 (+6.5%)
This commit is contained in:
parent
d03dc1ddd2
commit
8aab41485d
@ -1,7 +1,18 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest'
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
import { setActivePinia, createPinia } from 'pinia'
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
|
||||||
import { useUserStore } from './user'
|
import { useUserStore } from './user'
|
||||||
|
import { useAuthStore } from './auth'
|
||||||
|
import { apiClient } from '@/api/client'
|
||||||
|
import { ApiError } from '@/api/types'
|
||||||
|
|
||||||
|
// Mock the API client
|
||||||
|
vi.mock('@/api/client', () => ({
|
||||||
|
apiClient: {
|
||||||
|
get: vi.fn(),
|
||||||
|
patch: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
describe('useUserStore', () => {
|
describe('useUserStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -112,4 +123,637 @@ describe('useUserStore', () => {
|
|||||||
expect(store.linkedAccounts).toHaveLength(1)
|
expect(store.linkedAccounts).toHaveLength(1)
|
||||||
expect(store.linkedAccounts[0].provider).toBe('discord')
|
expect(store.linkedAccounts[0].provider).toBe('discord')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('fetchProfile', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('successfully fetches and transforms profile data', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile retrieves and transforms API data correctly.
|
||||||
|
*
|
||||||
|
* The store should fetch profile from API, transform snake_case to
|
||||||
|
* camelCase, and update both user store and auth store.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// Set auth as authenticated
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'Test User',
|
||||||
|
avatar_url: 'https://example.com/avatar.png',
|
||||||
|
has_starter_deck: true,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
linked_accounts: [
|
||||||
|
{
|
||||||
|
provider: 'google',
|
||||||
|
provider_user_id: 'google-123',
|
||||||
|
email: 'test@gmail.com',
|
||||||
|
linked_at: '2026-01-01T00:00:00Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(store.profile).not.toBeNull()
|
||||||
|
expect(store.profile?.id).toBe('user-123')
|
||||||
|
expect(store.profile?.displayName).toBe('Test User')
|
||||||
|
expect(store.profile?.linkedAccounts).toHaveLength(1)
|
||||||
|
expect(store.error).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false when not authenticated', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile fails gracefully when not authenticated.
|
||||||
|
*
|
||||||
|
* If user is not logged in, fetchProfile should return false
|
||||||
|
* and set an error without making API call.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// Ensure not authenticated
|
||||||
|
await authStore.logout(false) // Don't revoke on server in test
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Not authenticated')
|
||||||
|
expect(apiClient.get).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles API error responses', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile handles API errors gracefully.
|
||||||
|
*
|
||||||
|
* When API returns an error, the store should capture the
|
||||||
|
* error message and return false.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiError = new ApiError(
|
||||||
|
404,
|
||||||
|
'Not Found',
|
||||||
|
'User profile does not exist'
|
||||||
|
)
|
||||||
|
vi.mocked(apiClient.get).mockRejectedValueOnce(apiError)
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('User profile does not exist')
|
||||||
|
expect(store.profile).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles generic errors', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile handles non-API errors.
|
||||||
|
*
|
||||||
|
* Network errors or unexpected failures should be captured
|
||||||
|
* with a generic error message.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockRejectedValueOnce(new Error('Network failure'))
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Network failure')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles non-Error exceptions', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile handles thrown strings or objects.
|
||||||
|
*
|
||||||
|
* Some errors might be thrown as strings or plain objects.
|
||||||
|
* The store should handle these gracefully.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockRejectedValueOnce('Something went wrong')
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Failed to fetch profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles profile with no linked accounts', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile handles missing linked_accounts field.
|
||||||
|
*
|
||||||
|
* Not all users have linked accounts. The store should default
|
||||||
|
* to an empty array if the field is missing.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-456',
|
||||||
|
display_name: 'New User',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-02T00:00:00Z',
|
||||||
|
// linked_accounts field missing
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
const result = await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(store.profile?.linkedAccounts).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates auth store with profile data', async () => {
|
||||||
|
/**
|
||||||
|
* Test that fetchProfile syncs data to auth store.
|
||||||
|
*
|
||||||
|
* The auth store needs basic profile info for guards and UI.
|
||||||
|
* fetchProfile should update it after successful fetch.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const setUserSpy = vi.spyOn(authStore, 'setUser')
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-789',
|
||||||
|
display_name: 'Sync User',
|
||||||
|
avatar_url: 'https://example.com/sync.png',
|
||||||
|
has_starter_deck: true,
|
||||||
|
created_at: '2026-01-03T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(setUserSpy).toHaveBeenCalledWith({
|
||||||
|
id: 'user-789',
|
||||||
|
displayName: 'Sync User',
|
||||||
|
avatarUrl: 'https://example.com/sync.png',
|
||||||
|
hasStarterDeck: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets loading state during fetch', async () => {
|
||||||
|
/**
|
||||||
|
* Test that isLoading is true during API call.
|
||||||
|
*
|
||||||
|
* Components need to show loading spinners while profile
|
||||||
|
* is being fetched.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
let loadingDuringFetch = false
|
||||||
|
vi.mocked(apiClient.get).mockImplementation(async () => {
|
||||||
|
loadingDuringFetch = store.isLoading
|
||||||
|
return {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'Test',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(loadingDuringFetch).toBe(true)
|
||||||
|
expect(store.isLoading).toBe(false) // Should be false after completion
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears error on successful fetch', async () => {
|
||||||
|
/**
|
||||||
|
* Test that successful fetch clears previous errors.
|
||||||
|
*
|
||||||
|
* If a previous fetch failed, a successful fetch should
|
||||||
|
* clear the error state.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set an existing error
|
||||||
|
store.error = 'Previous error'
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'Test',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(store.error).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateDisplayName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('successfully updates display name', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName updates the name via API.
|
||||||
|
*
|
||||||
|
* The store should send the new name to API, then refetch
|
||||||
|
* the profile to get the updated data.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedProfile = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'New Name',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: true,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.patch).mockResolvedValueOnce(undefined)
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(updatedProfile)
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(apiClient.patch).toHaveBeenCalledWith('/api/users/me', {
|
||||||
|
display_name: 'New Name',
|
||||||
|
})
|
||||||
|
expect(apiClient.get).toHaveBeenCalledWith('/api/users/me')
|
||||||
|
expect(store.displayName).toBe('New Name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false when not authenticated', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName fails when not authenticated.
|
||||||
|
*
|
||||||
|
* Users must be logged in to update their profile.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
await authStore.logout(false)
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Not authenticated')
|
||||||
|
expect(apiClient.patch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles API validation errors', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName handles validation errors.
|
||||||
|
*
|
||||||
|
* API may reject names that are too short, too long, or
|
||||||
|
* contain profanity. These errors should be captured.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const validationError = new ApiError(
|
||||||
|
422,
|
||||||
|
'Unprocessable Entity',
|
||||||
|
'Display name must be between 3 and 20 characters'
|
||||||
|
)
|
||||||
|
vi.mocked(apiClient.patch).mockRejectedValueOnce(validationError)
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('AB')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Display name must be between 3 and 20 characters')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles profanity filter errors', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName handles profanity errors.
|
||||||
|
*
|
||||||
|
* API blocks profane or inappropriate names. These should
|
||||||
|
* be communicated clearly to the user.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const profanityError = new ApiError(
|
||||||
|
422,
|
||||||
|
'Unprocessable Entity',
|
||||||
|
'Display name contains inappropriate content'
|
||||||
|
)
|
||||||
|
vi.mocked(apiClient.patch).mockRejectedValueOnce(profanityError)
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('BadName')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Display name contains inappropriate content')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles network errors during update', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName handles network failures.
|
||||||
|
*
|
||||||
|
* Network errors during update should not crash the app.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mocked(apiClient.patch).mockRejectedValueOnce(new Error('Network timeout'))
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Network timeout')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles generic errors during update', async () => {
|
||||||
|
/**
|
||||||
|
* Test that updateDisplayName handles unexpected errors.
|
||||||
|
*
|
||||||
|
* Any unexpected error should result in a generic message.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mocked(apiClient.patch).mockRejectedValueOnce({ unexpected: 'error' })
|
||||||
|
|
||||||
|
const result = await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(store.error).toBe('Failed to update profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets loading state during update', async () => {
|
||||||
|
/**
|
||||||
|
* Test that isLoading is true during update.
|
||||||
|
*
|
||||||
|
* Components should show loading state while update is
|
||||||
|
* in progress.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
let loadingDuringUpdate = false
|
||||||
|
vi.mocked(apiClient.patch).mockImplementation(async () => {
|
||||||
|
loadingDuringUpdate = store.isLoading
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'New Name',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(loadingDuringUpdate).toBe(true)
|
||||||
|
expect(store.isLoading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears error on successful update', async () => {
|
||||||
|
/**
|
||||||
|
* Test that successful update clears previous errors.
|
||||||
|
*
|
||||||
|
* If a previous update failed, a successful one should
|
||||||
|
* clear the error state.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
store.error = 'Previous error'
|
||||||
|
|
||||||
|
vi.mocked(apiClient.patch).mockResolvedValueOnce(undefined)
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce({
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'New Name',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
await store.updateDisplayName('New Name')
|
||||||
|
|
||||||
|
expect(store.error).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles concurrent fetchProfile calls', async () => {
|
||||||
|
/**
|
||||||
|
* Test that concurrent fetchProfile calls don't cause issues.
|
||||||
|
*
|
||||||
|
* If multiple components call fetchProfile simultaneously,
|
||||||
|
* the store should handle it gracefully.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'Test',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValue(apiResponse)
|
||||||
|
|
||||||
|
// Call fetchProfile three times concurrently
|
||||||
|
const results = await Promise.all([
|
||||||
|
store.fetchProfile(),
|
||||||
|
store.fetchProfile(),
|
||||||
|
store.fetchProfile(),
|
||||||
|
])
|
||||||
|
|
||||||
|
// All should succeed
|
||||||
|
expect(results).toEqual([true, true, true])
|
||||||
|
// API should have been called three times
|
||||||
|
expect(apiClient.get).toHaveBeenCalledTimes(3)
|
||||||
|
// Store should have the profile
|
||||||
|
expect(store.profile).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles profile with null avatar_url', async () => {
|
||||||
|
/**
|
||||||
|
* Test that null avatar_url is handled correctly.
|
||||||
|
*
|
||||||
|
* Users without avatars should have null avatar_url,
|
||||||
|
* not undefined or empty string.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: 'Test',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
await store.fetchProfile()
|
||||||
|
|
||||||
|
expect(store.avatarUrl).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles empty display name in API response', async () => {
|
||||||
|
/**
|
||||||
|
* Test that empty display name falls back to "Unknown".
|
||||||
|
*
|
||||||
|
* If API returns empty display name (shouldn't happen but
|
||||||
|
* defensive), the computed property should provide fallback.
|
||||||
|
*/
|
||||||
|
const store = useUserStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
authStore.setTokens({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: 'refresh',
|
||||||
|
expiresAt: Date.now() + 3600000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = {
|
||||||
|
id: 'user-123',
|
||||||
|
display_name: '',
|
||||||
|
avatar_url: null,
|
||||||
|
has_starter_deck: false,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(apiClient.get).mockResolvedValueOnce(apiResponse)
|
||||||
|
|
||||||
|
await store.fetchProfile()
|
||||||
|
|
||||||
|
// Store has empty string, computed should still use it
|
||||||
|
expect(store.profile?.displayName).toBe('')
|
||||||
|
// But if profile was null, computed would return "Unknown"
|
||||||
|
store.profile = null
|
||||||
|
expect(store.displayName).toBe('Unknown')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user