import {QueryClient, QueryClientProvider} from '@tanstack/react-query' import {render, screen, within} from '@testing-library/react' import userEvent from '@testing-library/user-event' import {beforeEach, expect, test, vi} from 'vitest' import {HttpApiPanel} from './HttpApiPanel' import type {HttpApiOverview} from '@/api/cloudProxyClient' import type {CloudResource} from '@/types/resource' const api = vi.hoisted(() => ({ describeHttpApi: vi.fn(), listCloudResources: vi.fn(), createHttpApiRoute: vi.fn(), deleteHttpApiRoute: vi.fn(), setHttpApiRouteAuthorizer: vi.fn(), createHttpApiStage: vi.fn(), deleteHttpApiStage: vi.fn(), deployHttpApi: vi.fn(), createHttpApiAuthorizer: vi.fn(), deleteHttpApiAuthorizer: vi.fn(), updateHttpApiCors: vi.fn(), testApiGatewayRoute: vi.fn(), testWebSocketApi: vi.fn(), })) vi.mock('@/api/cloudProxyClient', () => api) const httpResource: CloudResource = { id: 'h1', name: 'notes-api', cloud: 'aws', service: 'apigateway', type: 'http-api', region: null, createdAt: null, metadata: {apigatewayService: 'http', protocol: 'HTTP'}, } const httpOverview: HttpApiOverview = { apiId: 'h1', protocol: 'HTTP', apiEndpoint: null, routeSelectionExpression: null, routes: [ {routeId: 'r1', routeKey: 'GET /notes/{id}', integration: {integrationId: 'i1', type: 'AWS_PROXY', uri: 'arn', functionName: 'notes', payloadFormatVersion: '2.0'}, authorizationType: 'NONE', authorizerId: null, authorizationScopes: [], returnsResponse: false}, {routeId: 'r2', routeKey: 'GET /me', integration: {integrationId: 'i1', type: 'AWS_PROXY', uri: 'arn', functionName: 'notes', payloadFormatVersion: '2.0'}, authorizationType: 'JWT', authorizerId: 'a1', authorizationScopes: [], returnsResponse: false}, ], stages: [{stageName: '$default', autoDeploy: true, deploymentId: null, lastUpdatedAt: null, invokePath: '/execute-api/h1/$default'}], authorizers: [{authorizerId: 'a1', name: 'cognito', type: 'JWT', identitySource: ['$request.header.Authorization'], issuer: 'http://floci:4566/us-east-1_pool', audience: ['client1']}], cors: null, } function renderPanel(resource: CloudResource = httpResource) { const client = new QueryClient({defaultOptions: {queries: {retry: false}}}) return render() } beforeEach(() => { vi.clearAllMocks() api.describeHttpApi.mockResolvedValue(httpOverview) api.listCloudResources.mockImplementation(async (_cloud: string, service: string) => service === 'serverless' ? [{id: 'notes', name: 'notes', type: 'lambda', cloud: 'aws', service: 'serverless', region: null, createdAt: null, metadata: {}}] : [{id: 'us-east-1_pool', name: 'notes-users', type: 'user-pool', cloud: 'aws', service: 'userpools', region: null, createdAt: null, metadata: {appClients: [{clientId: 'client1', clientName: 'web'}]}}]) api.createHttpApiRoute.mockResolvedValue(httpOverview.routes[0]) api.setHttpApiRouteAuthorizer.mockResolvedValue(httpOverview.routes[0]) api.createHttpApiAuthorizer.mockResolvedValue(httpOverview.authorizers[0]) api.updateHttpApiCors.mockResolvedValue({allowOrigins: ['*'], allowMethods: ['GET'], allowHeaders: [], exposeHeaders: [], maxAgeSeconds: null, allowCredentials: false}) api.testApiGatewayRoute.mockResolvedValue({statusCode: 200, headers: {}, body: '{"ok":true}', durationMs: 12}) }) test('shows the stage URL, routes with their targets and who may call them', async () => { renderPanel() expect(await screen.findByText('http://localhost:4566/execute-api/h1/$default')).toBeInTheDocument() const routes = within(screen.getByRole('region', {name: 'Routes'})) expect(routes.getByText('GET /notes/{id}')).toBeInTheDocument() expect(routes.getAllByText('Lambda: notes')).toHaveLength(2) expect(routes.getByRole('combobox', {name: 'Access for GET /me'})).toHaveValue('a1') expect(routes.getByRole('combobox', {name: 'Access for GET /notes/{id}'})).toHaveValue('') }) test('adds a Lambda route and protects an existing one', async () => { renderPanel() const routes = within(await screen.findByRole('region', {name: 'Routes'})) await userEvent.selectOptions(routes.getByRole('combobox', {name: 'Method'}), 'POST') await userEvent.clear(routes.getByRole('textbox', {name: 'Path'})) await userEvent.type(routes.getByRole('textbox', {name: 'Path'}), '/notes') await routes.findByRole('option', {name: 'notes'}) await userEvent.selectOptions(routes.getByRole('combobox', {name: 'Lambda function'}), 'notes') await userEvent.click(routes.getByRole('button', {name: 'Add route'})) expect(api.createHttpApiRoute).toHaveBeenCalledWith('aws', 'h1', {routeKey: 'POST /notes', functionName: 'notes', httpUrl: undefined, authorizerId: undefined, returnsResponse: undefined}) await userEvent.selectOptions(routes.getByRole('combobox', {name: 'Access for GET /notes/{id}'}), 'a1') expect(api.setHttpApiRouteAuthorizer).toHaveBeenCalledWith('aws', 'h1', 'r1', 'a1') }) test('creates a JWT authorizer from a Cognito pool and app client', async () => { renderPanel() const security = within(await screen.findByRole('region', {name: 'Security'})) await security.findByRole('option', {name: 'notes-users'}) await userEvent.selectOptions(security.getByRole('combobox', {name: 'User pool'}), 'us-east-1_pool') await userEvent.selectOptions(security.getByRole('combobox', {name: 'App client'}), 'client1') await userEvent.click(security.getByRole('button', {name: 'Add authorizer'})) expect(api.createHttpApiAuthorizer).toHaveBeenCalledWith('aws', 'h1', {name: 'cognito', userPoolId: 'us-east-1_pool', userPoolClientId: 'client1'}) }) test('sends a request to the stage and shows the response', async () => { renderPanel() const tester = within(await screen.findByRole('region', {name: 'Send a request'})) expect(tester.getByRole('textbox', {name: 'Request path'})).toHaveValue('/notes/1') await userEvent.click(tester.getByRole('button', {name: 'Send request'})) expect(api.testApiGatewayRoute).toHaveBeenCalledWith('aws', 'h1', expect.objectContaining({stage: '$default', method: 'GET', path: '/notes/1'})) expect(await tester.findByText('200')).toBeInTheDocument() }) test('a WebSocket API shows its wss URL and sends a message, without HTTP-only security', async () => { api.describeHttpApi.mockResolvedValue({ apiId: 'w1', protocol: 'WEBSOCKET', apiEndpoint: null, routeSelectionExpression: '$request.body.action', routes: [{routeId: 'r1', routeKey: 'sendMessage', integration: null, authorizationType: 'NONE', authorizerId: null, authorizationScopes: [], returnsResponse: true}], stages: [{stageName: 'dev', autoDeploy: true, deploymentId: null, lastUpdatedAt: null, invokePath: '/ws/w1/dev'}], authorizers: [], cors: null, } satisfies HttpApiOverview) api.testWebSocketApi.mockResolvedValue({connected: true, messages: ['{"echo":"hello"}'], closeCode: null, error: null, durationMs: 40}) renderPanel({...httpResource, id: 'w1', type: 'websocket-api', metadata: {apigatewayService: 'websocket', protocol: 'WEBSOCKET'}}) expect(await screen.findByText('ws://localhost:4566/ws/w1/dev')).toBeInTheDocument() expect(screen.queryByRole('region', {name: 'Security'})).not.toBeInTheDocument() const tester = within(screen.getByRole('region', {name: 'Send a message'})) expect(tester.getByRole('textbox', {name: 'Message'})).toHaveValue(JSON.stringify({action: 'sendMessage', data: 'hello'}, null, 2)) await userEvent.click(tester.getByRole('button', {name: 'Connect and send'})) expect(api.testWebSocketApi).toHaveBeenCalledWith('aws', 'w1', expect.objectContaining({stage: 'dev'})) expect(await tester.findByText('Connected')).toBeInTheDocument() })