test: Add comprehensive tests for UserAdapter

- Tests for all UserAdapter methods (getInfo, getProfile, getFavorites, etc.)
- Tests for error handling and caching behavior
- Fixed caching tests to explicitly enable caching (disabled by default)
- Added cache clearing after profile updates
- All 21 tests passing successfully
This commit is contained in:
Justin Edmund 2025-09-20 00:46:28 -07:00
parent 683c28e172
commit 87a65408f9
2 changed files with 469 additions and 5 deletions

View file

@ -0,0 +1,458 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { UserAdapter } from '../user.adapter'
import type { UserInfo, UserProfile } from '../user.adapter'
import type { Party } from '$lib/types/api/party'
describe('UserAdapter', () => {
let adapter: UserAdapter
let mockFetch: ReturnType<typeof vi.fn>
const mockUserInfo: UserInfo = {
id: 'user-1',
username: 'testuser',
language: 'en',
private: false,
gender: 1,
theme: 'dark',
role: 0,
avatar: {
picture: 'avatar.jpg',
element: 'fire'
}
}
const mockUserProfile: UserProfile = {
...mockUserInfo,
parties: [
{
id: 'party-1',
shortcode: 'abc123',
name: 'Test Party',
user: mockUserInfo
} as Party
]
}
const mockParty: Party = {
id: 'party-1',
shortcode: 'abc123',
name: 'Fire Team',
user: mockUserInfo,
visibility: 0,
element: 1,
characters: [],
weapons: [],
summons: []
} as Party
beforeEach(() => {
mockFetch = vi.fn()
global.fetch = mockFetch
adapter = new UserAdapter()
})
describe('getInfo', () => {
it('should fetch user information', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUserInfo
})
const result = await adapter.getInfo('testuser')
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/info/testuser'),
expect.any(Object)
)
expect(result).toEqual(mockUserInfo)
})
it('should encode username in URL', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUserInfo
})
await adapter.getInfo('user with spaces')
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/info/user%20with%20spaces'),
expect.any(Object)
)
})
})
describe('getProfile', () => {
it('should fetch user profile with parties', async () => {
const response = {
profile: mockUserProfile,
meta: {
count: 10,
total_pages: 2,
per_page: 5
}
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
const result = await adapter.getProfile('testuser')
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/testuser'),
expect.any(Object)
)
expect(result).toEqual({
user: mockUserProfile,
items: mockUserProfile.parties,
page: 1,
total: 10,
totalPages: 2,
perPage: 5
})
})
it('should handle pagination', async () => {
const response = {
profile: mockUserProfile,
meta: {
count: 10,
total_pages: 2,
per_page: 5
}
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
await adapter.getProfile('testuser', 2)
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/testuser'),
expect.objectContaining({
params: { page: 2 }
})
)
})
it('should handle empty parties array', async () => {
const response = {
profile: { ...mockUserProfile, parties: undefined },
meta: {}
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
const result = await adapter.getProfile('testuser')
expect(result.items).toEqual([])
})
it('should not include page param for page 1', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ profile: mockUserProfile })
})
await adapter.getProfile('testuser', 1)
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/testuser'),
expect.objectContaining({
params: undefined
})
)
})
})
describe('getFavorites', () => {
it('should fetch favorite parties', async () => {
const response = {
results: [mockParty],
total: 5,
total_pages: 1,
per: 20
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
const result = await adapter.getFavorites()
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/parties/favorites'),
expect.any(Object)
)
expect(result).toEqual({
items: [mockParty],
page: 1,
total: 5,
totalPages: 1,
perPage: 20
})
})
it('should handle pagination for favorites', async () => {
const response = {
results: [mockParty],
total: 30,
total_pages: 3,
per: 10
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
await adapter.getFavorites({ page: 2 })
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/parties/favorites'),
expect.objectContaining({
params: { page: 2 }
})
)
})
it('should use default perPage when not provided', async () => {
const response = {
results: [],
total: 0,
total_pages: 0
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => response
})
const result = await adapter.getFavorites()
expect(result.perPage).toBe(20)
})
})
describe('checkUsernameAvailability', () => {
it('should check username availability', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ available: true })
})
const result = await adapter.checkUsernameAvailability('newuser')
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/check-username'),
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ username: 'newuser' })
})
)
expect(result).toEqual({ available: true })
})
it('should return unavailable for taken username', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ available: false })
})
const result = await adapter.checkUsernameAvailability('existinguser')
expect(result).toEqual({ available: false })
})
})
describe('checkEmailAvailability', () => {
it('should check email availability', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ available: true })
})
const result = await adapter.checkEmailAvailability('new@example.com')
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/check-email'),
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ email: 'new@example.com' })
})
)
expect(result).toEqual({ available: true })
})
})
describe('updateProfile', () => {
it('should update user profile', async () => {
const updates = {
username: 'newusername',
theme: 'light' as const
}
const updatedUser = { ...mockUserInfo, ...updates }
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => updatedUser
})
const result = await adapter.updateProfile(updates)
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/me'),
expect.objectContaining({
method: 'PUT',
body: JSON.stringify(updates)
})
)
expect(result).toEqual(updatedUser)
})
it('should handle partial updates', async () => {
const updates = { private: true }
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ ...mockUserInfo, ...updates })
})
await adapter.updateProfile(updates)
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/me'),
expect.objectContaining({
body: JSON.stringify(updates)
})
)
})
})
describe('getCurrentUser', () => {
it('should fetch current user', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUserInfo
})
const result = await adapter.getCurrentUser()
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/users/me'),
expect.any(Object)
)
expect(result).toEqual(mockUserInfo)
})
})
describe('error handling', () => {
it('should handle network errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'))
await expect(adapter.getInfo('testuser')).rejects.toThrow()
})
it('should handle API errors', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
json: async () => ({ error: 'User not found' })
})
await expect(adapter.getProfile('nonexistent')).rejects.toThrow()
})
})
describe('caching', () => {
it('should cache user info requests when cache option is provided', async () => {
// Create adapter with caching enabled
const cachedAdapter = new UserAdapter({ cacheTime: 60000 })
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockUserInfo
})
// First call
await cachedAdapter.getInfo('testuser')
// Second call should use cache
await cachedAdapter.getInfo('testuser')
expect(mockFetch).toHaveBeenCalledTimes(1)
})
it('should cache profile requests when cache option is provided', async () => {
// Create adapter with caching enabled
const cachedAdapter = new UserAdapter({ cacheTime: 60000 })
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
profile: mockUserProfile,
meta: {}
})
})
// First call
await cachedAdapter.getProfile('testuser')
// Second call should use cache
await cachedAdapter.getProfile('testuser')
expect(mockFetch).toHaveBeenCalledTimes(1)
})
it('should not cache different pages', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
profile: mockUserProfile,
meta: {}
})
})
await adapter.getProfile('testuser', 1)
await adapter.getProfile('testuser', 2)
expect(mockFetch).toHaveBeenCalledTimes(2)
})
it('should clear cache after updates', async () => {
// Create adapter with caching enabled
const cachedAdapter = new UserAdapter({ cacheTime: 60000 })
// Setup initial cached request
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUserInfo
})
await cachedAdapter.getCurrentUser()
// Perform update (should clear cache)
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ ...mockUserInfo, theme: 'light' })
})
await cachedAdapter.updateProfile({ theme: 'light' })
// Next getCurrentUser should hit the API again (cache was cleared)
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ ...mockUserInfo, theme: 'light' })
})
await cachedAdapter.getCurrentUser()
expect(mockFetch).toHaveBeenCalledTimes(3)
})
})
})

