From 9b5b416d4d3b256aea5c549f12610d27efd06793 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Fri, 3 Apr 2020 08:32:11 -0300 Subject: [PATCH 01/20] Link userList to userPage through an onClick function in each box of userList --- frontend/src/UserList.js | 9 +++++++-- frontend/src/UserPage.js | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index 85cabeabb9..c47d68560b 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -19,8 +19,10 @@ import { Trans } from '@lingui/macro' import gql from 'graphql-tag' import { useQuery } from '@apollo/react-hooks' import { PaginationButtons } from './PaginationButtons' +import { useHistory } from 'react-router-dom' export function UserList() { + const history = useHistory() // This function generates the URL when the page loads const { loading, error, data } = useQuery( gql` @@ -102,14 +104,17 @@ export function UserList() { {data ? data.user.affiliations.edges[0].node.organization.affiliatedUsers.edges.map( - edge => { + (edge) => { return ( { - window.alert('clicked box') + history.push({ + pathname: '/user', + state: { detail: edge.node.user.userName }, + }) }} _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} p="30px" diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index e6e46b91ac..4c9fff3528 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -21,6 +21,10 @@ import { import { useApolloClient, useMutation, useQuery } from '@apollo/react-hooks' import { PasswordConfirmation } from './PasswordConfirmation' import gql from 'graphql-tag' +import { useLocation } from 'react-router-dom' + +export function UserPage(props) { + const location = useLocation() export function UserPage() { const userName = 'mike@korora.ca' @@ -71,7 +75,9 @@ export function UserPage() { error: queryUserError, data: queryUserData, } = useQuery(QUERY_USER, { - variables: { userName: userName }, + variables: { + userName: location.state ? location.state.detail : props.userName, + }, }) if (updatePasswordLoading || queryUserLoading) { @@ -86,7 +92,7 @@ export function UserPage() { ) } + +UserPage.propTypes = { userName: string } From 3c4256b55bba3cee2b4e290bf17363a3e99fddbf Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Fri, 3 Apr 2020 09:36:26 -0300 Subject: [PATCH 02/20] Move queries and mutations to frontend/src/graphql. --- frontend/src/UserList.js | 49 ++------------------------- frontend/src/UserPage.js | 36 ++------------------ frontend/src/graphql/mutations.js | 18 ++++++++++ frontend/src/graphql/queries.js | 55 +++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 80 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index c47d68560b..5a91d24487 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -16,7 +16,7 @@ import { } from '@chakra-ui/core' import { Trans } from '@lingui/macro' -import gql from 'graphql-tag' +import { QUERY_USERLIST } from './graphql/queries' import { useQuery } from '@apollo/react-hooks' import { PaginationButtons } from './PaginationButtons' import { useHistory } from 'react-router-dom' @@ -24,52 +24,7 @@ import { useHistory } from 'react-router-dom' export function UserList() { const history = useHistory() // This function generates the URL when the page loads - const { loading, error, data } = useQuery( - gql` - { - user { - affiliations { - edges { - node { - organization { - acronym - affiliatedUsers { - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - node { - id - user { - userName - displayName - tfa - affiliations { - edges { - node { - id - organization { - acronym - } - permission - } - } - } - } - } - } - } - } - } - } - } - } - } - `, - ) + const { loading, error, data } = useQuery(QUERY_USERLIST) if (loading) { return

Loading...

} diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index 4c9fff3528..b2701e8fb5 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -20,43 +20,13 @@ import { } from '@chakra-ui/core' import { useApolloClient, useMutation, useQuery } from '@apollo/react-hooks' import { PasswordConfirmation } from './PasswordConfirmation' -import gql from 'graphql-tag' import { useLocation } from 'react-router-dom' +import { QUERY_USER } from './graphql/queries' +import { UPDATE_PASSWORD } from './graphql/mutations' + export function UserPage(props) { const location = useLocation() - -export function UserPage() { - const userName = 'mike@korora.ca' - - // TODO: Move to mutations folder - const UPDATE_PASSWORD = gql` - mutation UpdatePassword( - $userName: EmailAddress! - $password: String! - $confirmPassword: String! - ) { - updatePassword( - userName: $userName - password: $password - confirmPassword: $confirmPassword - ) { - user { - userName - } - } - } - ` - - const QUERY_USER = gql` - query User($userName: EmailAddress!) { - user(userName: $userName) { - displayName - lang - } - } - ` - const client = useApolloClient() const toast = useToast() const history = useHistory() diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 698dcd8ce5..04da8bec60 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -42,4 +42,22 @@ export const VALIDATE_TWO_FACTOR = gql` } ` +export const UPDATE_PASSWORD = gql` + mutation UpdatePassword( + $userName: EmailAddress! + $password: String! + $confirmPassword: String! + ) { + updatePassword( + userName: $userName + password: $password + confirmPassword: $confirmPassword + ) { + user { + userName + } + } + } +` + export default '' diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 0250cfbf65..3ed053db7b 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -17,3 +17,58 @@ export const GENERATE_OTP_URL = gql` generateOtpUrl(email: $email) } ` + +export const QUERY_USERLIST = gql` + { + user { + affiliations { + edges { + node { + organization { + acronym + affiliatedUsers { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + node { + id + user { + userName + displayName + tfa + affiliations { + edges { + node { + id + organization { + acronym + } + permission + } + } + } + } + } + } + } + } + } + } + } + } + } +` + +export const QUERY_USER = gql` + query User($userName: EmailAddress!) { + user(userName: $userName) { + userName + displayName + lang + } + } +` From 1c4376620505c7d0d386a482ef8750b788a680dd Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Fri, 3 Apr 2020 10:46:46 -0300 Subject: [PATCH 03/20] Remove redundant and update test to include barebone mocked userlist test. --- frontend/src/UserList.js | 156 +++++++++++------------- frontend/src/__tests__/UserList.test.js | 96 ++++++++++++++- 2 files changed, 165 insertions(+), 87 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index 5a91d24487..a245a980dd 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -29,7 +29,7 @@ export function UserList() { return

Loading...

} if (error) { - console.log(error) + return

Error :(

} if (data) { console.log(data) @@ -61,95 +61,79 @@ export function UserList() { ? data.user.affiliations.edges[0].node.organization.affiliatedUsers.edges.map( (edge) => { return ( - - { - history.push({ - pathname: '/user', - state: { detail: edge.node.user.userName }, - }) - }} - _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} - p="30px" - > - - - {edge.node.user.displayName} - - - - - {edge.node.user.userName} - - - - - Orgs:  - {// Populate the user-orgs list. - edge.node.user.affiliations.edges.map( - (edge, i, arr) => { - if (arr.length - 1 === i) { - return ( - - {edge.node.organization.acronym} - - ) - } - return ( - - {edge.node.organization.acronym + ' | '} - - ) - }, - )} - + { + history.push({ + pathname: '/user', + state: { detail: edge.node.user.userName }, + }) + }} + _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} + p="30px" + > + + + {edge.node.user.displayName} + + + + + {edge.node.user.userName} + + + + + Orgs:  + {// Populate the user-orgs list. + edge.node.user.affiliations.edges.map((edge, i, arr) => { + if (arr.length - 1 === i) { + return ( + + {edge.node.organization.acronym} + + ) + } + return ( + + {edge.node.organization.acronym + ' | '} + + ) + })} - + + - - TwoFactor - - - Admin - - - TwoFactor + + - - + > + Admin + + + +
) }, ) diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index 4479e56d65..2172600d32 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -7,10 +7,12 @@ import { ThemeProvider, theme } from '@chakra-ui/core' import { I18nProvider } from '@lingui/react' import { MockedProvider } from '@apollo/react-testing' +import { QUERY_USERLIST } from '../graphql/queries' + describe('', () => { afterEach(cleanup) - it('the component renders', async () => { + it('renders', async () => { const { container } = render( @@ -24,4 +26,96 @@ describe('', () => { ) expect(container).toBeTruthy() }) + + it('the component renders correctly with mocked data', async () => { + const mocks = [ + { + request: { + query: QUERY_USERLIST, + }, + result: { + data: { + user: { + affiliations: { + edges: [ + { + node: { + organization: { + acronym: 'TEST', + affiliatedUsers: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'string', + endCursor: 'string', + }, + edges: [ + { + node: { + id: 'NzYzMzQ1MzQ1Ng==', + user: { + userName: 'testuser@testemail.ca', + displayName: 'Test User', + tfa: false, + affiliations: { + edges: [ + { + node: { + id: 'ODAyOTY1MDMyMQ==', + organization: { + acronym: 'GC', + }, + permission: 'SUPER_ADMIN', + }, + }, + { + node: { + id: 'NjI3NzcyNDQ=', + organization: { + acronym: 'BC', + }, + permission: 'USER_WRITE', + }, + }, + { + node: { + id: 'NjkyMzAyOTAx', + organization: { + acronym: 'BC', + }, + permission: 'ADMIN', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + ] + const { container } = render( + + + + + + + + + , + ) + expect(container).toBeTruthy() + + // TODO: Write test for this. + }) }) From d10b99a35c9305ea30574d4808415a0a53037a52 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 08:12:14 -0300 Subject: [PATCH 04/20] Add Mocked Data test and expectations. Optimize most imports. This is the commit with the strange unused 'import App' that when removed breaks the tests. --- frontend/src/UserList.js | 3 -- frontend/src/__tests__/UserList.test.js | 47 +++++++++++++++++-------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index a245a980dd..3688295456 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -31,9 +31,6 @@ export function UserList() { if (error) { return

Error :(

} - if (data) { - console.log(data) - } return ( diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index 2172600d32..f4e3fefb83 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -1,7 +1,11 @@ import React from 'react' import { UserList } from '../UserList' import { i18n } from '@lingui/core' -import { render, cleanup } from '@testing-library/react' +import { + render, + cleanup, + waitForElementToBeRemoved, +} from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { ThemeProvider, theme } from '@chakra-ui/core' import { I18nProvider } from '@lingui/react' @@ -9,10 +13,13 @@ import { MockedProvider } from '@apollo/react-testing' import { QUERY_USERLIST } from '../graphql/queries' +// If this unused import, the mocked data test fails. VERY weird. +import App from '../App' + describe('', () => { afterEach(cleanup) - it('renders', async () => { + it('successfully renders', async () => { const { container } = render( @@ -24,10 +31,10 @@ describe('', () => { , ) - expect(container).toBeTruthy() + expect(container).toBeDefined() }) - it('the component renders correctly with mocked data', async () => { + it('successfully renders with mocked data', async () => { const mocks = [ { request: { @@ -103,19 +110,29 @@ describe('', () => { }, }, ] - const { container } = render( - - - - + // Set the inital history item to user-list + const { container, queryAllByRole, getByText } = render( + + + + - - - - , +
+ + + , + ) + expect(container).toBeDefined() + + expect(getByText('Loading...')).toBeInTheDocument() + const loadingElement = getByText('Loading...') + + await waitForElementToBeRemoved(loadingElement).then(() => + console.log('Element no longer in DOM'), ) - expect(container).toBeTruthy() - // TODO: Write test for this. + // Get all of the mocked user cards, and expect there to be only one entry. + const userCards = queryAllByRole('userCard') + expect(userCards).toHaveLength(1) }) }) From bdfb713296d39dab93f8f0a5d70d960fc76c5db0 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 08:47:42 -0300 Subject: [PATCH 05/20] Add barebones UserPage test. Update error and loading state handlers for UserPage. --- frontend/src/UserPage.js | 12 +++- frontend/src/__tests__/UserPage.test.js | 75 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 frontend/src/__tests__/UserPage.test.js diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index b2701e8fb5..9d91f77c96 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -50,11 +50,19 @@ export function UserPage(props) { }, }) - if (updatePasswordLoading || queryUserLoading) { + if (queryUserLoading) { return

Loading...

} - if (queryUserError || updatePasswordError) { + if (queryUserError) { + return

Error

+ } + + if (updatePasswordLoading) { + return

Loading...

+ } + + if (updatePasswordError) { return

Error

} diff --git a/frontend/src/__tests__/UserPage.test.js b/frontend/src/__tests__/UserPage.test.js new file mode 100644 index 0000000000..c10f44f05b --- /dev/null +++ b/frontend/src/__tests__/UserPage.test.js @@ -0,0 +1,75 @@ +import React from 'react' +import { UserPage } from '../UserPage' +import { i18n } from '@lingui/core' +import { + render, + cleanup, + waitForElementToBeRemoved, +} from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { ThemeProvider, theme } from '@chakra-ui/core' +import { I18nProvider } from '@lingui/react' +import { MockedProvider } from '@apollo/react-testing' + +import { QUERY_USER } from '../graphql/queries' +import { UPDATE_PASSWORD } from '../graphql/mutations' + +describe('', () => { + afterEach(cleanup) + + it('successfully renders', async () => { + const { container } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + }) + + it('successfully renders with mock data', async () => { + const mocks = [ + { + request: { + query: QUERY_USER, + }, + result: { + user: { + userName: 'testuser@testemail.gc.ca', + displayName: 'Test User', + lang: 'English', + }, + }, + }, + { + request: { + query: UPDATE_PASSWORD, + }, + result: { + updatePassword: { + user: { + userName: 'Gregg_Grady4@hotmail.com', + }, + }, + }, + }, + ] + const { container, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + }) +}) From 883090940d9b3a4c93947aab2ca666963a7d9ccf Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 08:48:41 -0300 Subject: [PATCH 06/20] Update docs to maintain eslint --- frontend/src/__tests__/UserPage.test.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/__tests__/UserPage.test.js b/frontend/src/__tests__/UserPage.test.js index c10f44f05b..f865c73cde 100644 --- a/frontend/src/__tests__/UserPage.test.js +++ b/frontend/src/__tests__/UserPage.test.js @@ -1,11 +1,7 @@ import React from 'react' import { UserPage } from '../UserPage' import { i18n } from '@lingui/core' -import { - render, - cleanup, - waitForElementToBeRemoved, -} from '@testing-library/react' +import { render, cleanup } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { ThemeProvider, theme } from '@chakra-ui/core' import { I18nProvider } from '@lingui/react' @@ -59,7 +55,7 @@ describe('', () => { }, }, ] - const { container, getByText } = render( + const { container } = render( From 220da1b446d528b61bb00e82907928421f46c8c7 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 09:15:33 -0300 Subject: [PATCH 07/20] Add a redirect test for UserList. This test will check that when a listElement is clicked, the user is redirected to the user page which is at route '/user'. --- frontend/src/__tests__/UserList.test.js | 124 +++++++++++++++++++++++- frontend/src/__tests__/UserPage.test.js | 1 + 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index f4e3fefb83..e18a5fb55e 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -5,11 +5,14 @@ import { render, cleanup, waitForElementToBeRemoved, + fireEvent, + waitFor, } from '@testing-library/react' -import { MemoryRouter } from 'react-router-dom' +import { MemoryRouter, Router } from 'react-router-dom' import { ThemeProvider, theme } from '@chakra-ui/core' import { I18nProvider } from '@lingui/react' import { MockedProvider } from '@apollo/react-testing' +import { createMemoryHistory } from 'history' import { QUERY_USERLIST } from '../graphql/queries' @@ -135,4 +138,123 @@ describe('', () => { const userCards = queryAllByRole('userCard') expect(userCards).toHaveLength(1) }) + + it('redirects to userPage when a list element is clicked', async () => { + const mocks = [ + { + request: { + query: QUERY_USERLIST, + }, + result: { + data: { + user: { + affiliations: { + edges: [ + { + node: { + organization: { + acronym: 'TEST', + affiliatedUsers: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'string', + endCursor: 'string', + }, + edges: [ + { + node: { + id: 'NzYzMzQ1MzQ1Ng==', + user: { + userName: 'testuser@testemail.ca', + displayName: 'Test User', + tfa: false, + affiliations: { + edges: [ + { + node: { + id: 'ODAyOTY1MDMyMQ==', + organization: { + acronym: 'GC', + }, + permission: 'SUPER_ADMIN', + }, + }, + { + node: { + id: 'NjI3NzcyNDQ=', + organization: { + acronym: 'BC', + }, + permission: 'USER_WRITE', + }, + }, + { + node: { + id: 'NjkyMzAyOTAx', + organization: { + acronym: 'BC', + }, + permission: 'ADMIN', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + ] + + // create a history object and inject it so we can inspect it afterwards + // for the side effects of our form submission (a redirect to /!). + const history = createMemoryHistory({ + initialEntries: ['user-list'], + initialIndex: 0, + }) + + // Set the inital history item to user-list + const { container, queryAllByRole, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + + expect(getByText('Loading...')).toBeInTheDocument() + const loadingElement = getByText('Loading...') + + await waitForElementToBeRemoved(loadingElement).then(() => + console.log('Element no longer in DOM'), + ) + + // Get all of the mocked user cards, and expect there to be only one entry. + const userCards = queryAllByRole('userCard') + expect(userCards).toHaveLength(1) + + const leftClick = { button: 0 } + fireEvent.click(userCards[0], leftClick) + // default `button` property for click events is set to `0` which is a left click. + + await waitFor(() => { + // Path should be '/user', so expect that value + expect(history.location.pathname).toEqual('/user') + }) + }) }) diff --git a/frontend/src/__tests__/UserPage.test.js b/frontend/src/__tests__/UserPage.test.js index f865c73cde..5a9a6443ce 100644 --- a/frontend/src/__tests__/UserPage.test.js +++ b/frontend/src/__tests__/UserPage.test.js @@ -55,6 +55,7 @@ describe('', () => { }, }, ] + const { container } = render( From bee8cb27783b298dbfad8bd9e2e7b5ed42300e45 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 09:17:25 -0300 Subject: [PATCH 08/20] Remove unnecessary console log that was included in the '.then()' function. --- frontend/src/__tests__/UserList.test.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index e18a5fb55e..b50cf87fa3 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -130,9 +130,7 @@ describe('', () => { expect(getByText('Loading...')).toBeInTheDocument() const loadingElement = getByText('Loading...') - await waitForElementToBeRemoved(loadingElement).then(() => - console.log('Element no longer in DOM'), - ) + await waitForElementToBeRemoved(loadingElement) // Get all of the mocked user cards, and expect there to be only one entry. const userCards = queryAllByRole('userCard') @@ -240,9 +238,7 @@ describe('', () => { expect(getByText('Loading...')).toBeInTheDocument() const loadingElement = getByText('Loading...') - await waitForElementToBeRemoved(loadingElement).then(() => - console.log('Element no longer in DOM'), - ) + await waitForElementToBeRemoved(loadingElement) // Get all of the mocked user cards, and expect there to be only one entry. const userCards = queryAllByRole('userCard') From 28078ed407f0e0bbb9d44874924f646c26f13727 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Mon, 6 Apr 2020 10:25:31 -0300 Subject: [PATCH 09/20] Add badge rendering tests. Update logic for rendering in UserList.js --- frontend/src/UserList.js | 4 +- frontend/src/__tests__/UserList.test.js | 181 ++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index 3688295456..fc6c70f802 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -112,8 +112,8 @@ export function UserList() { ', () => { expect(history.location.pathname).toEqual('/user') }) }) + + it('badges are green when TwoFactor and Admin values are true', async () => { + const mocks = [ + { + request: { + query: QUERY_USERLIST, + }, + result: { + data: { + user: { + affiliations: { + edges: [ + { + node: { + organization: { + acronym: 'TEST', + affiliatedUsers: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'string', + endCursor: 'string', + }, + edges: [ + { + node: { + id: 'NzYzMzQ1MzQ1Ng==', + user: { + userName: 'testuser@testemail.ca', + displayName: 'Test User', + tfa: true, + affiliations: { + edges: [ + { + node: { + id: 'ODAyOTY1MDMyMQ==', + organization: { + acronym: 'GC', + }, + permission: 'SUPER_ADMIN', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + ] + + const { container, queryAllByRole, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + + expect(getByText('Loading...')).toBeInTheDocument() + const loadingElement = getByText('Loading...') + + await waitForElementToBeRemoved(loadingElement) + + // Get all of the mocked user cards, and expect there to be only one entry. + const userCards = queryAllByRole('userCard') + expect(userCards).toHaveLength(1) + + const tfaBadge = getByText(/TwoFactor/i) + const adminBadge = getByText(/Admin/i) + + expect(tfaBadge).toBeDefined() + expect(adminBadge).toBeDefined() + + expect(tfaBadge).toHaveStyle('background-color: rgb(198, 246, 213)') + expect(adminBadge).toHaveStyle('background-color: rgb(198, 246, 213)') + }) + it('badges are red when TwoFactor and Admin values are false', async () => { + const mocks = [ + { + request: { + query: QUERY_USERLIST, + }, + result: { + data: { + user: { + affiliations: { + edges: [ + { + node: { + organization: { + acronym: 'TEST', + affiliatedUsers: { + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, + startCursor: 'string', + endCursor: 'string', + }, + edges: [ + { + node: { + id: 'NzYzMzQ1MzQ1Ng==', + user: { + userName: 'testuser@testemail.ca', + displayName: 'Test User', + tfa: false, + affiliations: { + edges: [ + { + node: { + id: 'ODAyOTY1MDMyMQ==', + organization: { + acronym: 'GC', + }, + permission: 'USER_READ', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + }, + ] + + const { container, queryAllByRole, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + + expect(getByText('Loading...')).toBeInTheDocument() + const loadingElement = getByText('Loading...') + + await waitForElementToBeRemoved(loadingElement) + + // Get all of the mocked user cards, and expect there to be only one entry. + const userCards = queryAllByRole('userCard') + expect(userCards).toHaveLength(1) + + const tfaBadge = getByText(/TwoFactor/i) + const adminBadge = getByText(/Admin/i) + + expect(tfaBadge).toBeDefined() + expect(adminBadge).toBeDefined() + + expect(tfaBadge).toHaveStyle('background-color: rgb(254, 215, 215)') + expect(adminBadge).toHaveStyle('background-color: rgb(254, 215, 215)') + }) }) From 185f79427701a7cfdcc9e7aca3096b2615b035ca Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Tue, 7 Apr 2020 11:06:17 -0300 Subject: [PATCH 10/20] Disable functions on userPage when accessed from userList. Add tests. --- frontend/src/UserList.js | 8 +- frontend/src/UserPage.js | 102 +++++++++++++----------- frontend/src/__tests__/UserPage.test.js | 85 +++++++++----------- 3 files changed, 98 insertions(+), 97 deletions(-) diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index fc6c70f802..300b5a0cd0 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -53,7 +53,6 @@ export function UserList() { - {data ? data.user.affiliations.edges[0].node.organization.affiliatedUsers.edges.map( (edge) => { @@ -112,8 +111,10 @@ export function UserList() { Loading...

+ return

Loading user...

} if (queryUserError) { - return

Error

+ return

{String(queryUserError)}

} if (updatePasswordLoading) { @@ -63,7 +63,7 @@ export function UserPage(props) { } if (updatePasswordError) { - return

Error

+ return

{String(updatePasswordError)}

} return ( @@ -150,6 +150,7 @@ export function UserPage(props) { onClick={() => { history.push('/two-factor-code') }} + isDisabled={location.state ? true : false} > Enable 2FA @@ -163,6 +164,7 @@ export function UserPage(props) { Manage API keys + @@ -186,52 +189,57 @@ export function UserPage(props) { Change Password - - Change your password below by entering and confirming a new password. - - { - // Submit GraphQL mutation - console.log(values) - await updatePassword({ - variables: { - userName: 'testuser@test.ca', // This needs to be retreived from a seperate GQL query or props that will populate this entire page with data. - password: values.password, - confirmPassword: values.confirmPassword, - }, - }) - - if (!updatePasswordError) { - console.log(updatePasswordData) - toast({ - title: 'Password Updated.', - description: 'You have successfully changed your password.', - status: 'success', - duration: 9000, - isClosable: true, + {location.state ? ( + You can only change the password for your own account. + ) : ( + { + // Submit GraphQL mutation + console.log(values) + await updatePassword({ + variables: { + userName: 'testuser@test.ca', // This needs to be retreived from a seperate GQL query or props that will populate this entire page with data. + password: values.password, + confirmPassword: values.confirmPassword, + }, }) - } - }} - > - {({ handleSubmit, isSubmitting }) => ( -
- - - - - - - )} -
+ + if (!updatePasswordError) { + console.log(updatePasswordData) + toast({ + title: 'Password Updated.', + description: 'You have successfully changed your password.', + status: 'success', + duration: 9000, + isClosable: true, + }) + } + }} + > + {({ handleSubmit, isSubmitting }) => ( +
+ + Change your password below by entering and confirming a new + password. + + + + + + + + )} +
+ )} ) diff --git a/frontend/src/__tests__/UserPage.test.js b/frontend/src/__tests__/UserPage.test.js index 5a9a6443ce..0cecfd7f02 100644 --- a/frontend/src/__tests__/UserPage.test.js +++ b/frontend/src/__tests__/UserPage.test.js @@ -1,7 +1,7 @@ import React from 'react' import { UserPage } from '../UserPage' import { i18n } from '@lingui/core' -import { render, cleanup } from '@testing-library/react' +import { render, cleanup, act } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { ThemeProvider, theme } from '@chakra-ui/core' import { I18nProvider } from '@lingui/react' @@ -13,60 +13,53 @@ import { UPDATE_PASSWORD } from '../graphql/mutations' describe('', () => { afterEach(cleanup) - it('successfully renders', async () => { - const { container } = render( - - - - - - - - - , - ) - expect(container).toBeDefined() - }) + const values = { + userName: 'testuser@testemail.gc.ca', + } - it('successfully renders with mock data', async () => { - const mocks = [ - { - request: { - query: QUERY_USER, - }, - result: { - user: { - userName: 'testuser@testemail.gc.ca', - displayName: 'Test User', - lang: 'English', - }, + const mocks = [ + { + request: { + query: QUERY_USER, + variables: { + userName: values.userName, }, }, - { - request: { - query: UPDATE_PASSWORD, + result: { + user: { + userName: 'testuser@testemail.gc.ca', + displayName: 'Test User', + lang: 'English', }, - result: { - updatePassword: { - user: { - userName: 'Gregg_Grady4@hotmail.com', - }, + }, + }, + { + request: { + query: UPDATE_PASSWORD, + }, + result: { + updatePassword: { + user: { + userName: 'Gregg_Grady4@hotmail.com', }, }, }, - ] + }, + ] - const { container } = render( - - + it('renders without error', () => { + act(() => { + render( + - - - + + + + + - - , - ) - expect(container).toBeDefined() + , + ) + }) }) }) From 392fc52ca103f9c69f0ff0ce9a7e4d728b598438 Mon Sep 17 00:00:00 2001 From: fluxcd Date: Fri, 3 Apr 2020 14:41:29 +0000 Subject: [PATCH 11/20] Auto-release multiple images - gcr.io/track-compliance/api:master-596f9ee - gcr.io/track-compliance/frontend:master-91c4299 [ci skip] --- platform/overlays/gke/kustomization.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/overlays/gke/kustomization.yaml b/platform/overlays/gke/kustomization.yaml index 1883da27bc..bd3afab976 100644 --- a/platform/overlays/gke/kustomization.yaml +++ b/platform/overlays/gke/kustomization.yaml @@ -8,9 +8,9 @@ resources: - publicgateway.yaml images: - name: gcr.io/track-compliance/api - newTag: master-4301b10 + newTag: master-596f9ee - name: gcr.io/track-compliance/frontend - newTag: master-7ee5ba8 + newTag: master-91c4299 patchesStrategicMerge: - tracker-api-deployment.yaml - tracker-frontend-deployment.yaml From 8e77464cdf5603e6619263b9ee8e2d2042843fe4 Mon Sep 17 00:00:00 2001 From: fluxcd Date: Mon, 6 Apr 2020 10:21:30 +0000 Subject: [PATCH 12/20] Auto-release gcr.io/track-compliance/api:master-4301b10 [ci skip] --- platform/overlays/gke/kustomization.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/overlays/gke/kustomization.yaml b/platform/overlays/gke/kustomization.yaml index bd3afab976..5edd8475b6 100644 --- a/platform/overlays/gke/kustomization.yaml +++ b/platform/overlays/gke/kustomization.yaml @@ -8,7 +8,7 @@ resources: - publicgateway.yaml images: - name: gcr.io/track-compliance/api - newTag: master-596f9ee + newTag: master-4301b10 - name: gcr.io/track-compliance/frontend newTag: master-91c4299 patchesStrategicMerge: From 3e525fcab54779bc8d7a6d161b16c925de5b601c Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Tue, 7 Apr 2020 14:51:13 -0300 Subject: [PATCH 13/20] Add custom type for userList --- frontend/schema.faker.graphql | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index cfb3eebc8f..ea0f33d6b7 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -735,6 +735,11 @@ type Query { An api endpoint that will send a verification email to a given email address. """ sendValidationEmail(email: EmailAddress!): NotificationEmail + + """ + An api endpoint that will be used to populate a userList component in the front end. + """ + userList(organizaion: Acronym!): UserList } """DMARC aggregate report record""" @@ -1090,3 +1095,20 @@ type WWWScanEdge { """A cursor for use in pagination""" cursor: String! } + +""" +This custom object is used to populate the userList componenet in the front end. +""" +type UserList { + organizaion: Acronym! + userItems: [UserListItem]! +} + +type UserListItem implements Node { + """The ID of the object.""" + id: ID! + userName: EmailAddress! + displayName: String! + tfa: Boolean! + admin: Boolean! +} \ No newline at end of file From c95f27f71f5635d399ec0a756173e56bc6981258 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Wed, 8 Apr 2020 08:25:16 -0300 Subject: [PATCH 14/20] Add custom faker types for userPage and userList. Add these types to the query type as well. --- frontend/schema.faker.graphql | 44 +++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index ea0f33d6b7..e933c55264 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -740,6 +740,12 @@ type Query { An api endpoint that will be used to populate a userList component in the front end. """ userList(organizaion: Acronym!): UserList + + """ + An api endpoint that will be used to populate a userPage component in the front end. + """ + userPage(userName: EmailAddress!): UserPage + } """DMARC aggregate report record""" @@ -1100,15 +1106,49 @@ type WWWScanEdge { This custom object is used to populate the userList componenet in the front end. """ type UserList { - organizaion: Acronym! + """ Indicates which organization this list is being queried for.""" + organization: Acronym! + + """ A list of userItems that will make up the list on the front end. An item per user in the org." userItems: [UserListItem]! } type UserListItem implements Node { """The ID of the object.""" id: ID! + + """ The users email address or userName userName: EmailAddress! + + """ The users display name""" displayName: String! + + """ Indicates wether or not this user has enabled two factor authentication" tfa: Boolean! + + """ Indicates if this user is an admin of the organization specified in UserList query.""" admin: Boolean! -} \ No newline at end of file +} + +""" +This custom gql object is used to populate a userPage component in the front end. +""" +type UserPage{ + """ The users email address or userName + userName: EmailAddress! + + """ The users display name""" + displayName: String! + + """ Indicates which organization this users data is being displayed for.""" + organization: Acronym! + + """ Indicates the preferred language of this user.""" + lang: String! + + """ Indicates wether or not this user has enabled two factor authentication" + tfa: Boolean! + + """ Indicates if this user is an admin of the organization specified.""" + admin: Boolean! +} From 1efeeb7161f12faf29b8b7edcb05f984bdbf9948 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Wed, 8 Apr 2020 09:19:15 -0300 Subject: [PATCH 15/20] Update schema.faker.graphql to use a UserItemEdges. Update UserList.js to bind data based on new query format. Update /graphql/queries to user new query format based on custom type. Updated tests to user new query format. --- frontend/schema.faker.graphql | 24 +- frontend/src/UserList.js | 162 +++++++------- frontend/src/__tests__/UserList.test.js | 278 ++++++------------------ frontend/src/graphql/queries.js | 50 ++--- 4 files changed, 170 insertions(+), 344 deletions(-) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index e933c55264..5d18a34210 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -1109,21 +1109,24 @@ type UserList { """ Indicates which organization this list is being queried for.""" organization: Acronym! - """ A list of userItems that will make up the list on the front end. An item per user in the org." - userItems: [UserListItem]! + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection. Aka: A list of userItems that will make up the list on the front end.""" + edges: [UserItemEdge]! } type UserListItem implements Node { """The ID of the object.""" id: ID! - """ The users email address or userName + """ The users email address or userName """ userName: EmailAddress! """ The users display name""" displayName: String! - """ Indicates wether or not this user has enabled two factor authentication" + """ Indicates wether or not this user has enabled two factor authentication""" tfa: Boolean! """ Indicates if this user is an admin of the organization specified in UserList query.""" @@ -1134,7 +1137,7 @@ type UserListItem implements Node { This custom gql object is used to populate a userPage component in the front end. """ type UserPage{ - """ The users email address or userName + """ The users email address or userName""" userName: EmailAddress! """ The users display name""" @@ -1146,9 +1149,18 @@ type UserPage{ """ Indicates the preferred language of this user.""" lang: String! - """ Indicates wether or not this user has enabled two factor authentication" + """ Indicates wether or not this user has enabled two factor authentication""" tfa: Boolean! """ Indicates if this user is an admin of the organization specified.""" admin: Boolean! } + +"""A Relay edge containing a `UserItem` and its cursor.""" +type UserItemEdge { + """The item at the end of the edge""" + node: UserListItem! + + """A cursor for use in pagination""" + cursor: String! +} \ No newline at end of file diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index 300b5a0cd0..a3c829322d 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -54,98 +54,84 @@ export function UserList() { {data - ? data.user.affiliations.edges[0].node.organization.affiliatedUsers.edges.map( - (edge) => { - return ( - { - history.push({ - pathname: '/user', - state: { detail: edge.node.user.userName }, - }) - }} - _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} - p="30px" - > - - - {edge.node.user.displayName} - - - - - {edge.node.user.userName} - - - - - Orgs:  - {// Populate the user-orgs list. - edge.node.user.affiliations.edges.map((edge, i, arr) => { - if (arr.length - 1 === i) { - return ( - - {edge.node.organization.acronym} - - ) - } - return ( - - {edge.node.organization.acronym + ' | '} - - ) - })} - - - - - TwoFactor - - - Admin - - - { + return ( + { + history.push({ + pathname: '/user', + state: { detail: edge.node.userName }, + }) + }} + _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} + p="30px" + > + + + {edge.node.displayName} + + + + + {edge.node.userName} + + + + - - ) - }, - ) + > + TwoFactor +
+ + Admin + +
+ + + ) + }) : null} ) } + +/* + + Orgs:  + {// Populate the user-orgs list. + edge.node.user.affiliations.edges.map((edge, i, arr) => { + if (arr.length - 1 === i) { + return ( + + {edge.node.organization.acronym} + + ) + } + return ( + + {edge.node.organization.acronym + ' | '} + + ) + })} + +*/ diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index f7ce533240..de97229af0 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -45,74 +45,29 @@ describe('', () => { }, result: { data: { - user: { - affiliations: { - edges: [ - { - node: { - organization: { - acronym: 'TEST', - affiliatedUsers: { - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - startCursor: 'string', - endCursor: 'string', - }, - edges: [ - { - node: { - id: 'NzYzMzQ1MzQ1Ng==', - user: { - userName: 'testuser@testemail.ca', - displayName: 'Test User', - tfa: false, - affiliations: { - edges: [ - { - node: { - id: 'ODAyOTY1MDMyMQ==', - organization: { - acronym: 'GC', - }, - permission: 'SUPER_ADMIN', - }, - }, - { - node: { - id: 'NjI3NzcyNDQ=', - organization: { - acronym: 'BC', - }, - permission: 'USER_WRITE', - }, - }, - { - node: { - id: 'NjkyMzAyOTAx', - organization: { - acronym: 'BC', - }, - permission: 'ADMIN', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - ], + userList: { + organization: 'TEST', + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, }, + edges: [ + { + node: { + id: 'ODY0MDEzMTE1NA==', + userName: 'testuser@testemail.gc.ca', + admin: false, + tfa: false, + displayName: 'Test User', + }, + }, + ], }, }, }, }, ] + // Set the inital history item to user-list const { container, queryAllByRole, getByText } = render( @@ -145,69 +100,23 @@ describe('', () => { }, result: { data: { - user: { - affiliations: { - edges: [ - { - node: { - organization: { - acronym: 'TEST', - affiliatedUsers: { - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - startCursor: 'string', - endCursor: 'string', - }, - edges: [ - { - node: { - id: 'NzYzMzQ1MzQ1Ng==', - user: { - userName: 'testuser@testemail.ca', - displayName: 'Test User', - tfa: false, - affiliations: { - edges: [ - { - node: { - id: 'ODAyOTY1MDMyMQ==', - organization: { - acronym: 'GC', - }, - permission: 'SUPER_ADMIN', - }, - }, - { - node: { - id: 'NjI3NzcyNDQ=', - organization: { - acronym: 'BC', - }, - permission: 'USER_WRITE', - }, - }, - { - node: { - id: 'NjkyMzAyOTAx', - organization: { - acronym: 'BC', - }, - permission: 'ADMIN', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - ], + userList: { + organization: 'TEST', + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, }, + edges: [ + { + node: { + id: 'ODY0MDEzMTE1NA==', + userName: 'testuser@testemail.gc.ca', + admin: false, + tfa: false, + displayName: 'Test User', + }, + }, + ], }, }, }, @@ -262,51 +171,23 @@ describe('', () => { }, result: { data: { - user: { - affiliations: { - edges: [ - { - node: { - organization: { - acronym: 'TEST', - affiliatedUsers: { - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - startCursor: 'string', - endCursor: 'string', - }, - edges: [ - { - node: { - id: 'NzYzMzQ1MzQ1Ng==', - user: { - userName: 'testuser@testemail.ca', - displayName: 'Test User', - tfa: true, - affiliations: { - edges: [ - { - node: { - id: 'ODAyOTY1MDMyMQ==', - organization: { - acronym: 'GC', - }, - permission: 'SUPER_ADMIN', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - ], + userList: { + organization: 'TEST', + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, }, + edges: [ + { + node: { + id: 'ODY0MDEzMTE1NA==', + userName: 'testuser@testemail.gc.ca', + admin: true, + tfa: true, + displayName: 'Test User', + }, + }, + ], }, }, }, @@ -352,57 +233,28 @@ describe('', () => { }, result: { data: { - user: { - affiliations: { - edges: [ - { - node: { - organization: { - acronym: 'TEST', - affiliatedUsers: { - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - startCursor: 'string', - endCursor: 'string', - }, - edges: [ - { - node: { - id: 'NzYzMzQ1MzQ1Ng==', - user: { - userName: 'testuser@testemail.ca', - displayName: 'Test User', - tfa: false, - affiliations: { - edges: [ - { - node: { - id: 'ODAyOTY1MDMyMQ==', - organization: { - acronym: 'GC', - }, - permission: 'USER_READ', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }, - }, - ], + userList: { + organization: 'TEST', + pageInfo: { + hasNextPage: true, + hasPreviousPage: true, }, + edges: [ + { + node: { + id: 'ODY0MDEzMTE1NA==', + userName: 'testuser@testemail.gc.ca', + admin: false, + tfa: false, + displayName: 'Test User', + }, + }, + ], }, }, }, }, ] - const { container, queryAllByRole, getByText } = render( diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 3ed053db7b..7b37a6e20c 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -20,43 +20,19 @@ export const GENERATE_OTP_URL = gql` export const QUERY_USERLIST = gql` { - user { - affiliations { - edges { - node { - organization { - acronym - affiliatedUsers { - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - edges { - node { - id - user { - userName - displayName - tfa - affiliations { - edges { - node { - id - organization { - acronym - } - permission - } - } - } - } - } - } - } - } - } + userList(organizaion: "NS") { + organization + pageInfo { + hasNextPage + hasPreviousPage + } + edges { + node { + id + userName + admin + tfa + displayName } } } From fe81ba46ead82355c532d71a3604f5c14f51d8bf Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Wed, 8 Apr 2020 10:46:00 -0300 Subject: [PATCH 16/20] Add faker values for custom types --- frontend/schema.faker.graphql | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index 5d18a34210..f5cf8a25a4 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -1121,16 +1121,16 @@ type UserListItem implements Node { id: ID! """ The users email address or userName """ - userName: EmailAddress! + userName: EmailAddress! @fake(type: email) """ The users display name""" - displayName: String! + displayName: String! @fake(type: firstName) """ Indicates wether or not this user has enabled two factor authentication""" - tfa: Boolean! + tfa: Boolean! @examples(values: [true, false]) """ Indicates if this user is an admin of the organization specified in UserList query.""" - admin: Boolean! + admin: Boolean! @examples(values: [true, false]) } """ @@ -1138,22 +1138,22 @@ This custom gql object is used to populate a userPage component in the front end """ type UserPage{ """ The users email address or userName""" - userName: EmailAddress! + userName: EmailAddress! @fake(type: email) """ The users display name""" - displayName: String! + displayName: String! @fake(type: firstName) """ Indicates which organization this users data is being displayed for.""" - organization: Acronym! + organization: Acronym! @examples(values: ["GC", "ABC", "ASDF", "NSTIR", "BC"]) """ Indicates the preferred language of this user.""" - lang: String! + lang: String! @examples(values: ["English, "French]) """ Indicates wether or not this user has enabled two factor authentication""" - tfa: Boolean! + tfa: Boolean! @examples(values: [true, false]) """ Indicates if this user is an admin of the organization specified.""" - admin: Boolean! + admin: Boolean! @examples(values: [true, false]) } """A Relay edge containing a `UserItem` and its cursor.""" From ef99a724a2aff35263ba43733d92773832c5b963 Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Wed, 8 Apr 2020 10:51:46 -0300 Subject: [PATCH 17/20] Use custom object query. Bind admin value to checkbox. --- frontend/src/UserPage.js | 6 +++--- frontend/src/graphql/queries.js | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index 14a85a7408..3f2bfea43e 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -71,8 +71,8 @@ export function UserPage(props) { { window.alert('coming soon!!\n' + JSON.stringify(values, null, 2)) @@ -137,7 +137,7 @@ export function UserPage(props) { Administrative Account Account Active diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 7b37a6e20c..9386f8fc88 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -40,11 +40,14 @@ export const QUERY_USERLIST = gql` ` export const QUERY_USER = gql` - query User($userName: EmailAddress!) { - user(userName: $userName) { + query UserPage($userName: EmailAddress!) { + userPage(userName: $userName) { userName - displayName + tfa + organization + admin lang + displayName } } ` From 96a17d952282cb78e01d0b229a36fef2cd72c60c Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Thu, 9 Apr 2020 10:09:41 -0300 Subject: [PATCH 18/20] Add prop type string in userPage --- frontend/src/UserPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index 3f2bfea43e..58d840c55e 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -4,7 +4,7 @@ import React from 'react' import { Formik } from 'formik' import { useHistory } from 'react-router-dom' -//import { string } from 'prop-types' +import { string } from 'prop-types' import { Stack, @@ -137,7 +137,7 @@ export function UserPage(props) { Administrative Account Account Active From 15ca9eb4e510bf640710128fc5d481fe2733ed2f Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Thu, 9 Apr 2020 13:41:29 -0300 Subject: [PATCH 19/20] Make additional subtype for admin and org. Use new subtype in query. Update test and component to use new query. --- frontend/schema.faker.graphql | 25 +++++++++++++++---------- frontend/src/UserPage.js | 2 +- frontend/src/__tests__/UserPage.test.js | 4 ++++ frontend/src/graphql/queries.js | 6 ++++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frontend/schema.faker.graphql b/frontend/schema.faker.graphql index f5cf8a25a4..b2a7c9bc82 100644 --- a/frontend/schema.faker.graphql +++ b/frontend/schema.faker.graphql @@ -1133,6 +1133,15 @@ type UserListItem implements Node { admin: Boolean! @examples(values: [true, false]) } +"""A Relay edge containing a `UserItem` and its cursor.""" +type UserItemEdge { + """The item at the end of the edge""" + node: UserListItem! + + """A cursor for use in pagination""" + cursor: String! +} + """ This custom gql object is used to populate a userPage component in the front end. """ @@ -1143,9 +1152,6 @@ type UserPage{ """ The users display name""" displayName: String! @fake(type: firstName) - """ Indicates which organization this users data is being displayed for.""" - organization: Acronym! @examples(values: ["GC", "ABC", "ASDF", "NSTIR", "BC"]) - """ Indicates the preferred language of this user.""" lang: String! @examples(values: ["English, "French]) @@ -1153,14 +1159,13 @@ type UserPage{ tfa: Boolean! @examples(values: [true, false]) """ Indicates if this user is an admin of the organization specified.""" - admin: Boolean! @examples(values: [true, false]) + userAffiliations: [UserPageAffiliations]! } -"""A Relay edge containing a `UserItem` and its cursor.""" -type UserItemEdge { - """The item at the end of the edge""" - node: UserListItem! +type UserPageAffiliations{ + """ Indicates if this user is an admin of the organization""" + admin: Boolean! @examples(values: [true, false]) - """A cursor for use in pagination""" - cursor: String! + """ Indicates which organization this users data is being displayed for.""" + organization: Acronym! @examples(values: ["GC", "ABC", "ASDF", "NSTIR", "BC"]) } \ No newline at end of file diff --git a/frontend/src/UserPage.js b/frontend/src/UserPage.js index 58d840c55e..a83b1380ee 100644 --- a/frontend/src/UserPage.js +++ b/frontend/src/UserPage.js @@ -137,7 +137,7 @@ export function UserPage(props) { Administrative Account Account Active diff --git a/frontend/src/__tests__/UserPage.test.js b/frontend/src/__tests__/UserPage.test.js index 0cecfd7f02..0068242b51 100644 --- a/frontend/src/__tests__/UserPage.test.js +++ b/frontend/src/__tests__/UserPage.test.js @@ -30,6 +30,10 @@ describe('', () => { userName: 'testuser@testemail.gc.ca', displayName: 'Test User', lang: 'English', + userAffiliations: { + admin: true, + organization: 'TEST', + }, }, }, }, diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 9386f8fc88..dd18faf930 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -44,10 +44,12 @@ export const QUERY_USER = gql` userPage(userName: $userName) { userName tfa - organization - admin lang displayName + userAffiliations{ + admin + organization + } } } ` From af4a3f8dd6345b989ed2c68b1c46eb20109c9d6d Mon Sep 17 00:00:00 2001 From: Matt Peachey Date: Tue, 14 Apr 2020 08:04:01 -0300 Subject: [PATCH 20/20] Refactor userCard into its own component. Update UserList and UserList.test to accomodate these changes. Update UserCard Test. Add color check tests back to UserCard.test.js --- frontend/src/UserCard.js | 58 ++++++++++ frontend/src/UserList.js | 64 ++--------- frontend/src/__tests__/UserCard.test.js | 91 ++++++++++++++++ frontend/src/__tests__/UserList.test.js | 135 +----------------------- 4 files changed, 163 insertions(+), 185 deletions(-) create mode 100644 frontend/src/UserCard.js create mode 100644 frontend/src/__tests__/UserCard.test.js diff --git a/frontend/src/UserCard.js b/frontend/src/UserCard.js new file mode 100644 index 0000000000..5eea8f8325 --- /dev/null +++ b/frontend/src/UserCard.js @@ -0,0 +1,58 @@ +import React from 'react' + +import { Badge, Box, Text, PseudoBox } from '@chakra-ui/core' +import { Trans } from '@lingui/macro' + +import { useHistory } from 'react-router-dom' + +import { bool, string } from 'prop-types' + +export function UserCard(props) { + const history = useHistory() + return ( + { + history.push({ + pathname: '/user', + state: { detail: props.userName }, + }) + }} + _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} + p="30px" + > + + + {props.displayName} + + + + + {props.userName} + + + + + TwoFactor + + + Admin + + + + + ) +} + +UserCard.propTypes = { + displayName: string.isRequired, + userName: string.isRequired, + admin: bool.isRequired, + tfa: bool.isRequired, +} diff --git a/frontend/src/UserList.js b/frontend/src/UserList.js index a3c829322d..76a4065c46 100644 --- a/frontend/src/UserList.js +++ b/frontend/src/UserList.js @@ -1,28 +1,23 @@ import React from 'react' import { - Badge, Stack, SimpleGrid, Divider, - Box, - Text, Button, Icon, InputGroup, InputLeftElement, Input, - PseudoBox, } from '@chakra-ui/core' import { Trans } from '@lingui/macro' import { QUERY_USERLIST } from './graphql/queries' import { useQuery } from '@apollo/react-hooks' import { PaginationButtons } from './PaginationButtons' -import { useHistory } from 'react-router-dom' +import { UserCard } from './UserCard' export function UserList() { - const history = useHistory() // This function generates the URL when the page loads const { loading, error, data } = useQuery(QUERY_USERLIST) if (loading) { @@ -56,54 +51,13 @@ export function UserList() { {data ? data.userList.edges.map((edge) => { return ( - { - history.push({ - pathname: '/user', - state: { detail: edge.node.userName }, - }) - }} - _hover={{ borderColor: 'gray.200', bg: 'gray.200' }} - p="30px" - > - - - {edge.node.displayName} - - - - - {edge.node.userName} - - - - - TwoFactor - - - Admin - - - - + userName={edge.node.userName} + tfa={edge.node.tfa} + admin={edge.node.admin} + displayName={edge.node.displayName} + /> ) }) : null} @@ -115,7 +69,9 @@ export function UserList() { ) } -/* +/* -- Source code for adding organizations, not being used. -- + + Orgs:  {// Populate the user-orgs list. diff --git a/frontend/src/__tests__/UserCard.test.js b/frontend/src/__tests__/UserCard.test.js new file mode 100644 index 0000000000..5b7ec9385e --- /dev/null +++ b/frontend/src/__tests__/UserCard.test.js @@ -0,0 +1,91 @@ +import React from 'react' +import { i18n } from '@lingui/core' +import { render } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { ThemeProvider, theme } from '@chakra-ui/core' +import { I18nProvider } from '@lingui/react' +import { MockedProvider } from '@apollo/react-testing' + +import { UserCard } from '../UserCard' + +// If this unused import, the mocked data test fails. VERY weird. +import App from '../App' + +describe('', () => { + it('successfully renders', async () => { + const { container } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + }) + it('badges are green when TwoFactor and Admin values are true', async () => { + const { container, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + + const tfaBadge = getByText(/TwoFactor/i) + const adminBadge = getByText(/Admin/i) + + expect(tfaBadge).toBeDefined() + expect(adminBadge).toBeDefined() + + expect(tfaBadge).toHaveStyle('background-color: rgb(198, 246, 213)') + expect(adminBadge).toHaveStyle('background-color: rgb(198, 246, 213)') + }) + + it('badges are red when TwoFactor and Admin values are false', async () => { + const { container, getByText } = render( + + + + + + + + + , + ) + expect(container).toBeDefined() + + const tfaBadge = getByText(/TwoFactor/i) + const adminBadge = getByText(/Admin/i) + + expect(tfaBadge).toBeDefined() + expect(adminBadge).toBeDefined() + + expect(tfaBadge).toHaveStyle('background-color: rgb(254, 215, 215)') + expect(adminBadge).toHaveStyle('background-color: rgb(254, 215, 215)') + }) +}) diff --git a/frontend/src/__tests__/UserList.test.js b/frontend/src/__tests__/UserList.test.js index de97229af0..2da4791912 100644 --- a/frontend/src/__tests__/UserList.test.js +++ b/frontend/src/__tests__/UserList.test.js @@ -3,7 +3,6 @@ import { UserList } from '../UserList' import { i18n } from '@lingui/core' import { render, - cleanup, waitForElementToBeRemoved, fireEvent, waitFor, @@ -20,8 +19,6 @@ import { QUERY_USERLIST } from '../graphql/queries' import App from '../App' describe('', () => { - afterEach(cleanup) - it('successfully renders', async () => { const { container } = render( @@ -69,7 +66,7 @@ describe('', () => { ] // Set the inital history item to user-list - const { container, queryAllByRole, getByText } = render( + const { container, getAllByText, getByText } = render( @@ -88,7 +85,7 @@ describe('', () => { await waitForElementToBeRemoved(loadingElement) // Get all of the mocked user cards, and expect there to be only one entry. - const userCards = queryAllByRole('userCard') + const userCards = getAllByText('testuser@testemail.gc.ca') expect(userCards).toHaveLength(1) }) @@ -131,7 +128,7 @@ describe('', () => { }) // Set the inital history item to user-list - const { container, queryAllByRole, getByText } = render( + const { container, getAllByText, getByText } = render( @@ -150,7 +147,7 @@ describe('', () => { await waitForElementToBeRemoved(loadingElement) // Get all of the mocked user cards, and expect there to be only one entry. - const userCards = queryAllByRole('userCard') + const userCards = getAllByText('testuser@testemail.gc.ca') expect(userCards).toHaveLength(1) const leftClick = { button: 0 } @@ -162,128 +159,4 @@ describe('', () => { expect(history.location.pathname).toEqual('/user') }) }) - - it('badges are green when TwoFactor and Admin values are true', async () => { - const mocks = [ - { - request: { - query: QUERY_USERLIST, - }, - result: { - data: { - userList: { - organization: 'TEST', - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - }, - edges: [ - { - node: { - id: 'ODY0MDEzMTE1NA==', - userName: 'testuser@testemail.gc.ca', - admin: true, - tfa: true, - displayName: 'Test User', - }, - }, - ], - }, - }, - }, - }, - ] - - const { container, queryAllByRole, getByText } = render( - - - - - - - - - , - ) - expect(container).toBeDefined() - - expect(getByText('Loading...')).toBeInTheDocument() - const loadingElement = getByText('Loading...') - - await waitForElementToBeRemoved(loadingElement) - - // Get all of the mocked user cards, and expect there to be only one entry. - const userCards = queryAllByRole('userCard') - expect(userCards).toHaveLength(1) - - const tfaBadge = getByText(/TwoFactor/i) - const adminBadge = getByText(/Admin/i) - - expect(tfaBadge).toBeDefined() - expect(adminBadge).toBeDefined() - - expect(tfaBadge).toHaveStyle('background-color: rgb(198, 246, 213)') - expect(adminBadge).toHaveStyle('background-color: rgb(198, 246, 213)') - }) - it('badges are red when TwoFactor and Admin values are false', async () => { - const mocks = [ - { - request: { - query: QUERY_USERLIST, - }, - result: { - data: { - userList: { - organization: 'TEST', - pageInfo: { - hasNextPage: true, - hasPreviousPage: true, - }, - edges: [ - { - node: { - id: 'ODY0MDEzMTE1NA==', - userName: 'testuser@testemail.gc.ca', - admin: false, - tfa: false, - displayName: 'Test User', - }, - }, - ], - }, - }, - }, - }, - ] - const { container, queryAllByRole, getByText } = render( - - - - - - - - - , - ) - expect(container).toBeDefined() - - expect(getByText('Loading...')).toBeInTheDocument() - const loadingElement = getByText('Loading...') - - await waitForElementToBeRemoved(loadingElement) - - // Get all of the mocked user cards, and expect there to be only one entry. - const userCards = queryAllByRole('userCard') - expect(userCards).toHaveLength(1) - - const tfaBadge = getByText(/TwoFactor/i) - const adminBadge = getByText(/Admin/i) - - expect(tfaBadge).toBeDefined() - expect(adminBadge).toBeDefined() - - expect(tfaBadge).toHaveStyle('background-color: rgb(254, 215, 215)') - expect(adminBadge).toHaveStyle('background-color: rgb(254, 215, 215)') - }) })