feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
769
frontend/src/hooks/__tests__/useApiTokens.test.ts
Normal file
769
frontend/src/hooks/__tests__/useApiTokens.test.ts
Normal file
@@ -0,0 +1,769 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('../../api/client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
useApiTokens,
|
||||
useCreateApiToken,
|
||||
useRevokeApiToken,
|
||||
useUpdateApiToken,
|
||||
useTestTokensForDocs,
|
||||
API_SCOPES,
|
||||
SCOPE_PRESETS,
|
||||
} from '../useApiTokens';
|
||||
import type {
|
||||
APIToken,
|
||||
APITokenCreateResponse,
|
||||
CreateTokenData,
|
||||
TestTokenForDocs,
|
||||
APIScope,
|
||||
} from '../useApiTokens';
|
||||
import apiClient from '../../api/client';
|
||||
|
||||
// Create a wrapper with QueryClientProvider
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return React.createElement(
|
||||
QueryClientProvider,
|
||||
{ client: queryClient },
|
||||
children
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// Mock data
|
||||
const mockApiToken: APIToken = {
|
||||
id: 'token-123',
|
||||
name: 'Test Token',
|
||||
key_prefix: 'ss_test',
|
||||
scopes: ['services:read', 'bookings:write'],
|
||||
is_active: true,
|
||||
is_sandbox: false,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
last_used_at: '2024-01-15T12:30:00Z',
|
||||
expires_at: null,
|
||||
created_by: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User',
|
||||
},
|
||||
};
|
||||
|
||||
const mockApiTokenCreateResponse: APITokenCreateResponse = {
|
||||
...mockApiToken,
|
||||
key: 'ss_test_1234567890abcdef',
|
||||
};
|
||||
|
||||
const mockTestToken: TestTokenForDocs = {
|
||||
id: 'test-token-123',
|
||||
name: 'Test Token for Docs',
|
||||
key_prefix: 'ss_test',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
describe('useApiTokens hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useApiTokens', () => {
|
||||
it('fetches API tokens successfully', async () => {
|
||||
const mockTokens = [mockApiToken];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockTokens });
|
||||
|
||||
const { result } = renderHook(() => useApiTokens(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockTokens);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/v1/tokens/');
|
||||
});
|
||||
|
||||
it('handles empty token list', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
||||
|
||||
const { result } = renderHook(() => useApiTokens(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles fetch error', async () => {
|
||||
const mockError = new Error('Failed to fetch tokens');
|
||||
vi.mocked(apiClient.get).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useApiTokens(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(mockError);
|
||||
});
|
||||
|
||||
it('returns multiple tokens correctly', async () => {
|
||||
const mockTokens = [
|
||||
mockApiToken,
|
||||
{
|
||||
...mockApiToken,
|
||||
id: 'token-456',
|
||||
name: 'Production Token',
|
||||
is_sandbox: false,
|
||||
},
|
||||
{
|
||||
...mockApiToken,
|
||||
id: 'token-789',
|
||||
name: 'Sandbox Token',
|
||||
is_sandbox: true,
|
||||
},
|
||||
];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockTokens });
|
||||
|
||||
const { result } = renderHook(() => useApiTokens(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toHaveLength(3);
|
||||
expect(result.current.data).toEqual(mockTokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateApiToken', () => {
|
||||
it('creates API token successfully', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockApiTokenCreateResponse });
|
||||
|
||||
const { result } = renderHook(() => useCreateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const createData: CreateTokenData = {
|
||||
name: 'Test Token',
|
||||
scopes: ['services:read', 'bookings:write'],
|
||||
};
|
||||
|
||||
let response: APITokenCreateResponse | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync(createData);
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
|
||||
expect(response).toEqual(mockApiTokenCreateResponse);
|
||||
expect(response?.key).toBe('ss_test_1234567890abcdef');
|
||||
});
|
||||
|
||||
it('creates token with expiration date', async () => {
|
||||
const expiresAt = '2024-12-31T23:59:59Z';
|
||||
const tokenWithExpiry = {
|
||||
...mockApiTokenCreateResponse,
|
||||
expires_at: expiresAt,
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: tokenWithExpiry });
|
||||
|
||||
const { result } = renderHook(() => useCreateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const createData: CreateTokenData = {
|
||||
name: 'Expiring Token',
|
||||
scopes: ['services:read'],
|
||||
expires_at: expiresAt,
|
||||
};
|
||||
|
||||
let response: APITokenCreateResponse | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync(createData);
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
|
||||
expect(response?.expires_at).toBe(expiresAt);
|
||||
});
|
||||
|
||||
it('creates sandbox token', async () => {
|
||||
const sandboxToken = {
|
||||
...mockApiTokenCreateResponse,
|
||||
is_sandbox: true,
|
||||
key_prefix: 'ss_test',
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: sandboxToken });
|
||||
|
||||
const { result } = renderHook(() => useCreateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const createData: CreateTokenData = {
|
||||
name: 'Sandbox Token',
|
||||
scopes: ['services:read'],
|
||||
is_sandbox: true,
|
||||
};
|
||||
|
||||
let response: APITokenCreateResponse | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync(createData);
|
||||
});
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/v1/tokens/', createData);
|
||||
expect(response?.is_sandbox).toBe(true);
|
||||
});
|
||||
|
||||
it('invalidates token list after successful creation', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockApiTokenCreateResponse });
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
|
||||
const { result: createResult } = renderHook(() => useCreateApiToken(), { wrapper });
|
||||
|
||||
// Wait for initial fetch
|
||||
await waitFor(() => {
|
||||
expect(tokenListResult.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;
|
||||
|
||||
// Create new token
|
||||
await act(async () => {
|
||||
await createResult.current.mutateAsync({
|
||||
name: 'New Token',
|
||||
scopes: ['services:read'],
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for refetch
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creation error', async () => {
|
||||
const mockError = new Error('Failed to create token');
|
||||
vi.mocked(apiClient.post).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useCreateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let caughtError;
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
name: 'Test Token',
|
||||
scopes: ['services:read'],
|
||||
});
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
});
|
||||
|
||||
expect(caughtError).toEqual(mockError);
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('creates token with all available scopes', async () => {
|
||||
const allScopesToken = {
|
||||
...mockApiTokenCreateResponse,
|
||||
scopes: API_SCOPES.map(s => s.value),
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: allScopesToken });
|
||||
|
||||
const { result } = renderHook(() => useCreateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
const createData: CreateTokenData = {
|
||||
name: 'Full Access Token',
|
||||
scopes: API_SCOPES.map(s => s.value),
|
||||
};
|
||||
|
||||
let response: APITokenCreateResponse | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync(createData);
|
||||
});
|
||||
|
||||
expect(response?.scopes).toHaveLength(API_SCOPES.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRevokeApiToken', () => {
|
||||
it('revokes API token successfully', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({});
|
||||
|
||||
const { result } = renderHook(() => useRevokeApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync('token-123');
|
||||
});
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/v1/tokens/token-123/');
|
||||
});
|
||||
|
||||
it('invalidates token list after successful revocation', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({});
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
|
||||
const { result: revokeResult } = renderHook(() => useRevokeApiToken(), { wrapper });
|
||||
|
||||
// Wait for initial fetch
|
||||
await waitFor(() => {
|
||||
expect(tokenListResult.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;
|
||||
|
||||
// Revoke token
|
||||
await act(async () => {
|
||||
await revokeResult.current.mutateAsync('token-123');
|
||||
});
|
||||
|
||||
// Wait for refetch
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles revocation error', async () => {
|
||||
const mockError = new Error('Failed to revoke token');
|
||||
vi.mocked(apiClient.delete).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useRevokeApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let caughtError;
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync('token-123');
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
});
|
||||
|
||||
expect(caughtError).toEqual(mockError);
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateApiToken', () => {
|
||||
it('updates API token successfully', async () => {
|
||||
const updatedToken = {
|
||||
...mockApiToken,
|
||||
name: 'Updated Token Name',
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let response: APIToken | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { name: 'Updated Token Name' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
|
||||
name: 'Updated Token Name',
|
||||
});
|
||||
expect(response?.name).toBe('Updated Token Name');
|
||||
});
|
||||
|
||||
it('updates token scopes', async () => {
|
||||
const updatedToken = {
|
||||
...mockApiToken,
|
||||
scopes: ['services:read', 'bookings:read', 'customers:read'],
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { scopes: ['services:read', 'bookings:read', 'customers:read'] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
|
||||
scopes: ['services:read', 'bookings:read', 'customers:read'],
|
||||
});
|
||||
});
|
||||
|
||||
it('deactivates token', async () => {
|
||||
const deactivatedToken = {
|
||||
...mockApiToken,
|
||||
is_active: false,
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: deactivatedToken });
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let response: APIToken | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { is_active: false },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
|
||||
is_active: false,
|
||||
});
|
||||
expect(response?.is_active).toBe(false);
|
||||
});
|
||||
|
||||
it('updates token expiration', async () => {
|
||||
const newExpiry = '2025-12-31T23:59:59Z';
|
||||
const updatedToken = {
|
||||
...mockApiToken,
|
||||
expires_at: newExpiry,
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { expires_at: newExpiry },
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
|
||||
expires_at: newExpiry,
|
||||
});
|
||||
});
|
||||
|
||||
it('invalidates token list after successful update', async () => {
|
||||
const updatedToken = { ...mockApiToken, name: 'Updated' };
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [mockApiToken] });
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result: tokenListResult } = renderHook(() => useApiTokens(), { wrapper });
|
||||
const { result: updateResult } = renderHook(() => useUpdateApiToken(), { wrapper });
|
||||
|
||||
// Wait for initial fetch
|
||||
await waitFor(() => {
|
||||
expect(tokenListResult.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
const initialCallCount = vi.mocked(apiClient.get).mock.calls.length;
|
||||
|
||||
// Update token
|
||||
await act(async () => {
|
||||
await updateResult.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { name: 'Updated' },
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for refetch
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(apiClient.get).mock.calls.length).toBeGreaterThan(initialCallCount);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles update error', async () => {
|
||||
const mockError = new Error('Failed to update token');
|
||||
vi.mocked(apiClient.patch).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let caughtError;
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: { name: 'Updated' },
|
||||
});
|
||||
} catch (error) {
|
||||
caughtError = error;
|
||||
}
|
||||
});
|
||||
|
||||
expect(caughtError).toEqual(mockError);
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates multiple fields at once', async () => {
|
||||
const updatedToken = {
|
||||
...mockApiToken,
|
||||
name: 'Updated Token',
|
||||
scopes: ['services:read', 'bookings:read'],
|
||||
expires_at: '2025-12-31T23:59:59Z',
|
||||
};
|
||||
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: updatedToken });
|
||||
|
||||
const { result } = renderHook(() => useUpdateApiToken(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
tokenId: 'token-123',
|
||||
data: {
|
||||
name: 'Updated Token',
|
||||
scopes: ['services:read', 'bookings:read'],
|
||||
expires_at: '2025-12-31T23:59:59Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/v1/tokens/token-123/', {
|
||||
name: 'Updated Token',
|
||||
scopes: ['services:read', 'bookings:read'],
|
||||
expires_at: '2025-12-31T23:59:59Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useTestTokensForDocs', () => {
|
||||
it('fetches test tokens successfully', async () => {
|
||||
const mockTestTokens = [mockTestToken];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockTestTokens });
|
||||
|
||||
const { result } = renderHook(() => useTestTokensForDocs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockTestTokens);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/v1/tokens/test-tokens/');
|
||||
});
|
||||
|
||||
it('handles empty test token list', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
||||
|
||||
const { result } = renderHook(() => useTestTokensForDocs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles fetch error', async () => {
|
||||
const mockError = new Error('Failed to fetch test tokens');
|
||||
vi.mocked(apiClient.get).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useTestTokensForDocs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(mockError);
|
||||
});
|
||||
|
||||
it('returns multiple test tokens', async () => {
|
||||
const mockTestTokens = [
|
||||
mockTestToken,
|
||||
{
|
||||
...mockTestToken,
|
||||
id: 'test-token-456',
|
||||
name: 'Another Test Token',
|
||||
},
|
||||
];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockTestTokens });
|
||||
|
||||
const { result } = renderHook(() => useTestTokensForDocs(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('uses staleTime for caching', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: [mockTestToken] });
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result: result1 } = renderHook(() => useTestTokensForDocs(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result1.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
// Render hook again - should use cached data
|
||||
const { result: result2 } = renderHook(() => useTestTokensForDocs(), { wrapper });
|
||||
|
||||
expect(result2.current.data).toEqual([mockTestToken]);
|
||||
// Should only call API once due to staleTime cache
|
||||
expect(vi.mocked(apiClient.get).mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API_SCOPES constant', () => {
|
||||
it('contains expected scopes', () => {
|
||||
expect(API_SCOPES).toBeDefined();
|
||||
expect(Array.isArray(API_SCOPES)).toBe(true);
|
||||
expect(API_SCOPES.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('has correct structure for each scope', () => {
|
||||
API_SCOPES.forEach((scope: APIScope) => {
|
||||
expect(scope).toHaveProperty('value');
|
||||
expect(scope).toHaveProperty('label');
|
||||
expect(scope).toHaveProperty('description');
|
||||
expect(typeof scope.value).toBe('string');
|
||||
expect(typeof scope.label).toBe('string');
|
||||
expect(typeof scope.description).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('contains essential scopes', () => {
|
||||
const scopeValues = API_SCOPES.map(s => s.value);
|
||||
expect(scopeValues).toContain('services:read');
|
||||
expect(scopeValues).toContain('bookings:read');
|
||||
expect(scopeValues).toContain('bookings:write');
|
||||
expect(scopeValues).toContain('customers:read');
|
||||
expect(scopeValues).toContain('customers:write');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SCOPE_PRESETS constant', () => {
|
||||
it('contains expected presets', () => {
|
||||
expect(SCOPE_PRESETS).toBeDefined();
|
||||
expect(SCOPE_PRESETS).toHaveProperty('booking_widget');
|
||||
expect(SCOPE_PRESETS).toHaveProperty('read_only');
|
||||
expect(SCOPE_PRESETS).toHaveProperty('full_access');
|
||||
});
|
||||
|
||||
it('booking_widget preset has correct structure', () => {
|
||||
const preset = SCOPE_PRESETS.booking_widget;
|
||||
expect(preset).toHaveProperty('label');
|
||||
expect(preset).toHaveProperty('description');
|
||||
expect(preset).toHaveProperty('scopes');
|
||||
expect(Array.isArray(preset.scopes)).toBe(true);
|
||||
expect(preset.scopes).toContain('services:read');
|
||||
expect(preset.scopes).toContain('bookings:write');
|
||||
});
|
||||
|
||||
it('read_only preset contains only read scopes', () => {
|
||||
const preset = SCOPE_PRESETS.read_only;
|
||||
expect(preset.scopes.every(scope => scope.includes(':read'))).toBe(true);
|
||||
});
|
||||
|
||||
it('full_access preset contains all scopes', () => {
|
||||
const preset = SCOPE_PRESETS.full_access;
|
||||
expect(preset.scopes).toHaveLength(API_SCOPES.length);
|
||||
expect(preset.scopes).toEqual(API_SCOPES.map(s => s.value));
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeScript types', () => {
|
||||
it('APIToken type includes all required fields', () => {
|
||||
const token: APIToken = mockApiToken;
|
||||
expect(token.id).toBeDefined();
|
||||
expect(token.name).toBeDefined();
|
||||
expect(token.key_prefix).toBeDefined();
|
||||
expect(token.scopes).toBeDefined();
|
||||
expect(token.is_active).toBeDefined();
|
||||
expect(token.is_sandbox).toBeDefined();
|
||||
expect(token.created_at).toBeDefined();
|
||||
});
|
||||
|
||||
it('APITokenCreateResponse extends APIToken with key', () => {
|
||||
const createResponse: APITokenCreateResponse = mockApiTokenCreateResponse;
|
||||
expect(createResponse.key).toBeDefined();
|
||||
expect(createResponse.id).toBeDefined();
|
||||
expect(createResponse.name).toBeDefined();
|
||||
});
|
||||
|
||||
it('CreateTokenData has correct structure', () => {
|
||||
const createData: CreateTokenData = {
|
||||
name: 'Test',
|
||||
scopes: ['services:read'],
|
||||
};
|
||||
expect(createData.name).toBe('Test');
|
||||
expect(createData.scopes).toEqual(['services:read']);
|
||||
});
|
||||
|
||||
it('TestTokenForDocs has minimal fields', () => {
|
||||
const testToken: TestTokenForDocs = mockTestToken;
|
||||
expect(testToken.id).toBeDefined();
|
||||
expect(testToken.name).toBeDefined();
|
||||
expect(testToken.key_prefix).toBeDefined();
|
||||
expect(testToken.created_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user