/** * Integration test: API Key management flow * * Tests the cross-component interaction: * ApiServer → SecretKeyButton → SecretKeyModal * * Renders real ApiServer, SecretKeyButton, and SecretKeyModal together * with only service-layer mocks. Deep modal interactions (create/delete) * are covered by unit tests in secret-key-modal.spec.tsx. */ import { act, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import ApiServer from '@/app/components/develop/ApiServer' // ---------- fake timers (HeadlessUI Dialog transitions) ---------- beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }) }) afterEach(() => { vi.runOnlyPendingTimers() vi.useRealTimers() }) async function flushUI() { await act(async () => { vi.runAllTimers() }) } // ---------- mocks ---------- vi.mock('@/context/app-context', () => ({ useAppContext: () => ({ currentWorkspace: { id: 'ws-1', name: 'Workspace' }, isCurrentWorkspaceManager: true, isCurrentWorkspaceEditor: true, }), })) vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: vi.fn((val: number) => `Time:${val}`), formatDate: vi.fn((val: string) => `Date:${val}`), }), })) vi.mock('@/service/apps', () => ({ createApikey: vi.fn().mockResolvedValue({ token: 'sk-new-token-1234567890abcdef' }), delApikey: vi.fn().mockResolvedValue({}), })) vi.mock('@/service/datasets', () => ({ createApikey: vi.fn().mockResolvedValue({ token: 'dk-new' }), delApikey: vi.fn().mockResolvedValue({}), })) const mockApiKeys = vi.fn().mockReturnValue({ data: [] }) const mockIsLoading = vi.fn().mockReturnValue(false) vi.mock('@/service/use-apps', () => ({ useAppApiKeys: () => ({ data: mockApiKeys(), isLoading: mockIsLoading(), }), useInvalidateAppApiKeys: () => vi.fn(), })) vi.mock('@/service/knowledge/use-dataset', () => ({ useDatasetApiKeys: () => ({ data: null, isLoading: false }), useInvalidateDatasetApiKeys: () => vi.fn(), })) // ---------- tests ---------- describe('API Key management flow', () => { beforeEach(() => { vi.clearAllMocks() mockApiKeys.mockReturnValue({ data: [] }) mockIsLoading.mockReturnValue(false) }) it('ApiServer renders URL, status badge, and API Key button', () => { render() expect(screen.getByText('https://api.dify.ai/v1')).toBeInTheDocument() expect(screen.getByText('appApi.ok')).toBeInTheDocument() expect(screen.getByText('appApi.apiKey')).toBeInTheDocument() }) it('clicking API Key button opens SecretKeyModal with real modal content', async () => { const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) render() // Click API Key button (rendered by SecretKeyButton) await act(async () => { await user.click(screen.getByText('appApi.apiKey')) }) await flushUI() // SecretKeyModal should render with real HeadlessUI Dialog await waitFor(() => { expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument() expect(screen.getByText('appApi.apiKeyModal.apiSecretKeyTips')).toBeInTheDocument() expect(screen.getByText('appApi.apiKeyModal.createNewSecretKey')).toBeInTheDocument() }) }) it('modal shows loading state when API keys are being fetched', async () => { const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) mockIsLoading.mockReturnValue(true) render() await act(async () => { await user.click(screen.getByText('appApi.apiKey')) }) await flushUI() await waitFor(() => { expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument() }) // Loading indicator should be present expect(document.body.querySelector('[role="status"]')).toBeInTheDocument() }) it('modal can be closed by clicking X icon', async () => { const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) render() // Open modal await act(async () => { await user.click(screen.getByText('appApi.apiKey')) }) await flushUI() await waitFor(() => { expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument() }) // Click X icon to close const closeIcon = document.body.querySelector('svg.cursor-pointer') expect(closeIcon).toBeInTheDocument() await act(async () => { await user.click(closeIcon!) }) await flushUI() // Modal should close await waitFor(() => { expect(screen.queryByText('appApi.apiKeyModal.apiSecretKeyTips')).not.toBeInTheDocument() }) }) it('renders correctly with different API URLs', async () => { const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }) const { rerender } = render( , ) expect(screen.getByText('http://localhost:5001/v1')).toBeInTheDocument() // Open modal and verify it works with the same appId await act(async () => { await user.click(screen.getByText('appApi.apiKey')) }) await flushUI() await waitFor(() => { expect(screen.getByText('appApi.apiKeyModal.apiSecretKey')).toBeInTheDocument() }) // Close modal, update URL and re-verify const xIcon = document.body.querySelector('svg.cursor-pointer') await act(async () => { await user.click(xIcon!) }) await flushUI() rerender( , ) expect(screen.getByText('https://api.production.com/v1')).toBeInTheDocument() }) })