test: add web-client API service unit coverage

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-23 12:49:07 +00:00
parent 0f828dafb0
commit c5688ee20d
3 changed files with 241 additions and 1 deletions

View File

@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { apiKeysApi } from '../api-keys.api';
import { userApi } from '../user.api';
vi.mock('../user.api', () => ({
userApi: {
getProfile: vi.fn(),
setProfileAttribute: vi.fn(),
},
}));
const mockedUserApi = vi.mocked(userApi);
describe('apiKeysApi', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal('crypto', {
getRandomValues: (buffer: Uint8Array) => {
buffer.fill(1);
return buffer;
},
randomUUID: () => 'uuid-123',
subtle: {
digest: vi.fn(async () => new Uint8Array([0xde, 0xad, 0xbe, 0xef]).buffer),
},
});
});
it('lists active API keys and filters revoked records', async () => {
mockedUserApi.getProfile.mockResolvedValue({
success: true,
data: {
attributes: [
{
key: 'api_key_active',
value: JSON.stringify({
id: 'active',
name: 'Active Key',
keyPrefix: 'gg_active',
keySuffix: '1234',
keyHash: 'hash',
createdAt: '2026-01-01T00:00:00.000Z',
lastUsedAt: null,
expiresAt: null,
revokedAt: null,
}),
valueType: 'Json',
},
{
key: 'api_key_revoked',
value: JSON.stringify({
id: 'revoked',
name: 'Revoked Key',
keyPrefix: 'gg_revoked',
keySuffix: '5678',
keyHash: 'hash',
createdAt: '2026-01-02T00:00:00.000Z',
lastUsedAt: null,
expiresAt: null,
revokedAt: '2026-01-03T00:00:00.000Z',
}),
valueType: 'Json',
},
],
},
timestamp: new Date().toISOString(),
} as never);
const response = await apiKeysApi.list('user-1');
expect(response.success).toBe(true);
expect(response.data).toHaveLength(1);
expect(response.data?.[0].id).toBe('active');
});
it('creates API key and persists description and key record', async () => {
mockedUserApi.setProfileAttribute.mockResolvedValue({
success: true,
data: { key: 'any', value: 'any', valueType: 'String' },
timestamp: new Date().toISOString(),
} as never);
const response = await apiKeysApi.create('user-1', {
name: 'Primary key',
description: 'Main key for integrations',
});
expect(response.success).toBe(true);
expect(response.data?.key.startsWith('gg_')).toBe(true);
expect(response.data?.apiKey.id).toBe('uuid-123');
expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledTimes(2);
expect(mockedUserApi.setProfileAttribute).toHaveBeenNthCalledWith(
1,
'user-1',
'api_key_desc_uuid-123',
expect.objectContaining({ value: 'Main key for integrations' })
);
expect(mockedUserApi.setProfileAttribute).toHaveBeenNthCalledWith(
2,
'user-1',
'api_key_uuid-123',
expect.objectContaining({ valueType: 'Json' })
);
});
it('revokes an existing API key record', async () => {
mockedUserApi.getProfile.mockResolvedValue({
success: true,
data: {
attributes: [
{
key: 'api_key_target',
value: JSON.stringify({
id: 'target',
name: 'Target Key',
keyPrefix: 'gg_target',
keySuffix: '9999',
keyHash: 'hash',
createdAt: '2026-01-01T00:00:00.000Z',
lastUsedAt: null,
expiresAt: null,
revokedAt: null,
}),
valueType: 'Json',
},
],
},
timestamp: new Date().toISOString(),
} as never);
mockedUserApi.setProfileAttribute.mockResolvedValue({
success: true,
data: { key: 'api_key_target', value: 'revoked', valueType: 'Json' },
timestamp: new Date().toISOString(),
} as never);
const response = await apiKeysApi.delete('user-1', 'target');
expect(response.success).toBe(true);
expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledTimes(1);
expect(mockedUserApi.setProfileAttribute).toHaveBeenCalledWith(
'user-1',
'api_key_target',
expect.objectContaining({ valueType: 'Json' })
);
});
});

View File

@@ -0,0 +1,88 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { storageApi } from '../storage.api';
import { apiClient } from '../client';
vi.mock('../client', () => ({
apiClient: {
post: vi.fn(),
get: vi.fn(),
},
}));
const mockedApiClient = vi.mocked(apiClient);
describe('storageApi.uploadAvatar', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('uploads avatar and resolves CDN URL', async () => {
mockedApiClient.post.mockResolvedValue({
success: true,
data: {
success: true,
fileId: 'file-1',
objectKey: 'avatars/file-1.png',
},
timestamp: new Date().toISOString(),
} as never);
mockedApiClient.get.mockResolvedValue({
success: true,
data: {
url: 'https://cdn.goodgo.dev/avatars/file-1.png',
isCDN: true,
description: 'cdn',
},
timestamp: new Date().toISOString(),
} as never);
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
const response = await storageApi.uploadAvatar(file);
expect(response.success).toBe(true);
expect(response.data?.fileId).toBe('file-1');
expect(response.data?.url).toBe('https://cdn.goodgo.dev/avatars/file-1.png');
});
it('falls back to empty URL when CDN lookup fails', async () => {
mockedApiClient.post.mockResolvedValue({
success: true,
data: {
success: true,
fileId: 'file-2',
objectKey: 'avatars/file-2.png',
},
timestamp: new Date().toISOString(),
} as never);
mockedApiClient.get.mockRejectedValue(new Error('cdn lookup failed'));
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
const response = await storageApi.uploadAvatar(file);
expect(response.success).toBe(true);
expect(response.data?.fileId).toBe('file-2');
expect(response.data?.url).toBe('');
});
it('throws when upload API returns unsuccessful payload', async () => {
mockedApiClient.post.mockResolvedValue({
success: false,
data: {
success: false,
error: 'Upload failed',
},
error: {
message: 'Upload failed',
code: 'UPLOAD_FAILED',
},
timestamp: new Date().toISOString(),
} as never);
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
await expect(storageApi.uploadAvatar(file)).rejects.toThrow('Upload failed');
});
});

View File

@@ -12,7 +12,10 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
include: ['src/stores/__tests__/**/*.test.ts'],
include: [
'src/stores/__tests__/**/*.test.ts',
'src/services/api/__tests__/**/*.test.ts',
],
exclude: ['e2e/**', 'src/**/__tests__/**/*.integration.test.tsx'],
coverage: {
provider: 'v8',