()
+ const queryClient = useQueryClient()
+
+ const runPrefetch = async () => {
+ await queryClient.prefetchQuery({
+ queryKey: [
+ 'BranchContents',
+ provider,
+ owner,
+ repo,
+ branch,
+ path,
+ filters,
+ ],
+ queryFn: ({ signal }) =>
+ Api.graphql({
+ provider,
+ query,
+ signal,
+ variables: {
+ name: owner,
+ repo,
+ branch,
+ path,
+ filters,
+ },
+ }).then((res) => {
+ const parsedRes = BranchContentsSchema.safeParse(res?.data)
+
+ if (!parsedRes.success) {
+ return Promise.reject({
+ status: 404,
+ data: {},
+ dev: 'usePrefetchBranchDirEntry - 404 schema parsing failed',
+ } satisfies NetworkErrorObject)
+ }
+
+ const data = parsedRes.data
+
+ if (data?.owner?.repository?.__typename === 'NotFoundError') {
+ return Promise.reject({
+ status: 404,
+ data: {},
+ dev: 'usePrefetchBranchDirEntry - 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: 'usePrefetchBranchDirEntry - 403 OwnerNotActivatedError',
+ } satisfies NetworkErrorObject)
+ }
+
+ let results
+ if (
+ data?.owner?.repository?.branch?.head?.pathContents?.__typename ===
+ 'PathContents'
+ ) {
+ results =
+ data?.owner?.repository?.branch?.head?.pathContents?.results
+ }
+
+ return {
+ __typename:
+ data?.owner?.repository?.branch?.head?.pathContents?.__typename,
+ results: results ?? null,
+ indicationRange:
+ data?.owner?.repository?.repositoryConfig?.indicationRange,
+ }
+ }),
+ staleTime: 10000,
+ })
+ }
+
+ return { runPrefetch }
+}
diff --git a/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx b/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx
index abdeb0aaf8..1c7e73ff86 100644
--- a/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx
+++ b/src/services/pathContents/branch/dir/useRepoBranchContents.spec.tsx
@@ -4,33 +4,13 @@ import { graphql } from 'msw'
import { setupServer } from 'msw/node'
import { MemoryRouter, Route } from 'react-router-dom'
-import { useRepoBranchContents } from './index'
+import { useRepoBranchContents } from './useRepoBranchContents'
-const queryClient = new QueryClient({
- defaultOptions: { queries: { retry: false } },
-})
-
-const wrapper: React.FC = ({ children }) => (
-
-
- {children}
-
-
-)
-
-const server = setupServer()
-
-beforeAll(() => server.listen())
-afterEach(() => {
- queryClient.clear()
- server.resetHandlers()
-})
-afterAll(() => server.close())
-
-const dataReturned = {
+const mockData = {
owner: {
- username: 'Rabee-AbuBaker',
+ username: 'cool-user',
repository: {
+ __typename: 'Repository',
repositoryConfig: {
indicationRange: {
upperRange: 80,
@@ -64,6 +44,7 @@ const mockDataUnknownPath = {
owner: {
username: 'codecov',
repository: {
+ __typename: 'Repository',
repositoryConfig: {
indicationRange: {
upperRange: 80,
@@ -86,6 +67,7 @@ const mockDataMissingCoverage = {
owner: {
username: 'codecov',
repository: {
+ __typename: 'Repository',
repositoryConfig: {
indicationRange: {
upperRange: 80,
@@ -104,51 +86,97 @@ const mockDataMissingCoverage = {
},
}
+const mockDataRepositoryNotFound = {
+ owner: {
+ repository: {
+ __typename: 'NotFoundError',
+ message: 'repository not found',
+ },
+ },
+}
+
+const mockDataOwnerNotActivated = {
+ owner: {
+ repository: {
+ __typename: 'OwnerNotActivatedError',
+ message: 'owner not activated',
+ },
+ },
+}
+
+const mockUnsuccessfulParseError = {}
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+})
+
+const wrapper: React.FC = ({ children }) => (
+
+
+ {children}
+
+
+)
+
+const server = setupServer()
+
+beforeAll(() => {
+ server.listen()
+})
+
+afterEach(() => {
+ queryClient.clear()
+ server.resetHandlers()
+})
+
+afterAll(() => {
+ server.close()
+})
+
+interface SetupArgs {
+ isMissingCoverage?: boolean
+ isUnknownPath?: boolean
+ isRepositoryNotFoundError?: boolean
+ isOwnerNotActivatedError?: boolean
+ isUnsuccessfulParseError?: boolean
+}
+
describe('useRepoBranchContents', () => {
- function setup(isMissingCoverage = false, isUnknownPath = false) {
+ function setup({
+ isMissingCoverage = false,
+ isUnknownPath = false,
+ isOwnerNotActivatedError = false,
+ isRepositoryNotFoundError = false,
+ isUnsuccessfulParseError = false,
+ }: SetupArgs) {
server.use(
graphql.query('BranchContents', (req, res, ctx) => {
if (isMissingCoverage) {
return res(ctx.status(200), ctx.data(mockDataMissingCoverage))
- }
- if (isUnknownPath) {
+ } else if (isUnknownPath) {
return res(ctx.status(200), ctx.data(mockDataUnknownPath))
+ } else if (isRepositoryNotFoundError) {
+ return res(ctx.status(200), ctx.data(mockDataRepositoryNotFound))
+ } else if (isOwnerNotActivatedError) {
+ return res(ctx.status(200), ctx.data(mockDataOwnerNotActivated))
+ } else if (isUnsuccessfulParseError) {
+ return res(ctx.status(200), ctx.data(mockUnsuccessfulParseError))
}
- return res(ctx.status(200), ctx.data(dataReturned))
+
+ return res(ctx.status(200), ctx.data(mockData))
})
)
}
describe('when called', () => {
- beforeEach(() => {
- setup()
- })
-
- it('renders isLoading true', () => {
- const { result } = renderHook(
- () =>
- useRepoBranchContents({
- provider: 'gh',
- owner: 'Rabee-AbuBaker',
- repo: 'another-test',
- branch: 'main',
- path: '',
- }),
- {
- wrapper,
- }
- )
-
- expect(result.current.isLoading).toBeTruthy()
- })
-
describe('when data is loaded', () => {
it('returns the data', async () => {
+ setup({})
const { result } = renderHook(
() =>
useRepoBranchContents({
provider: 'gh',
- owner: 'Rabee-AbuBaker',
+ owner: 'cool-user',
repo: 'another-test',
branch: 'main',
path: '',
@@ -158,40 +186,39 @@ describe('useRepoBranchContents', () => {
}
)
- await waitFor(() => result.current.isLoading)
- await waitFor(() => !result.current.isLoading)
- await waitFor(() => result.current.isSuccess)
- expect(result.current.data).toEqual(
- expect.objectContaining({
- results: [
- {
- __typename: 'PathContentDir',
- hits: 9,
- misses: 0,
- partials: 0,
- lines: 10,
- name: 'src',
- path: 'src',
- percentCovered: 100.0,
+ await waitFor(() =>
+ expect(result.current.data).toEqual(
+ expect.objectContaining({
+ results: [
+ {
+ __typename: 'PathContentDir',
+ hits: 9,
+ misses: 0,
+ partials: 0,
+ lines: 10,
+ name: 'src',
+ path: 'src',
+ percentCovered: 100.0,
+ },
+ ],
+ indicationRange: {
+ upperRange: 80,
+ lowerRange: 60,
},
- ],
- indicationRange: {
- upperRange: 80,
- lowerRange: 60,
- },
- })
+ })
+ )
)
})
})
describe('on missing coverage', () => {
it('returns no results', async () => {
- setup(true)
+ setup({ isMissingCoverage: true })
const { result } = renderHook(
() =>
useRepoBranchContents({
provider: 'gh',
- owner: 'Rabee-AbuBaker',
+ owner: 'cool-user',
repo: 'another-test',
branch: 'main',
path: '',
@@ -201,31 +228,29 @@ describe('useRepoBranchContents', () => {
}
)
- await waitFor(() => result.current.isLoading)
- await waitFor(() => !result.current.isLoading)
- await waitFor(() => result.current.isSuccess)
-
- expect(result.current.data).toEqual(
- expect.objectContaining({
- indicationRange: {
- upperRange: 80,
- lowerRange: 60,
- },
- results: null,
- pathContentsType: 'MissingCoverage',
- })
+ await waitFor(() =>
+ expect(result.current.data).toEqual(
+ expect.objectContaining({
+ indicationRange: {
+ upperRange: 80,
+ lowerRange: 60,
+ },
+ results: null,
+ pathContentsType: 'MissingCoverage',
+ })
+ )
)
})
})
describe('on unknown path', () => {
it('returns no results', async () => {
- setup(false, true)
+ setup({ isUnknownPath: true })
const { result } = renderHook(
() =>
useRepoBranchContents({
provider: 'gh',
- owner: 'Rabee-AbuBaker',
+ owner: 'cool-user',
repo: 'another-test',
branch: 'main',
path: '',
@@ -235,21 +260,115 @@ describe('useRepoBranchContents', () => {
}
)
- await waitFor(() => result.current.isLoading)
- await waitFor(() => !result.current.isLoading)
- await waitFor(() => result.current.isSuccess)
-
- expect(result.current.data).toEqual(
- expect.objectContaining({
- indicationRange: {
- upperRange: 80,
- lowerRange: 60,
- },
- results: null,
- pathContentsType: 'UnknownPath',
- })
+ await waitFor(() =>
+ expect(result.current.data).toEqual(
+ expect.objectContaining({
+ indicationRange: {
+ upperRange: 80,
+ lowerRange: 60,
+ },
+ results: null,
+ pathContentsType: 'UnknownPath',
+ })
+ )
)
})
})
+
+ describe('request rejects', () => {
+ let oldConsoleError = console.error
+
+ beforeEach(() => {
+ console.error = () => null
+ })
+
+ afterEach(() => {
+ console.error = oldConsoleError
+ })
+
+ describe('on repository not found', () => {
+ it('rejects to repository not found error', async () => {
+ setup({ isUnsuccessfulParseError: true })
+ const { result } = renderHook(
+ () =>
+ useRepoBranchContents({
+ provider: 'gh',
+ owner: 'cool-user',
+ repo: 'another-test',
+ branch: 'main',
+ path: '',
+ }),
+ {
+ wrapper,
+ }
+ )
+
+ await waitFor(() =>
+ expect(result.current.error).toEqual(
+ expect.objectContaining({
+ status: 404,
+ dev: 'useRepoBranchContents - 404 schema parsing failed',
+ })
+ )
+ )
+ })
+ })
+
+ describe('on owner not activated', () => {
+ it('rejects to owner not activated error', async () => {
+ setup({ isRepositoryNotFoundError: true })
+ const { result } = renderHook(
+ () =>
+ useRepoBranchContents({
+ provider: 'gh',
+ owner: 'cool-user',
+ repo: 'another-test',
+ branch: 'main',
+ path: '',
+ }),
+ {
+ wrapper,
+ }
+ )
+
+ await waitFor(() =>
+ expect(result.current.error).toEqual(
+ expect.objectContaining({
+ status: 404,
+ dev: 'useRepoBranchContents - 404 NotFoundError',
+ })
+ )
+ )
+ })
+ })
+
+ describe('failing to parse schema', () => {
+ it('rejects to unknown error', async () => {
+ setup({ isOwnerNotActivatedError: true })
+ const { result } = renderHook(
+ () =>
+ useRepoBranchContents({
+ provider: 'gh',
+ owner: 'cool-user',
+ repo: 'another-test',
+ branch: 'main',
+ path: '',
+ }),
+ {
+ wrapper,
+ }
+ )
+
+ await waitFor(() =>
+ expect(result.current.error).toEqual(
+ expect.objectContaining({
+ status: 403,
+ dev: 'useRepoBranchContents - 403 OwnerNotActivatedError',
+ })
+ )
+ )
+ })
+ })
+ })
})
})
diff --git a/src/services/pathContents/branch/dir/useRepoBranchContents.tsx b/src/services/pathContents/branch/dir/useRepoBranchContents.tsx
index 4e21c2c7e5..f47dd23314 100644
--- a/src/services/pathContents/branch/dir/useRepoBranchContents.tsx
+++ b/src/services/pathContents/branch/dir/useRepoBranchContents.tsx
@@ -2,21 +2,17 @@ import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
import { UnknownFlagsSchema } from 'services/impactedFiles/schemas'
+import {
+ RepoNotFoundErrorSchema,
+ RepoOwnerNotActivatedErrorSchema,
+} from 'services/repo'
import { RepoConfig } from 'services/repo/useRepoConfig'
import Api from 'shared/api'
+import { NetworkErrorObject } from 'shared/api/helpers'
+import A from 'ui/A'
import { query } from './constants'
-interface FetchRepoContentsArgs {
- provider: string
- owner: string
- repo: string
- branch: string
- path: string
- filters?: {}
- signal?: AbortSignal
-}
-
const BasePathContentSchema = z.object({
hits: z.number(),
misses: z.number(),
@@ -73,69 +69,30 @@ const PathContentsUnionSchema = z.discriminatedUnion('__typename', [
export type PathContentResultType = z.infer
+const RepositorySchema = z.object({
+ __typename: z.literal('Repository'),
+ repositoryConfig: RepoConfig,
+ branch: z.object({
+ head: z
+ .object({
+ pathContents: PathContentsUnionSchema.nullish(),
+ })
+ .nullable(),
+ }),
+})
+
const BranchContentsSchema = z.object({
owner: z
.object({
- repository: z.object({
- repositoryConfig: RepoConfig,
- branch: z.object({
- head: z
- .object({
- pathContents: PathContentsUnionSchema.nullish(),
- })
- .nullable(),
- }),
- }),
+ repository: z.discriminatedUnion('__typename', [
+ RepositorySchema,
+ RepoNotFoundErrorSchema,
+ RepoOwnerNotActivatedErrorSchema,
+ ]),
})
.nullable(),
})
-function fetchRepoContents({
- provider,
- owner,
- repo,
- branch,
- path,
- filters,
- signal,
-}: FetchRepoContentsArgs) {
- return Api.graphql({
- provider,
- query,
- signal,
- variables: {
- name: owner,
- repo,
- branch,
- path,
- filters,
- },
- }).then((res) => {
- const parsedData = BranchContentsSchema.safeParse(res?.data)
-
- if (!parsedData.success) {
- return null
- }
-
- let results
- const pathContentsType =
- parsedData?.data?.owner?.repository?.branch?.head?.pathContents
- ?.__typename
- if (pathContentsType === 'PathContents') {
- results =
- parsedData?.data?.owner?.repository?.branch?.head?.pathContents?.results
- }
-
- return {
- results: results ?? null,
- pathContentsType,
- indicationRange:
- parsedData?.data?.owner?.repository?.repositoryConfig?.indicationRange,
- __typename: res?.data?.owner?.repository?.branch?.head?.__typename,
- }
- })
-}
-
interface RepoBranchContentsArgs {
provider: string
owner: string
@@ -159,26 +116,73 @@ export function useRepoBranchContents({
options,
}: RepoBranchContentsArgs) {
return useQuery({
- queryKey: [
- 'BranchContents',
- provider,
- owner,
- repo,
- branch,
- path,
- filters,
- query,
- ],
- queryFn: ({ signal }) =>
- fetchRepoContents({
+ queryKey: ['BranchContents', provider, owner, repo, branch, path, filters],
+ queryFn: ({ signal }) => {
+ return Api.graphql({
provider,
- owner,
- repo,
- branch,
- path,
- filters,
+ query,
signal,
- }),
+ variables: {
+ name: owner,
+ repo,
+ branch,
+ path,
+ filters,
+ },
+ }).then((res) => {
+ const parsedRes = BranchContentsSchema.safeParse(res?.data)
+
+ if (!parsedRes.success) {
+ return Promise.reject({
+ status: 404,
+ data: {},
+ dev: 'useRepoBranchContents - 404 schema parsing failed',
+ } satisfies NetworkErrorObject)
+ }
+
+ const data = parsedRes.data
+
+ if (data?.owner?.repository?.__typename === 'NotFoundError') {
+ return Promise.reject({
+ status: 404,
+ data: {},
+ dev: 'useRepoBranchContents - 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: 'useRepoBranchContents - 403 OwnerNotActivatedError',
+ } satisfies NetworkErrorObject)
+ }
+
+ let results
+ const pathContentsType =
+ data?.owner?.repository?.branch?.head?.pathContents?.__typename
+ if (pathContentsType === 'PathContents') {
+ results = data?.owner?.repository?.branch?.head?.pathContents?.results
+ }
+
+ return {
+ results: results ?? null,
+ pathContentsType,
+ indicationRange:
+ data?.owner?.repository?.repositoryConfig?.indicationRange,
+ __typename: res?.data?.owner?.repository?.branch?.head?.__typename,
+ }
+ })
+ },
...options,
})
}
diff --git a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx
index c4fd2071f9..fdf758c6a3 100644
--- a/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx
+++ b/src/shared/ContentsTable/TableEntries/BranchEntries/BranchDirEntry.spec.jsx
@@ -11,6 +11,7 @@ const mockData = {
owner: {
username: 'codecov',
repository: {
+ __typename: 'Repository',
repositoryConfig: {
indicationRange: {
upperRange: 80,
@@ -173,7 +174,7 @@ describe('BranchDirEntry', () => {
await waitFor(() =>
expect(queryClient.getQueryState().data).toStrictEqual({
- __typename: undefined,
+ __typename: 'PathContents',
indicationRange: {
upperRange: 80,
lowerRange: 60,