-
Notifications
You must be signed in to change notification settings - Fork 5
chore(Coder plugin): add tests for auth fallback UI #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Parkreiner
wants to merge
65
commits into
main
Choose a base branch
from
mes/auth-fallback-tests
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
9db4173
wip: commit progress on fallback UI
Parkreiner 50d9008
chore: move dep to peer dependencies
Parkreiner c57d4c9
wip: commit more progress
Parkreiner e567260
wip: more progress
Parkreiner 61ee528
refactor: consolidate card logic
Parkreiner 9b16cc2
fix: update component tracking hooks
Parkreiner 344593d
fix: add a11y landmark to auth fallback
Parkreiner 2041f17
wip: commit more style progress
Parkreiner 43cfd52
wip: commit more progress
Parkreiner 560d009
wip: more progress
Parkreiner 98faa12
wip: cleanup current approach
Parkreiner a73b1b1
wip: commit progress on observer approach
Parkreiner 0011dd2
wip: fix infinite loop for mutation logic
Parkreiner b8bc1ef
fix: prevent padding patches from firing too often
Parkreiner 2112995
fix: improve scoping of style overrides
Parkreiner d04274c
chore: finish intial version of fallback stylling
Parkreiner ae85984
fix: tidy up types
Parkreiner 6e18204
wip: create initial version of dialog form
Parkreiner f097f9b
wip: commit progress on modal
Parkreiner e1a70dd
chore: finish styling for modal wrapper
Parkreiner c4101f6
fix: update padding for FormDialog
Parkreiner f8bb852
wip: start extracting out auth form
Parkreiner 98c96af
fix: add missing barrel export file
Parkreiner a404ddb
fix: make sure that auth form isn't dismissed early
Parkreiner af2f383
fix: update auth imports
Parkreiner 5b04628
fix: update spacing for auth modal
Parkreiner 8ae3d14
refactor: clean up auth provider for clarity
Parkreiner 1b1f8c4
docs: rewrite comment for clarity
Parkreiner 1ea39e0
fix: improve granularity between official Coder components and user c…
Parkreiner 9236ca4
fix: update all internal consumers of useCoderAuth
Parkreiner b90ebd0
wip: commit initial version of useCoderQuery helper hook
Parkreiner 12a7b3e
refactor: rename hooks to avoid confusion
Parkreiner c146788
fix: update exports for plugin
Parkreiner e7e7755
docs: fill in incomplete sentence
Parkreiner 8493080
wip: commit initial version of useMutation wrapper
Parkreiner 8087e19
refactor: extract retry factor into global constant
Parkreiner 20fa328
merge
Parkreiner ca257ae
fix: add explicit return type to useCoderMutation
Parkreiner a92ec05
wip: start extracting auth logic into better reusable components
Parkreiner 028c6b7
fix: update card to have better styling for body
Parkreiner aea9345
wip: commit progress on style refactoring
Parkreiner ceb65e1
fix: update vertical padding for card wrapper
Parkreiner 8d1343f
chore: delete CoderAuthWrapper component
Parkreiner 91e0702
fix: update styling for auth fallback
Parkreiner b644eec
chore: shrink size of PR
Parkreiner b127195
fix: update imports
Parkreiner 7118896
docs: add comment about description setup
Parkreiner b364e0c
fix: remove risk of runtime render errors in auth form
Parkreiner 39f9a06
fix: update imports
Parkreiner 1148eae
fix: update font sizes to use relative units
Parkreiner 5aca4f7
fix: update peer dependencies for react-dom
Parkreiner b04482e
refactor: clean up auth revalidation logic
Parkreiner 063006c
wip: start updating tests for new code changes
Parkreiner 1702755
fix: adding missing test case for auth card
Parkreiner 1d928bb
wip: commit progress on auth form test updates
Parkreiner 34f027f
fix: removal vetigal properties
Parkreiner e0a4760
fix: get all CoderAuthForm tests passing
Parkreiner 15a4e22
fix: update import for auth hook in test
Parkreiner ba43e8a
wip: commit progress on tests
Parkreiner 06c128a
wip: commit more test progress
Parkreiner fc9f83d
wip: commit progress on fixing test isolation
Parkreiner 44801af
chore: finish all tests for auth fallback logic
Parkreiner 26c65ed
refactor: clean up setup code and revise comments
Parkreiner 719a9a6
Merge branch 'main' into mes/auth-fallback-tests
Parkreiner c553dc3
fix: remove risks of race conditions in tests
Parkreiner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
193 changes: 193 additions & 0 deletions
193
plugins/backstage-plugin-coder/src/components/CoderProvider/CoderAuthProvider.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/** | ||
* @file Ideally all the files in CoderProvider could be treated as | ||
* implementation details, and we could have a single test file for all the | ||
* pieces joined together. | ||
* | ||
* But because the auth is so complicated, it helps to have tests just for it. | ||
* | ||
* --- | ||
* @todo 2024-05-23 - Right now, there is a conflict when you try to call | ||
* Backstage's wrapInTestApp and also try to mock out localStorage. They | ||
* interact in such a way that when you call your mock's getItem method, it | ||
* immediately throws an error. Didn't want to get rid of wrapInTestApp, because | ||
* then that would require removing official Backstage components. wrapInTestApp | ||
* sets up a lot of things behind the scenes like React Router that these | ||
* components rely on. | ||
* | ||
* Figured out a way to write the tests that didn't involve extra mocking, but | ||
* it's not as airtight as it could be. Definitely worth opening an issue with | ||
* the Backstage repo upstream. | ||
*/ | ||
import React, { type ReactNode } from 'react'; | ||
import { render, screen, waitFor, within } from '@testing-library/react'; | ||
import { | ||
BACKSTAGE_APP_ROOT_ID, | ||
CoderAuthProvider, | ||
TOKEN_STORAGE_KEY, | ||
useEndUserCoderAuth, | ||
useInternalCoderAuth, | ||
} from './CoderAuthProvider'; | ||
import { CoderClient, coderClientApiRef } from '../../api/CoderClient'; | ||
import { | ||
getMockConfigApi, | ||
getMockDiscoveryApi, | ||
getMockIdentityApi, | ||
mockAppConfig, | ||
mockCoderAuthToken, | ||
} from '../../testHelpers/mockBackstageData'; | ||
import { UrlSync } from '../../api/UrlSync'; | ||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils'; | ||
import { QueryClientProvider } from '@tanstack/react-query'; | ||
import { getMockQueryClient } from '../../testHelpers/setup'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { CoderAppConfigProvider } from './CoderAppConfigProvider'; | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
|
||
const appRootNodes = document.querySelectorAll(BACKSTAGE_APP_ROOT_ID); | ||
appRootNodes.forEach(node => node.remove()); | ||
window.localStorage.removeItem(TOKEN_STORAGE_KEY); | ||
}); | ||
|
||
function renderAuthProvider(children?: ReactNode) { | ||
const urlSync = new UrlSync({ | ||
apis: { | ||
configApi: getMockConfigApi(), | ||
discoveryApi: getMockDiscoveryApi(), | ||
}, | ||
}); | ||
|
||
const queryClient = getMockQueryClient(); | ||
const identityApi = getMockIdentityApi(); | ||
|
||
// Can't use initialToken property, because then the Auth provider won't be | ||
// aware of it. When testing for UI authentication, we need to feed the token | ||
// from localStorage to the provider, which then feeds it to the client while | ||
// keeping track of the React state changes | ||
const coderClient = new CoderClient({ | ||
apis: { urlSync, identityApi }, | ||
}); | ||
|
||
const mockAppRoot = document.createElement('div'); | ||
mockAppRoot.id = BACKSTAGE_APP_ROOT_ID; | ||
document.body.append(mockAppRoot); | ||
|
||
return render( | ||
<TestApiProvider apis={[[coderClientApiRef, coderClient]]}> | ||
<QueryClientProvider client={queryClient}> | ||
<CoderAppConfigProvider appConfig={mockAppConfig}> | ||
<CoderAuthProvider>{children}</CoderAuthProvider> | ||
</CoderAppConfigProvider> | ||
</QueryClientProvider> | ||
</TestApiProvider>, | ||
{ | ||
baseElement: mockAppRoot, | ||
wrapper: ({ children }) => wrapInTestApp(children), | ||
}, | ||
); | ||
} | ||
|
||
describe(`${CoderAuthProvider.name}`, () => { | ||
/** | ||
* @todo Figure out what general auth state logic could benefit from tests | ||
* and put them in a separate describe block | ||
*/ | ||
describe('Fallback auth input', () => { | ||
const fallbackTriggerMatcher = /Authenticate with Coder/; | ||
|
||
function MockTrackedComponent() { | ||
const auth = useInternalCoderAuth(); | ||
return <p>Authenticated? {auth.isAuthenticated ? 'Yes!' : 'No...'}</p>; | ||
} | ||
|
||
function MockEndUserComponent() { | ||
const auth = useEndUserCoderAuth(); | ||
return <p>Authenticated? {auth.isAuthenticated ? 'Yes!' : 'No...'}</p>; | ||
} | ||
|
||
it('Will never display the auth fallback if the user is already authenticated', async () => { | ||
/** | ||
* Not 100% sure on why this works. We load the token in before rendering, | ||
* so that we can bring the token into the UI on the initial render | ||
* | ||
* But as part of that rendering, wrapInTestApp eventually replaces | ||
* localStorage with a mock. So the initial state is getting carried over | ||
* to the mock? Or maybe it's not really a full mock and is just a spy? | ||
*/ | ||
window.localStorage.setItem(TOKEN_STORAGE_KEY, mockCoderAuthToken); | ||
|
||
renderAuthProvider( | ||
<> | ||
<MockTrackedComponent /> | ||
<MockEndUserComponent /> | ||
</>, | ||
); | ||
|
||
await waitFor(() => { | ||
const authenticatedComponents = screen.getAllByText(/Yes!/); | ||
expect(authenticatedComponents).toHaveLength(2); | ||
}); | ||
|
||
const authFallbackTrigger = screen.queryByRole('button', { | ||
name: fallbackTriggerMatcher, | ||
}); | ||
|
||
expect(authFallbackTrigger).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('Will display an auth fallback input when there are no components on screen that use Coder plugin logic', async () => { | ||
renderAuthProvider(); | ||
const authFallbackTrigger = await screen.findByRole('button', { | ||
name: fallbackTriggerMatcher, | ||
}); | ||
|
||
expect(authFallbackTrigger).toBeInTheDocument(); | ||
}); | ||
|
||
it('Will never display the auth fallback if there are tracked Coder components that let you submit auth info in other ways', async () => { | ||
renderAuthProvider(<MockTrackedComponent />); | ||
|
||
const authFallbackTrigger = screen.queryByRole('button', { | ||
name: fallbackTriggerMatcher, | ||
}); | ||
|
||
await screen.findByText(/No\.\.\./); | ||
expect(authFallbackTrigger).not.toBeInTheDocument(); | ||
}); | ||
|
||
it(`Does not consider users of ${useEndUserCoderAuth.name} when deciding whether to show fallback auth UI`, async () => { | ||
renderAuthProvider(<MockEndUserComponent />); | ||
const authFallbackTrigger = await screen.findByRole('button', { | ||
name: fallbackTriggerMatcher, | ||
}); | ||
|
||
expect(authFallbackTrigger).toBeInTheDocument(); | ||
}); | ||
|
||
it('Lets the user go through a full authentication flow via the fallback auth UI', async () => { | ||
renderAuthProvider(); | ||
const user = userEvent.setup(); | ||
|
||
const authFallbackTrigger = await screen.findByRole('button', { | ||
name: fallbackTriggerMatcher, | ||
}); | ||
|
||
await user.click(authFallbackTrigger); | ||
const authForm = await screen.findByRole('form', { | ||
name: /Authenticate with Coder/, | ||
}); | ||
|
||
const tokenInput = await within(authForm).findByLabelText(/Auth token/); | ||
await user.type(tokenInput, mockCoderAuthToken); | ||
|
||
const submitButton = await within(authForm).findByRole('button', { | ||
name: /Authenticate/, | ||
}); | ||
|
||
await user.click(submitButton); | ||
expect(authForm).not.toBeInTheDocument(); | ||
expect(authFallbackTrigger).not.toBeInTheDocument(); | ||
}); | ||
}); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just decided to tuck this in here to keep the layout effect logic more self-contained