From 5005356de59dd590e605b825b167244bf91e3f07 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Fri, 10 Jan 2025 15:07:49 -0400 Subject: [PATCH 1/2] create new mutation for updating bundle cache state --- .../bundleAnalysis/useUpdateBundleCache.tsx | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/services/bundleAnalysis/useUpdateBundleCache.tsx diff --git a/src/services/bundleAnalysis/useUpdateBundleCache.tsx b/src/services/bundleAnalysis/useUpdateBundleCache.tsx new file mode 100644 index 0000000000..441d6ee9c1 --- /dev/null +++ b/src/services/bundleAnalysis/useUpdateBundleCache.tsx @@ -0,0 +1,111 @@ +import { useMutation as useMutationV5 } from '@tanstack/react-queryV5' +import { z } from 'zod' + +import Api from 'shared/api' +import { rejectNetworkError } from 'shared/api/helpers' + +const UpdateBundleCacheInputSchema = z.array( + z.object({ + bundleName: z.string(), + isCached: z.boolean(), + }) +) + +const MutationErrorSchema = z.discriminatedUnion('__typename', [ + z.object({ + __typename: z.literal('UnauthenticatedError'), + message: z.string(), + }), + z.object({ + __typename: z.literal('ValidationError'), + message: z.string(), + }), +]) + +const MutationRequestSchema = z.object({ + updateBundleCacheConfig: z + .object({ + results: UpdateBundleCacheInputSchema.nullable(), + error: MutationErrorSchema.nullable(), + }) + .nullable(), +}) + +const query = ` +mutation UpdateBundleCacheConfig( + $owner: String! + $repo: String! + $bundles: [BundleCacheConfigInput!]! +) { + updateBundleCacheConfig( + input: { owner: $owner, repoName: $repo, bundles: $bundles } + ) { + results { + bundleName + isCached + } + error { + __typename + ... on UnauthenticatedError { + message + } + ... on ValidationError { + message + } + } + } +}` + +interface UseUpdateBundleCacheArgs { + provider: string + owner: string + repo: string +} + +export const useUpdateBundleCache = ({ + provider, + owner, + repo, +}: UseUpdateBundleCacheArgs) => { + return useMutationV5({ + throwOnError: false, + mutationFn: (input: z.infer) => { + return Api.graphqlMutation({ + provider, + query, + variables: { owner, repo, bundles: input }, + mutationPath: 'updateBundleCache', + }).then((res) => { + const parsedData = MutationRequestSchema.safeParse(res.data) + + if (!parsedData.success) { + return rejectNetworkError({ + status: 400, + error: parsedData.error, + data: {}, + dev: 'useUpdateBundleCache - 400 failed to parse data', + }) + } + + const updateBundleCacheConfig = parsedData.data.updateBundleCacheConfig + if ( + updateBundleCacheConfig?.error?.__typename === 'UnauthenticatedError' + ) { + return Promise.reject({ + error: 'UnauthenticatedError', + message: updateBundleCacheConfig?.error?.message, + }) + } + + if (updateBundleCacheConfig?.error?.__typename === 'ValidationError') { + return Promise.reject({ + error: 'ValidationError', + message: updateBundleCacheConfig?.error?.message, + }) + } + + return updateBundleCacheConfig?.results ?? [] + }) + }, + }) +} From f58dfb49ec25a9049e2e717e5cceb584e16a1641 Mon Sep 17 00:00:00 2001 From: nicholas-codecov Date: Fri, 10 Jan 2025 15:11:15 -0400 Subject: [PATCH 2/2] add in tests for new mutation --- .../useUpdateBundleCache.test.tsx | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/services/bundleAnalysis/useUpdateBundleCache.test.tsx diff --git a/src/services/bundleAnalysis/useUpdateBundleCache.test.tsx b/src/services/bundleAnalysis/useUpdateBundleCache.test.tsx new file mode 100644 index 0000000000..2faaf5aea1 --- /dev/null +++ b/src/services/bundleAnalysis/useUpdateBundleCache.test.tsx @@ -0,0 +1,197 @@ +import { + QueryClientProvider as QueryClientProviderV5, + QueryClient as QueryClientV5, +} from '@tanstack/react-queryV5' +import { act, renderHook, waitFor } from '@testing-library/react' +import { graphql, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +import { useUpdateBundleCache } from './useUpdateBundleCache' + +const mockSuccessfulResponse = { + data: { + updateBundleCacheConfig: { + results: [{ bundleName: 'bundle-1', isCached: true }], + error: null, + }, + }, +} + +const mockParsingError = { + data: null, + errors: [{ message: 'Parsing error' }], +} + +const mockUnauthenticatedError = { + data: { + updateBundleCacheConfig: { + results: null, + error: { + __typename: 'UnauthenticatedError', + message: 'Unauthenticated error', + }, + }, + }, +} + +const mockValidationError = { + data: { + updateBundleCacheConfig: { + results: null, + error: { __typename: 'ValidationError', message: 'Validation error' }, + }, + }, +} + +const queryClient = new QueryClientV5({ + defaultOptions: { mutations: { retry: false } }, +}) + +const wrapper: React.FC = ({ children }) => ( + {children} +) + +const server = setupServer() + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + queryClient.clear() + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + isParsingError?: boolean + isUnauthenticatedError?: boolean + isValidationError?: boolean +} + +describe('useUpdateBundleCache', () => { + function setup({ + isParsingError = false, + isUnauthenticatedError = false, + isValidationError = false, + }: SetupArgs) { + server.use( + graphql.mutation('UpdateBundleCacheConfig', () => { + if (isParsingError) { + return HttpResponse.json(mockParsingError) + } else if (isUnauthenticatedError) { + return HttpResponse.json(mockUnauthenticatedError) + } else if (isValidationError) { + return HttpResponse.json(mockValidationError) + } + return HttpResponse.json(mockSuccessfulResponse) + }) + ) + } + + describe('when the mutation is successful', () => { + it('returns the updated results', async () => { + setup({}) + const { result } = renderHook( + () => + useUpdateBundleCache({ + provider: 'gh', + owner: 'owner', + repo: 'repo', + }), + { wrapper } + ) + + act(() => + result.current.mutate([{ bundleName: 'bundle-1', isCached: true }]) + ) + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([ + { bundleName: 'bundle-1', isCached: true }, + ]) + }) + }) + + describe('when the mutation fails', () => { + describe('when the mutation fails with a parsing error', () => { + it('returns a parsing error', async () => { + setup({ isParsingError: true }) + const { result } = renderHook( + () => + useUpdateBundleCache({ + provider: 'gh', + owner: 'owner', + repo: 'repo', + }), + { wrapper } + ) + + act(() => + result.current.mutate([{ bundleName: 'bundle-1', isCached: true }]) + ) + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.error).toEqual({ + data: {}, + dev: 'useUpdateBundleCache - 400 failed to parse data', + status: 400, + }) + }) + }) + + describe('when the mutation fails with an unauthenticated error', () => { + it('returns an unauthenticated error', async () => { + setup({ isUnauthenticatedError: true }) + const { result } = renderHook( + () => + useUpdateBundleCache({ + provider: 'gh', + owner: 'owner', + repo: 'repo', + }), + { wrapper } + ) + + act(() => + result.current.mutate([{ bundleName: 'bundle-1', isCached: true }]) + ) + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.error).toEqual({ + error: 'UnauthenticatedError', + message: 'Unauthenticated error', + }) + }) + }) + + describe('when the mutation fails with a validation error', () => { + it('returns a validation error', async () => { + setup({ isValidationError: true }) + const { result } = renderHook( + () => + useUpdateBundleCache({ + provider: 'gh', + owner: 'owner', + repo: 'repo', + }), + { wrapper } + ) + + act(() => + result.current.mutate([{ bundleName: 'bundle-1', isCached: true }]) + ) + + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.error).toEqual({ + error: 'ValidationError', + message: 'Validation error', + }) + }) + }) + }) +})