View file

@ -56,8 +56,8 @@ export class UserAdapter extends BaseAdapter {
items,
page,
total: response.meta?.count,
totalPages: response.meta?.total_pages,
perPage: response.meta?.per_page
totalPages: response.meta?.total_pages || response.meta?.totalPages,
perPage: response.meta?.per_page || response.meta?.perPage
}
}
@ -77,7 +77,8 @@ export class UserAdapter extends BaseAdapter {
const response = await this.request<{
results: Party[]
total: number
total_pages: number
total_pages?: number
totalPages?: number
per?: number
}>('/parties/favorites', { params })
@ -85,7 +86,7 @@ export class UserAdapter extends BaseAdapter {
items: response.results,
page,
total: response.total,
totalPages: response.total_pages,
totalPages: response.total_pages || response.totalPages || 1,
perPage: response.per || 20
}
}
@ -114,10 +115,15 @@ export class UserAdapter extends BaseAdapter {
* Update user profile
*/
async updateProfile(updates: Partial<UserInfo>): Promise<UserInfo> {
return this.request<UserInfo>('/users/me', {
const result = await this.request<UserInfo>('/users/me', {
method: 'PUT',
body: JSON.stringify(updates)
})
// Clear cache for current user after update
this.clearCache('/users/me')
return result
}
/**