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 {GraphqlPanel} from './GraphqlPanel' import type {CloudResource} from '@/types/resource' const api = vi.hoisted(() => ({ getCloudResource: vi.fn(), listCloudResources: vi.fn(), updateGraphqlSchema: vi.fn(), createGraphqlApiKey: vi.fn(), deleteGraphqlApiKey: vi.fn(), createGraphqlDataSource: vi.fn(), deleteGraphqlDataSource: vi.fn(), createGraphqlResolver: vi.fn(), deleteGraphqlResolver: vi.fn(), })) vi.mock('@/api/cloudProxyClient', () => api) vi.mock('@/lib/accountStore', () => ({useAccountId: () => '000000000000'})) const sdl = 'type Note { id: ID! text: String }\ntype Query { note(id: ID!): Note notes: [Note] }' function graphqlApi(metadata: Record = {}): CloudResource { return { id: 'a1', name: 'notes-api', cloud: 'aws', service: 'graphql', type: 'graphqlApi', region: 'us-east-1', createdAt: null, metadata: { apiId: 'a1', authenticationType: 'API_KEY', authenticationLabel: 'API key', graphqlUrl: 'http://localhost:4566/v1/apis/a1/graphql', realtimeUrl: 'ws://localhost:4566/v1/apis/a1/graphql/realtime', definition: sdl, schemaStatus: {status: 'SUCCESS', details: null}, types: [{name: 'Query', fields: ['note', 'notes']}, {name: 'Note', fields: ['id', 'text']}], apiKeys: [{id: 'da2-key', description: 'web', expiresAt: '2026-11-01T22:00:00.000Z'}], dataSources: [{name: 'notesTable', type: 'AMAZON_DYNAMODB', description: null, tableName: 'notes', functionArn: null, serviceRoleArn: 'arn:aws:iam::000000000000:role/appsync'}], resolvers: [{typeName: 'Query', fieldName: 'note', dataSourceName: 'notesTable', kind: 'UNIT', runtime: 'APPSYNC_JS', code: ''}], ...metadata, }, } } function renderPanel(resource: CloudResource) { api.getCloudResource.mockResolvedValue(resource) const client = new QueryClient({defaultOptions: {queries: {retry: false}}}) return render() } beforeEach(() => { vi.clearAllMocks() api.listCloudResources.mockResolvedValue([{id: 'notes', name: 'notes', metadata: {}}]) for (const fn of [api.updateGraphqlSchema, api.createGraphqlApiKey, api.deleteGraphqlApiKey, api.createGraphqlDataSource, api.deleteGraphqlDataSource, api.createGraphqlResolver, api.deleteGraphqlResolver]) { fn.mockResolvedValue(graphqlApi()) } }) test('shows the endpoint, saved schema, keys, data sources and resolvers, and says resolvers do not run', async () => { renderPanel(graphqlApi()) expect(await screen.findByText('http://localhost:4566/v1/apis/a1/graphql')).toBeInTheDocument() expect(screen.getByLabelText('Schema definition')).toHaveValue(sdl) expect(screen.getByText('da2-key')).toBeInTheDocument() expect(screen.getByText('Query.note')).toBeInTheDocument() expect(screen.getByText(/does not run resolvers yet/)).toBeInTheDocument() }) test('offers a starter schema for a new API and saves an edited one', async () => { const user = userEvent.setup() renderPanel(graphqlApi({definition: null, types: [], resolvers: [], schemaStatus: null})) const schema = await screen.findByLabelText('Schema definition') expect(await screen.findByText(/This starter schema is not saved/)).toBeInTheDocument() await user.clear(schema) await user.type(schema, 'type Query {{ hello: String }') await user.click(screen.getByRole('button', {name: 'Save schema'})) expect(api.updateGraphqlSchema).toHaveBeenCalledWith('aws', 'a1', 'type Query { hello: String }') expect(await screen.findByText('Schema saved.')).toBeInTheDocument() }) test('saves the starter schema as shown when it is not edited', async () => { const user = userEvent.setup() renderPanel(graphqlApi({definition: null, types: [], resolvers: [], schemaStatus: null})) await screen.findByText(/This starter schema is not saved/) await user.click(screen.getByRole('button', {name: 'Save schema'})) expect(api.updateGraphqlSchema).toHaveBeenCalledWith('aws', 'a1', expect.stringContaining('type Query {')) }) test('creates an API key that lasts the chosen days', async () => { const user = userEvent.setup() renderPanel(graphqlApi()) const form = await screen.findByRole('form', {name: 'Create API key'}) await user.type(within(form).getByLabelText('Description'), 'mobile') const days = within(form).getByLabelText('Expires in (days)') await user.clear(days) await user.type(days, '30') await user.click(within(form).getByRole('button', {name: 'Create API key'})) expect(api.createGraphqlApiKey).toHaveBeenCalledWith('aws', 'a1', {description: 'mobile', expiresInDays: 30}) }) test('creates a DynamoDB data source from a chosen table and role', async () => { const user = userEvent.setup() renderPanel(graphqlApi()) const form = await screen.findByRole('form', {name: 'Create data source'}) await user.type(within(form).getByLabelText('Name'), 'notesStore') await user.selectOptions(within(form).getByLabelText('Type'), 'AMAZON_DYNAMODB') await user.selectOptions(within(form).getByLabelText('Table'), await within(form).findByRole('option', {name: 'notes'})) await user.type(within(form).getByLabelText('Service role ARN'), 'arn:aws:iam::000000000000:role/appsync') await user.click(within(form).getByRole('button', {name: 'Create data source'})) expect(api.createGraphqlDataSource).toHaveBeenCalledWith('aws', 'a1', {name: 'notesStore', type: 'AMAZON_DYNAMODB', tableName: 'notes', serviceRoleArn: 'arn:aws:iam::000000000000:role/appsync'}) }) test('attaches a resolver to a field without one, starting from the data source template', async () => { const user = userEvent.setup() renderPanel(graphqlApi()) const form = await screen.findByRole('form', {name: 'Attach resolver'}) await user.selectOptions(within(form).getByLabelText('Type'), 'Query') const field = within(form).getByLabelText('Field') expect(within(field).queryByRole('option', {name: 'note'})).not.toBeInTheDocument() await user.selectOptions(field, 'notes') await user.selectOptions(within(form).getByLabelText('Data source'), 'notesTable') expect((within(form).getByLabelText('Resolver code') as HTMLTextAreaElement).value).toContain("@aws-appsync/utils/dynamodb") await user.click(within(form).getByRole('button', {name: 'Attach resolver'})) expect(api.createGraphqlResolver).toHaveBeenCalledWith('aws', 'a1', expect.objectContaining({typeName: 'Query', fieldName: 'notes', dataSourceName: 'notesTable'})) expect(await screen.findByText('Attached a resolver to Query.notes.')).toBeInTheDocument() }) test('deletes a key and detaches a resolver after confirming', async () => { const user = userEvent.setup() vi.spyOn(window, 'confirm').mockReturnValue(true) renderPanel(graphqlApi()) await user.click(await screen.findByRole('button', {name: 'Delete da2-key'})) expect(api.deleteGraphqlApiKey).toHaveBeenCalledWith('aws', 'a1', 'da2-key') await user.click(screen.getByRole('button', {name: 'Detach Query.note'})) expect(api.deleteGraphqlResolver).toHaveBeenCalledWith('aws', 'a1', 'Query', 'note') })