test: add web-client API service unit coverage
Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
149
apps/web-client/src/services/api/__tests__/api-keys.api.test.ts
Normal file
149
apps/web-client/src/services/api/__tests__/api-keys.api.test.ts
Normal 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' })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user