- 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>
770 lines
22 KiB
TypeScript
770 lines
22 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|