diff --git a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx index f2a240ddd5..e326df54f7 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.jsx @@ -98,16 +98,16 @@ function CommitRoutes() { function CommitErrorBanners() { const { owner } = useParams() const { data: ownerData } = useOwner({ username: owner }) - const { data: commitErrorData } = useCommitErrors() + const { data } = useCommitErrors() - const invalidYaml = commitErrorData?.yamlErrors?.find( + const invalidYaml = data?.yamlErrors?.find( (err) => err?.errorCode === 'invalid_yaml' ) return ( <> {ownerData?.isCurrentUserPartOfOrg && ( - + )} {invalidYaml && } diff --git a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.spec.jsx b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.spec.jsx index aa3ea27da3..35db9372a2 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.spec.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/CommitCoverage.spec.jsx @@ -211,6 +211,7 @@ const mockCommitErrors = (hasErrors = false) => { return { owner: { repository: { + __typename: 'Repository', commit: { yamlErrors: { edges: yamlErrors, diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx index 099925d446..31545a67be 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.jsx @@ -11,8 +11,9 @@ import YamlModalErrorBanner from './YamlModalErrorBanner' const YAMLViewer = lazy(() => import('./YAMLViewer')) function YamlModal({ showYAMLModal, setShowYAMLModal }) { - const { data: commitErrors } = useCommitErrors() - const invalidYaml = commitErrors?.yamlErrors?.find( + const { data } = useCommitErrors() + + const invalidYaml = data?.yamlErrors?.find( (err) => err?.errorCode === 'invalid_yaml' ) diff --git a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.spec.jsx b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.spec.jsx index f23effff1d..81a187edb2 100644 --- a/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.spec.jsx +++ b/src/pages/CommitDetailPage/CommitCoverage/YamlModal/YamlModal.spec.jsx @@ -25,6 +25,7 @@ const wrapper = ({ children }) => ( const mockCommitNoYamlErrors = { owner: { repository: { + __typename: 'Repository', commit: { yamlErrors: { edges: [], @@ -40,6 +41,7 @@ const mockCommitNoYamlErrors = { const mockCommitYamlErrors = { owner: { repository: { + __typename: 'Repository', commit: { yamlErrors: { edges: [{ node: { errorCode: 'invalid_yaml' } }], diff --git a/src/services/commitErrors/index.js b/src/services/commitErrors/index.ts similarity index 100% rename from src/services/commitErrors/index.js rename to src/services/commitErrors/index.ts diff --git a/src/services/commitErrors/useCommitErrors.js b/src/services/commitErrors/useCommitErrors.js deleted file mode 100644 index bfbf17260d..0000000000 --- a/src/services/commitErrors/useCommitErrors.js +++ /dev/null @@ -1,55 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { useParams } from 'react-router-dom' - -import Api from 'shared/api' -import { mapEdges } from 'shared/utils/graphql' - -export function useCommitErrors() { - const { provider, owner, repo, commit: commitid } = useParams() - const query = ` - query CommitErrors($owner: String!, $repo: String!, $commitid: String!) { - owner(username: $owner) { - repository: repositoryDeprecated(name: $repo) { - commit(id: $commitid) { - yamlErrors: errors(errorType: YAML_ERROR){ - edges{ - node{ - errorCode - } - } - } - botErrors: errors(errorType: BOT_ERROR){ - edges{ - node{ - errorCode - } - } - } - } - } - } - } - ` - - return useQuery({ - queryKey: ['CommitErrors', provider, owner, repo, commitid, query], - queryFn: ({ signal }) => { - return Api.graphql({ - provider, - query, - signal, - variables: { - owner, - repo, - commitid, - }, - }) - }, - select: ({ data }) => { - return { - yamlErrors: mapEdges(data?.owner?.repository?.commit?.yamlErrors) || [], - botErrors: mapEdges(data?.owner?.repository?.commit?.botErrors) || [], - } - }, - }) -} diff --git a/src/services/commitErrors/useCommitErrors.spec.js b/src/services/commitErrors/useCommitErrors.spec.js deleted file mode 100644 index f7391ccfe3..0000000000 --- a/src/services/commitErrors/useCommitErrors.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { renderHook, waitFor } from '@testing-library/react' -import { graphql } from 'msw' -import { setupServer } from 'msw/node' -import { MemoryRouter, Route } from 'react-router-dom' - -import { useCommitErrors } from './useCommitErrors' - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}) -const wrapper = ({ children }) => ( - - - {children} - - -) - -const dataReturned = { - owner: { - repository: { - commit: { - yamlErrors: { - edges: [{ node: { errorCode: 'invalid_yaml' } }], - }, - botErrors: { - edges: [{ node: { errorCode: 'repo_bot_invalid' } }], - }, - }, - }, - }, -} - -const server = setupServer() - -beforeAll(() => server.listen()) -beforeEach(() => { - server.resetHandlers() - queryClient.clear() -}) -afterAll(() => server.close()) - -describe('useCommitErrors', () => { - function setup() { - server.use( - graphql.query(`CommitErrors`, (req, res, ctx) => { - return res(ctx.status(200), ctx.data(dataReturned)) - }) - ) - } - - describe('when called and user is authenticated', () => { - beforeEach(() => { - setup() - }) - - it('returns commit info', async () => { - const { result } = renderHook(() => useCommitErrors(), { - wrapper, - }) - - await waitFor(() => result.current.isSuccess) - - await waitFor(() => - expect(result.current.data).toEqual({ - botErrors: [{ errorCode: 'repo_bot_invalid' }], - yamlErrors: [{ errorCode: 'invalid_yaml' }], - }) - ) - }) - }) -}) diff --git a/src/services/commitErrors/useCommitErrors.spec.tsx b/src/services/commitErrors/useCommitErrors.spec.tsx new file mode 100644 index 0000000000..a7d6f19156 --- /dev/null +++ b/src/services/commitErrors/useCommitErrors.spec.tsx @@ -0,0 +1,158 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { graphql } from 'msw' +import { setupServer } from 'msw/node' +import React from 'react' +import { MemoryRouter, Route } from 'react-router-dom' + +import { useCommitErrors } from './useCommitErrors' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) +const wrapper: React.FC = ({ children }) => ( + + + {children} + + +) + +const dataReturned = { + owner: { + repository: { + __typename: 'Repository', + commit: { + yamlErrors: { + edges: [{ node: { errorCode: 'invalid_yaml' } }], + }, + botErrors: { + edges: [{ node: { errorCode: 'repo_bot_invalid' } }], + }, + }, + }, + }, +} + +const mockNotFoundError = { + owner: { + repository: { + __typename: 'NotFoundError', + message: 'commit not found', + }, + }, +} + +const mockOwnerNotActivatedError = { + owner: { + repository: { + __typename: 'OwnerNotActivatedError', + message: 'owner not activated', + }, + }, +} + +const mockUnsuccessfulParseError = {} + +const server = setupServer() + +beforeAll(() => server.listen()) +beforeEach(() => { + server.resetHandlers() + queryClient.clear() +}) +afterAll(() => server.close()) + +describe('useCommitErrors', () => { + function setup({ + isNotFoundError = false, + isOwnerNotActivatedError = false, + isUnsuccessfulParseError = false, + }) { + server.use( + graphql.query(`CommitErrors`, (req, res, ctx) => { + if (isNotFoundError) { + return res(ctx.status(200), ctx.data(mockNotFoundError)) + } else if (isOwnerNotActivatedError) { + return res(ctx.status(200), ctx.data(mockOwnerNotActivatedError)) + } else if (isUnsuccessfulParseError) { + return res(ctx.status(200), ctx.data(mockUnsuccessfulParseError)) + } else { + return res(ctx.status(200), ctx.data(dataReturned)) + } + }) + ) + } + + describe('when called and user is authenticated', () => { + beforeEach(() => { + setup({}) + }) + + it('returns commit info', async () => { + const { result } = renderHook(() => useCommitErrors(), { + wrapper, + }) + + await waitFor(() => result.current.isSuccess) + + await waitFor(() => + expect(result.current.data).toEqual({ + botErrors: [{ errorCode: 'repo_bot_invalid' }], + yamlErrors: [{ errorCode: 'invalid_yaml' }], + }) + ) + }) + }) + describe('when called but repository errors', () => { + it('can return unsuccessful parse error', async () => { + setup({ isUnsuccessfulParseError: true }) + const { result } = renderHook(() => useCommitErrors(), { + wrapper, + }) + + await waitFor(() => expect(result.current.isError).toBeTruthy()) + await waitFor(() => + expect(result.current.error).toEqual( + expect.objectContaining({ + status: 404, + }) + ) + ) + }) + it('can return not found error', async () => { + setup({ isNotFoundError: true }) + const { result } = renderHook(() => useCommitErrors(), { + wrapper, + }) + + await waitFor(() => expect(result.current.isError).toBeTruthy()) + await waitFor(() => + expect(result.current.error).toEqual( + expect.objectContaining({ + status: 404, + }) + ) + ) + }) + it('can return owner not activated error', async () => { + setup({ isOwnerNotActivatedError: true }) + const { result } = renderHook(() => useCommitErrors(), { + wrapper, + }) + + await waitFor(() => expect(result.current.isError).toBeTruthy()) + await waitFor(() => + expect(result.current.error).toEqual( + expect.objectContaining({ + status: 403, + }) + ) + ) + }) + }) +}) diff --git a/src/services/commitErrors/useCommitErrors.tsx b/src/services/commitErrors/useCommitErrors.tsx new file mode 100644 index 0000000000..8f4bd9642a --- /dev/null +++ b/src/services/commitErrors/useCommitErrors.tsx @@ -0,0 +1,151 @@ +import { useQuery } from '@tanstack/react-query' +import { useParams } from 'react-router-dom' +import { z } from 'zod' + +import { + RepoNotFoundErrorSchema, + RepoOwnerNotActivatedErrorSchema, +} from 'services/repo' +import Api from 'shared/api' +import { NetworkErrorObject } from 'shared/api/helpers' +import { mapEdges } from 'shared/utils/graphql' +import A from 'ui/A' + +interface URLParams { + provider: string + owner: string + repo: string + commit: string +} + +const query = ` +query CommitErrors($owner: String!, $repo: String!, $commitid: String!) { + owner(username: $owner) { + repository (name: $repo) { + __typename + ... on Repository { + commit(id: $commitid) { + yamlErrors: errors(errorType: YAML_ERROR){ + edges { + node { + errorCode + } + } + } + botErrors: errors(errorType: BOT_ERROR){ + edges { + node { + errorCode + } + } + } + } + } + ... on NotFoundError { + message + } + ... on OwnerNotActivatedError { + message + } + } + } +} +` + +const NodeSchema = z.object({ + node: z.object({ errorCode: z.string() }), +}) + +const RepositorySchema = z.object({ + __typename: z.literal('Repository'), + commit: z + .object({ + yamlErrors: z + .object({ + edges: z.array(NodeSchema.nullable()), + }) + .nullish(), + botErrors: z + .object({ + edges: z.array(NodeSchema.nullable()), + }) + .nullish(), + }) + .nullable(), +}) + +const useCommitErrorsSchema = z.object({ + owner: z + .object({ + repository: z + .discriminatedUnion('__typename', [ + RepositorySchema, + RepoNotFoundErrorSchema, + RepoOwnerNotActivatedErrorSchema, + ]) + .nullable(), + }) + .nullable(), +}) + +export function useCommitErrors() { + const { provider, owner, repo, commit: commitid } = useParams() + + return useQuery({ + queryKey: ['CommitErrors', provider, owner, repo, commitid], + queryFn: ({ signal }) => + Api.graphql({ + provider, + query, + signal, + variables: { + owner, + repo, + commitid, + }, + }).then((res) => { + const parsedData = useCommitErrorsSchema.safeParse(res?.data) + + if (!parsedData.success) { + return Promise.reject({ + status: 404, + data: {}, + dev: 'useCommitErrors - 404 Failed to parse data', + } satisfies NetworkErrorObject) + } + + const data = parsedData.data + + if (data?.owner?.repository?.__typename === 'NotFoundError') { + return Promise.reject({ + status: 404, + data: {}, + dev: 'useCommitErrors - 404 NotFoundError', + } satisfies NetworkErrorObject) + } + + if (data?.owner?.repository?.__typename === 'OwnerNotActivatedError') { + return Promise.reject({ + status: 403, + data: { + detail: ( +

+ Activation is required to view this repo, please{' '} + {/* @ts-expect-error */} + click here to activate + your account. +

+ ), + }, + dev: 'useCommitErrors - 403 OwnerNotActivated Error', + } satisfies NetworkErrorObject) + } + + return { + yamlErrors: + mapEdges(data?.owner?.repository?.commit?.yamlErrors) || [], + botErrors: mapEdges(data?.owner?.repository?.commit?.botErrors) || [], + } + }), + }) +}