Skip to content

Commit 5a632e9

Browse files
fix: Add fixes and new rules to tokenless banner show logic (#3764)
1 parent 80eaa5a commit 5a632e9

File tree

12 files changed

+384
-21
lines changed

12 files changed

+384
-21
lines changed

src/pages/AccountSettings/tabs/OktaAccess/queries/OktaConfigQueryOpts.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('useOktaConfig', () => {
9393
consoleSpy.mockRestore()
9494
})
9595

96-
it('rejects with 404 status', async () => {
96+
it('rejects with 400 status', async () => {
9797
setup({})
9898

9999
const { result } = renderHook(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const ADMIN_TOKEN_REQUIRED_BANNER = 'admin-token-required-banner'
2+
export const MEMBER_TOKEN_REQUIRED_BANNER = 'member-token-required-banner'
3+
export const ADMIN_TOKEN_NOT_REQUIRED_BANNER = 'admin-token-not-required-banner'
4+
export const MEMBER_TOKEN_NOT_REQUIRED_BANNER =
5+
'member-token-not-required-banner'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export { default } from './TokenlessSection'
2+
export {
3+
ADMIN_TOKEN_REQUIRED_BANNER,
4+
MEMBER_TOKEN_REQUIRED_BANNER,
5+
ADMIN_TOKEN_NOT_REQUIRED_BANNER,
6+
MEMBER_TOKEN_NOT_REQUIRED_BANNER,
7+
} from './constants'

src/pages/AccountSettings/tabs/OrgUploadToken/TokenlessSection/useSetUploadTokenRequired.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { useAddNotification } from 'services/toastNotification'
66
import Api from 'shared/api'
77
import { Provider } from 'shared/api/helpers'
88
import { rejectNetworkError } from 'shared/api/rejectNetworkError'
9+
import { removeFromLocalStorage } from 'ui/TopBanner/TopBanner'
10+
11+
import {
12+
ADMIN_TOKEN_NOT_REQUIRED_BANNER,
13+
ADMIN_TOKEN_REQUIRED_BANNER,
14+
MEMBER_TOKEN_NOT_REQUIRED_BANNER,
15+
MEMBER_TOKEN_REQUIRED_BANNER,
16+
} from './constants'
917

1018
const TOAST_DURATION = 10000
1119

@@ -102,6 +110,13 @@ export const useSetUploadTokenRequired = ({
102110
disappearAfter: TOAST_DURATION,
103111
})
104112

113+
// we want to show the banners again when this setting is changed
114+
// even if the user dismissed them in the past
115+
removeFromLocalStorage(MEMBER_TOKEN_NOT_REQUIRED_BANNER)
116+
removeFromLocalStorage(ADMIN_TOKEN_NOT_REQUIRED_BANNER)
117+
removeFromLocalStorage(MEMBER_TOKEN_REQUIRED_BANNER)
118+
removeFromLocalStorage(ADMIN_TOKEN_REQUIRED_BANNER)
119+
105120
// only want to invalidate the query if the mutation was successful
106121
// otherwise we're just going to re-fetch the same data
107122
queryClient.invalidateQueries(['GetUploadTokenRequired'])
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2+
import {
3+
QueryClientProvider as QueryClientProviderV5,
4+
QueryClient as QueryClientV5,
5+
useQuery as useQueryV5,
6+
} from '@tanstack/react-queryV5'
7+
import { renderHook, waitFor } from '@testing-library/react'
8+
import { graphql, HttpResponse } from 'msw'
9+
import { setupServer } from 'msw/node'
10+
import { MemoryRouter, Route } from 'react-router-dom'
11+
12+
import { TokenlessQueryOpts } from './TokenlessQueryOpts'
13+
14+
const queryClient = new QueryClient({
15+
defaultOptions: { queries: { retry: false } },
16+
})
17+
const queryClientV5 = new QueryClientV5({
18+
defaultOptions: { queries: { retry: false } },
19+
})
20+
21+
const wrapper =
22+
(initialEntries = '/gh'): React.FC<React.PropsWithChildren> =>
23+
({ children }) => (
24+
<QueryClientProviderV5 client={queryClientV5}>
25+
<QueryClientProvider client={queryClient}>
26+
<MemoryRouter initialEntries={[initialEntries]}>
27+
<Route path="/:provider">{children}</Route>
28+
</MemoryRouter>
29+
</QueryClientProvider>
30+
</QueryClientProviderV5>
31+
)
32+
33+
const server = setupServer()
34+
beforeAll(() => {
35+
server.listen()
36+
})
37+
38+
beforeEach(() => {
39+
server.resetHandlers()
40+
queryClientV5.clear()
41+
})
42+
43+
afterAll(() => {
44+
server.close()
45+
})
46+
47+
const mockCodecovOrg = {
48+
hasActiveRepos: true,
49+
hasPublicRepos: true,
50+
}
51+
52+
describe('TokenlessQueryOpts', () => {
53+
function setup({ invalidSchema = false }) {
54+
server.use(
55+
graphql.query('OwnerTokenlessData', () => {
56+
if (invalidSchema) {
57+
return HttpResponse.json({ data: {} })
58+
}
59+
return HttpResponse.json({ data: { owner: mockCodecovOrg } })
60+
})
61+
)
62+
}
63+
64+
describe('when called and user is authenticated', () => {
65+
it('returns the org', async () => {
66+
setup({})
67+
const { result } = renderHook(
68+
() =>
69+
useQueryV5(
70+
TokenlessQueryOpts({ username: 'codecov', provider: 'gh' })
71+
),
72+
{ wrapper: wrapper() }
73+
)
74+
75+
await waitFor(() => expect(result.current.data).toEqual(mockCodecovOrg))
76+
})
77+
})
78+
79+
describe('invalid schema', () => {
80+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
81+
82+
afterAll(() => {
83+
consoleSpy.mockRestore()
84+
})
85+
86+
it('rejects with 400 status', async () => {
87+
setup({ invalidSchema: true })
88+
89+
const { result } = renderHook(
90+
() =>
91+
useQueryV5(
92+
TokenlessQueryOpts({
93+
username: 'codecov',
94+
provider: 'gh',
95+
})
96+
),
97+
{ wrapper: wrapper() }
98+
)
99+
await waitFor(() => result.current.isLoading)
100+
await waitFor(() => !result.current.isLoading)
101+
102+
await waitFor(() =>
103+
expect(result.current.error).toEqual(
104+
expect.objectContaining({
105+
dev: 'TokenlessQueryOpts - Parsing Error',
106+
status: 400,
107+
})
108+
)
109+
)
110+
})
111+
})
112+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { queryOptions as queryOptionsV5 } from '@tanstack/react-queryV5'
2+
import { z } from 'zod'
3+
4+
import Api from 'shared/api'
5+
import { Provider } from 'shared/api/helpers'
6+
import { rejectNetworkError } from 'shared/api/rejectNetworkError'
7+
8+
const OwnerSchema = z.object({
9+
hasActiveRepos: z.boolean(),
10+
hasPublicRepos: z.boolean(),
11+
})
12+
13+
export type Owner = z.infer<typeof OwnerSchema>
14+
15+
const RequestSchema = z.object({
16+
owner: OwnerSchema.nullable(),
17+
})
18+
19+
interface TokenlessQueryArgs {
20+
username?: string
21+
provider: Provider
22+
opts?: {
23+
suspense?: boolean
24+
enabled?: boolean
25+
}
26+
}
27+
28+
const query = `query OwnerTokenlessData($username: String!) {
29+
owner(username: $username) {
30+
hasActiveRepos
31+
hasPublicRepos
32+
}
33+
}
34+
`
35+
36+
export function TokenlessQueryOpts({
37+
username,
38+
provider,
39+
opts = { enabled: username !== undefined },
40+
}: TokenlessQueryArgs) {
41+
const variables = { username }
42+
return queryOptionsV5({
43+
queryKey: ['OwnerTokenlessData', variables, provider],
44+
queryFn: ({ signal }) =>
45+
Api.graphql({
46+
provider,
47+
query,
48+
variables,
49+
signal,
50+
}).then((res) => {
51+
const parsedRes = RequestSchema.safeParse(res?.data)
52+
53+
if (!parsedRes.success) {
54+
return rejectNetworkError({
55+
errorName: 'Parsing Error',
56+
errorDetails: {
57+
callingFn: 'TokenlessQueryOpts',
58+
error: parsedRes.error,
59+
},
60+
})
61+
}
62+
63+
return parsedRes?.data?.owner ?? null
64+
}),
65+
...opts,
66+
})
67+
}

src/shared/GlobalTopBanners/TokenlessBanner/TokenNotRequiredBanner/TokenNotRequiredBanner.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import React from 'react'
22
import { useParams } from 'react-router'
33

4+
import {
5+
ADMIN_TOKEN_NOT_REQUIRED_BANNER,
6+
MEMBER_TOKEN_NOT_REQUIRED_BANNER,
7+
} from 'pages/AccountSettings/tabs/OrgUploadToken/TokenlessSection'
48
import { useUploadTokenRequired } from 'services/uploadTokenRequired'
59
import A from 'ui/A'
610
import Icon from 'ui/Icon'
@@ -13,7 +17,7 @@ interface UseParams {
1317

1418
const AdminTokenNotRequiredBanner: React.FC = () => {
1519
return (
16-
<TopBanner localStorageKey="admin-token-not-required-banner">
20+
<TopBanner localStorageKey={ADMIN_TOKEN_NOT_REQUIRED_BANNER}>
1721
<TopBanner.Start>
1822
<p className="items-center gap-1 md:flex">
1923
<span className="flex items-center gap-1 font-semibold">
@@ -41,7 +45,7 @@ const AdminTokenNotRequiredBanner: React.FC = () => {
4145

4246
const MemberTokenNotRequiredBanner: React.FC = () => {
4347
return (
44-
<TopBanner localStorageKey="member-token-not-required-banner">
48+
<TopBanner localStorageKey={MEMBER_TOKEN_NOT_REQUIRED_BANNER}>
4549
<TopBanner.Start>
4650
<p className="items-center gap-1 md:flex">
4751
<span className="flex items-center gap-1 font-semibold">

src/shared/GlobalTopBanners/TokenlessBanner/TokenRequiredBanner/TokenRequiredBanner.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useState } from 'react'
22
import { useParams } from 'react-router'
33

4+
import {
5+
ADMIN_TOKEN_REQUIRED_BANNER,
6+
MEMBER_TOKEN_REQUIRED_BANNER,
7+
} from 'pages/AccountSettings/tabs/OrgUploadToken/TokenlessSection'
48
import { useUploadTokenRequired } from 'services/uploadTokenRequired'
59
import A from 'ui/A'
610
import Button from 'ui/Button'
@@ -79,7 +83,7 @@ const AdminTokenRequiredBanner: React.FC = () => {
7983
const orgUploadToken = data?.orgUploadToken
8084

8185
return (
82-
<TopBanner localStorageKey="admin-token-required-banner">
86+
<TopBanner localStorageKey={ADMIN_TOKEN_REQUIRED_BANNER}>
8387
<TopBanner.Start>
8488
<p className="items-center gap-1 md:flex">
8589
<span className="flex items-center gap-1 font-semibold">
@@ -121,7 +125,7 @@ const MemberTokenRequiredBanner: React.FC = () => {
121125
const orgUploadToken = data?.orgUploadToken
122126

123127
return (
124-
<TopBanner localStorageKey="member-token-required-banner">
128+
<TopBanner localStorageKey={MEMBER_TOKEN_REQUIRED_BANNER}>
125129
<TopBanner.Start>
126130
<p className="items-center gap-1 md:flex">
127131
<span className="flex items-center gap-1 font-semibold">

0 commit comments

Comments
 (0)