diff --git a/package-lock.json b/package-lock.json index 0a2df255..ea061690 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.46.1", "babel-plugin-named-exports-order": "^0.0.2", + "core-js": "3.37.1", "eslint-plugin-prettier": "^3.4.0", "husky": "^7.0.1", "prettier": "^2.3.2", @@ -10501,22 +10502,22 @@ } }, "node_modules/@testing-library/dom": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.0.tgz", - "integrity": "sha512-6YWYPPpxG3e/xOo6HIWwB/58HukkwIVTOaZ0VwdMVjhRUX/01E4FtQbck9GazOOj7MXHc5RBzMrU86iBJHbI+A==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^5.0.0", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.4.4", + "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { @@ -10656,83 +10657,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.0.tgz", - "integrity": "sha512-Dffe68pGwI6WlLRYR2I0piIkyole9cSBH5jGQKCGMRpHW5RHCqAUaqc2Kv0tUyd4dU4DLPKhJIjyKOnjv4tuUw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "^5.0.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/react/node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true - }, - "node_modules/@testing-library/react/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/user-event": { "version": "14.4.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.4.3.tgz", @@ -10818,9 +10742,10 @@ } }, "node_modules/@types/aria-query": { - "version": "4.2.2", - "dev": true, - "license": "MIT" + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true }, "node_modules/@types/babel__core": { "version": "7.1.16", @@ -14954,9 +14879,9 @@ } }, "node_modules/core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "hasInstallScript": true, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index 8cd54391..2e00d1d1 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/parser": "^5.46.1", "babel-plugin-named-exports-order": "^0.0.2", + "core-js": "3.37.1", "eslint-plugin-prettier": "^3.4.0", "husky": "^7.0.1", "prettier": "^2.3.2", @@ -96,6 +97,9 @@ "stylelint-config-standard": "^22.0.0", "webpack": "^5.75.0" }, + "overrides": { + "@testing-library/dom": "^9.0.1" + }, "engines": { "node": ">=20.11.1" }, diff --git a/public/assets/images/ORCID-iD_icon-vector.svg b/public/assets/features/orcidlink/images/ORCID-iD_icon-vector.svg similarity index 100% rename from public/assets/images/ORCID-iD_icon-vector.svg rename to public/assets/features/orcidlink/images/ORCID-iD_icon-vector.svg diff --git a/public/assets/features/orcidlink/images/ORCID-sign-in.png b/public/assets/features/orcidlink/images/ORCID-sign-in.png new file mode 100644 index 00000000..f4274693 Binary files /dev/null and b/public/assets/features/orcidlink/images/ORCID-sign-in.png differ diff --git a/src/app/Routes.tsx b/src/app/Routes.tsx index 8482b84f..a475074d 100644 --- a/src/app/Routes.tsx +++ b/src/app/Routes.tsx @@ -26,6 +26,7 @@ import { usePageTracking, } from '../common/hooks'; import ORCIDLinkFeature from '../features/orcidlink'; +import ORCIDLinkCreateLink from '../features/orcidlink/CreateLink'; export const LOGIN_ROUTE = '/legacy/login'; export const ROOT_REDIRECT_ROUTE = '/narratives'; @@ -83,6 +84,9 @@ const Routes: FC = () => { } />} /> + + } />} /> + {/* IFrame Fallback Routes */} diff --git a/src/features/orcidlink/CreateLink/index.test.tsx b/src/features/orcidlink/CreateLink/index.test.tsx new file mode 100644 index 00000000..572fb967 --- /dev/null +++ b/src/features/orcidlink/CreateLink/index.test.tsx @@ -0,0 +1,135 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { createTestStore } from '../../../app/store'; +import { INITIAL_STORE_STATE } from '../test/data'; +import CreateLinkIndex from './index'; + +describe('The CreateLink component', () => { + const user = userEvent.setup(); + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + debugLogSpy = jest.spyOn(console, 'debug'); + }); + + async function expectAccordion( + container: HTMLElement, + titleText: string, + contentText: string + ) { + expect(container).toHaveTextContent(titleText); + + const faq1Content = await screen.findByText(new RegExp(contentText), { + exact: false, + }); + + expect(faq1Content).not.toBeVisible(); + + const faq1Title = await screen.findByText(new RegExp(titleText), { + exact: false, + }); + await user.click(faq1Title); + + await waitFor(() => { + expect(faq1Content).toBeVisible(); + }); + } + + it('renders placeholder content', () => { + const { container } = render( + + + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + }); + + it('cancel button returns to the ORCID Link home page', async () => { + const user = userEvent.setup(); + let fakeHomeCalled = false; + function FakeHome() { + fakeHomeCalled = true; + return
FAKE HOME
; + } + const { container } = render( + + + + } /> + } /> + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + + const cancelButton = await screen.findByText('Cancel'); + await user.click(cancelButton); + + await waitFor(() => { + expect(fakeHomeCalled).toBe(true); + }); + }); + + it('cancel button returns to the ORCID Link home page', async () => { + const user = userEvent.setup(); + const { container } = render( + + + + } /> + + + + ); + + expect(container).toHaveTextContent('Create Your KBase ORCID® Link'); + expect(container).toHaveTextContent('FAQs'); + expect(document.title).toBe('KBase: ORCID Link - Create Link'); + + const continueButton = await screen.findByText('Continue to ORCID®'); + await user.click(continueButton); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith( + 'WILL START THE LINKING PROCESS' + ); + }); + }); + + it('faq accordions are present and work', async () => { + const { container } = render( + + + + } /> + + + + ); + + expectAccordion( + container, + "What if I don't have an ORCID® Account", + "But what if you don't have an ORCID® account?" + ); + + expectAccordion( + container, + 'But I already log in with ORCID®', + 'Your ORCID® sign-in link is only used to obtain your ORCID® iD during sign-in' + ); + }); +}); diff --git a/src/features/orcidlink/CreateLink/index.tsx b/src/features/orcidlink/CreateLink/index.tsx new file mode 100644 index 00000000..499fea32 --- /dev/null +++ b/src/features/orcidlink/CreateLink/index.tsx @@ -0,0 +1,174 @@ +import { faArrowRight, faMailReply } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + Unstable_Grid2 as Grid, +} from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { usePageTitle } from '../../layout/layoutSlice'; +import { + ORCID_LABEL, + ORCID_LINK_LABEL, + ORCID_SIGN_IN_SCREENSHOT_URL, +} from '../constants'; +import styles from '../orcidlink.module.scss'; + +export default function ORCIDLinkCreateLink() { + usePageTitle('ORCID Link - Create Link'); + const navigate = useNavigate(); + return ( + + + + + + +

+ You do not currently have a link from your KBase account to an{' '} + {ORCID_LABEL} account. +

+ +

+ + After clicking the "Continue" button below, you will + be redirected to {ORCID_LABEL} + + , where you may sign in to your {ORCID_LABEL} account and grant + permission to KBase to access certain aspects of your{' '} + {ORCID_LABEL} account. +

+ +

+ What if you don't have an {ORCID_LABEL} Account?{' '} + Check out the FAQs to the right for an answer. +

+ +

+ After finishing at {ORCID_LABEL}, you will be returned to KBase + and asked to confirm the link. Once confirmed, the{' '} + {ORCID_LINK_LABEL} + will be added to your account. +

+ +

+ For security purposes, once you start a linking session, you + will have 10 minutes to complete the process. +

+ +

+ For more information,{' '} + + consult the {ORCID_LINK_LABEL} documentation + + . +

+
+ {/* Note that the card actions padding is overridden so that it matches + that of the card content and header. There are a number of formatting + issues with Cards. Some will apparently be fixed in v6. */} + + + + +
+
+ + + + + + + What if I don't have an {ORCID_LABEL} Account? + + +

+ In order to link your {ORCID_LABEL} account to your KBase + account, you will need to sign in at {ORCID_LABEL}. +

+

+ But what if you don't have an {ORCID_LABEL} account? +

+

+ When you reach the {ORCID_LABEL} Sign In page, you may elect + to register for a new account. +

+ ORCID® Sign In +

+ After registering, the linking process will be resumed, just + as if you had simply signed in with an existing{' '} + {ORCID_LABEL} account. +

+
+
+ + + But I already log in with {ORCID_LABEL} + + +

+ If you already log in with {ORCID_LABEL}, it may seem odd to + need to create a separate {ORCID_LINK_LABEL}. +

+

+ Your {ORCID_LABEL} sign-in link is only used to obtain your{' '} + {ORCID_LABEL} iD during sign-in. This is, in turn, used to + look up the associated KBase account and log you in. +

+

+ In contrast, {ORCID_LINK_LABEL} provides expanded and + long-term access, which allows KBase to provide tools for + you that that can access limited aspects of your{' '} + {ORCID_LABEL} account. The {ORCID_LINK_LABEL} can be added + or removed at any time without affecting your ability to + sign in to KBase through {ORCID_LABEL}. +

+
+
+
+
+
+
+
+ ); +} diff --git a/src/features/orcidlink/Home/index.test.tsx b/src/features/orcidlink/Home/index.test.tsx index 11f7c91f..8d0e2cd5 100644 --- a/src/features/orcidlink/Home/index.test.tsx +++ b/src/features/orcidlink/Home/index.test.tsx @@ -9,7 +9,7 @@ import { INITIAL_STORE_STATE, INITIAL_UNAUTHENTICATED_STORE_STATE, } from '../test/data'; -import { mockIsLinked, mockIsLinked_not } from '../test/mocks'; +import { mockIsLinkedNotResponse, mockIsLinkedResponse } from '../test/mocks'; import HomeController from './index'; jest.mock('../HomeLinked', () => { @@ -39,7 +39,7 @@ describe('The HomeController Component', () => { const body = await request.json(); switch (body['method']) { case 'is-linked': { - return mockIsLinked(body); + return mockIsLinkedResponse(body); } default: return ''; @@ -76,7 +76,7 @@ describe('The HomeController Component', () => { const body = await request.json(); switch (body['method']) { case 'is-linked': { - return mockIsLinked_not(body); + return mockIsLinkedNotResponse(body); } default: return ''; @@ -117,7 +117,7 @@ describe('The HomeController Component', () => { switch (body['method']) { case 'is-linked': { // In this mock, user "foo" is linked, user "bar" is not. - return mockIsLinked(body); + return mockIsLinkedResponse(body); } default: return ''; diff --git a/src/features/orcidlink/HomeLinked/ManageTab.test.tsx b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx new file mode 100644 index 00000000..b8b1ffc3 --- /dev/null +++ b/src/features/orcidlink/HomeLinked/ManageTab.test.tsx @@ -0,0 +1,208 @@ +import { screen } from '@testing-library/dom'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { noOp } from '../../common'; +import { LINK_RECORD_1, PROFILE_1 } from '../test/data'; +import ManageTab from './ManageTab'; + +describe('The ManageTab component', () => { + it('renders normally', () => { + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + const { container } = render( + + + + ); + + expect(container).toHaveTextContent('Remove your KBase ORCID'); + expect(container).toHaveTextContent( + 'Removing the link will not alter any of your data' + ); + expect(container).toHaveTextContent( + 'Please note that after you remove the link at KBase' + ); + }); + + it('cancel confirmation for removal of link', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + render( + + + + ); + + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + + expect(removeButton).toBeVisible(); + + await user.click(removeButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).toBeVisible(); + }); + + // Cancel the dialog + + const cancelButton = await screen.findByText('Cancel'); + + expect(cancelButton).toBeVisible(); + + await user.click(cancelButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).not.toBeVisible(); + }); + }); + + it('confirm removal of link', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + let removeLinkCalled = false; + const removeLink = () => { + removeLinkCalled = true; + }; + + render( + + + + ); + + expect(removeLinkCalled).toBe(false); + + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + + expect(removeButton).toBeVisible(); + + await user.click(removeButton); + + const dialog = await screen.findByText('Confirm Removal of ORCID® Link'); + + expect(dialog).toBeVisible(); + + // Confirm the dialog + + const yesButton = await screen.findByText( + 'Yes, go ahead and remove this link' + ); + + expect(yesButton).toBeVisible(); + + await user.click(yesButton); + + expect(removeLinkCalled).toBe(true); + }); + + it('confirmation dialog can be canceled', async () => { + const user = userEvent.setup(); + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + + const { container } = render( + + + + ); + + // First, show that the main content is displayed. + expect(container).toHaveTextContent('Remove your KBase ORCID® Link'); + + // Then open the dialog. + const removeButton = await screen.findByText('Remove KBase ORCID® Link …'); + expect(removeButton).toBeVisible(); + await user.click(removeButton); + const dialog = await screen.findByText('Confirm Removal of ORCID® Link'); + expect(dialog).toBeVisible(); + + // Cancel the dialog + const cancelButton = await screen.findByText('Cancel'); + expect(cancelButton).toBeVisible(); + await user.click(cancelButton); + + await waitFor(() => { + expect( + screen.queryByText('Confirm Removal of ORCID® Link') + ).not.toBeVisible(); + }); + + // And the main view should still be there. + expect(container).toHaveTextContent('Remove your KBase ORCID® Link'); + }); + + it('the "show in user profile" toggle calls the correct prop when clicked', async () => { + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + const orcidSiteURL = 'foo'; + let toggleValue = false; + const toggleShowInProfile = () => { + toggleValue = !toggleValue; + }; + + render( + + + + ); + + const toggleControl = await screen.findByText('Yes'); + + const user = userEvent.setup(); + + await user.click(toggleControl); + + await waitFor(() => { + expect(toggleValue).toBe(true); + }); + + await user.click(toggleControl); + + await waitFor(() => { + expect(toggleValue).toBe(false); + }); + }); +}); diff --git a/src/features/orcidlink/HomeLinked/ManageTab.tsx b/src/features/orcidlink/HomeLinked/ManageTab.tsx new file mode 100644 index 00000000..eb5312ba --- /dev/null +++ b/src/features/orcidlink/HomeLinked/ManageTab.tsx @@ -0,0 +1,148 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Button, + Card, + CardActions, + CardContent, + CardHeader, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Switch, + Typography, + Unstable_Grid2 as Grid, +} from '@mui/material'; +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + LinkRecordPublic, + ORCIDProfile, +} from '../../../common/api/orcidLinkCommon'; +import RealName from '../common/RealName'; + +export interface ManageTabProps { + linkRecord: LinkRecordPublic; + profile: ORCIDProfile; + orcidSiteURL: string; + removeLink: () => void; + toggleShowInProfile: () => void; +} + +export default function ManageTab({ + linkRecord, + profile, + orcidSiteURL, + removeLink, + toggleShowInProfile, +}: ManageTabProps) { + const [confirm, setConfirm] = useState(false); + + return ( + + + + + + + You may remove your KBase ORCID® Link at any time. + + +

+ Removing the link will not alter any of your data stored at KBase + or ORCID®. It will simply delete the link to your ORCID® account, + preventing KBase from accessing your ORCID® profile thereafter. +

+ +

+ Please note that after you remove the link at KBase, you may also + want to{' '} + + revoke the permissions granted to KBase at ORCID® + {' '} + as well. +

+
+ + + + Confirm Removal of ORCID® Link + +

Are you sure you want to remove this KBase ORCID® Link?

+

+ ORCID® iD is {linkRecord.orcid_auth.orcid} for{' '} + + + +

+
+ + + + +
+
+
+
+ + + + + + Show in User Profile? + + } + label="Yes" + onChange={() => { + toggleShowInProfile(); + }} + /> + + + When enabled your ORCID® iD will be displayed in{' '} + + your User Profile + + + + + + +
+ ); +} diff --git a/src/features/orcidlink/HomeLinked/OverviewTab.tsx b/src/features/orcidlink/HomeLinked/OverviewTab.tsx index 7a892a03..6571413b 100644 --- a/src/features/orcidlink/HomeLinked/OverviewTab.tsx +++ b/src/features/orcidlink/HomeLinked/OverviewTab.tsx @@ -10,6 +10,7 @@ import { LinkRecordPublic, ORCIDProfile, } from '../../../common/api/orcidLinkCommon'; +import MoreInformation from '../common/MoreInformation'; import LinkInfo from './LinkInfo'; export interface OverviewTabProps { @@ -39,16 +40,28 @@ export default function OverviewTab({ - + - NOTES HERE + + Your KBase ORCID® Link gives KBase tools access to your ORCID® + account while you are logged into KBase. + +

+ Your KBase ORCID® Link will be stored at KBase until you remove + it. +

+

+ The link will only be used when you are signed in to KBase. In + addition, any tool that uses the link will alert you before using + it, and will explain how it will use it. +

- LINKS TO MORE INFO HERE +
diff --git a/src/features/orcidlink/HomeLinked/index.test.tsx b/src/features/orcidlink/HomeLinked/index.test.tsx index 7e4edbc9..9a821ce7 100644 --- a/src/features/orcidlink/HomeLinked/index.test.tsx +++ b/src/features/orcidlink/HomeLinked/index.test.tsx @@ -1,119 +1,31 @@ import { render, screen, waitFor } from '@testing-library/react'; -import fetchMock, { MockResponseInit } from 'jest-fetch-mock'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; +import { ErrorBoundary } from 'react-error-boundary'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { createTestStore } from '../../../app/store'; import { INITIAL_STORE_STATE, - LINK_RECORD_1, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED, + INITIAL_UNAUTHENTICATED_STORE_STATE, PROFILE_1, SERVICE_INFO_1, } from '../test/data'; import { - jsonRPC20_ErrorResponse, - jsonRPC20_ResultResponse, - mockIsLinked, + setupMockRegularUser, + setupMockRegularUserWithError, } from '../test/mocks'; import HomeLinkedController from './index'; -function setupMockRegularUser() { - fetchMock.mockResponse( - async (request): Promise => { - const { pathname } = new URL(request.url); - // put a little delay in here so that we have a better - // chance of catching temporary conditions, like loading. - await new Promise((resolve) => { - setTimeout(() => { - resolve(null); - }, 300); - }); - switch (pathname) { - // Mocks for the orcidlink api - case '/services/orcidlink/api/v1': { - if (request.method !== 'POST') { - return ''; - } - const body = await request.json(); - const id = body['id']; - switch (body['method']) { - case 'is-linked': - // In this mock, user "foo" is linked, user "bar" is not. - return jsonRPC20_ResultResponse(id, mockIsLinked(body)); - case 'get-orcid-profile': - // simulate fetching an orcid profile - return jsonRPC20_ResultResponse(id, PROFILE_1); - case 'owner-link': - // simulate fetching the link record for a user - return jsonRPC20_ResultResponse(id, LINK_RECORD_1); - case 'info': - // simulate getting service info. - return jsonRPC20_ResultResponse(id, SERVICE_INFO_1); - default: - return ''; - } - } - default: - return ''; - } - } - ); -} - -function setupMockRegularUserWithError() { - fetchMock.mockResponse( - async (request): Promise => { - const { pathname } = new URL(request.url); - // put a little delay in here so that we have a better - // chance of catching temporary conditions, like loading. - await new Promise((resolve) => { - setTimeout(() => { - resolve(null); - }, 300); - }); - switch (pathname) { - // MOcks for the orcidlink api - case '/services/orcidlink/api/v1': { - if (request.method !== 'POST') { - return ''; - } - const body = await request.json(); - const id = body['id'] as string; - switch (body['method']) { - case 'is-linked': - return jsonRPC20_ErrorResponse( - id, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED - ); - case 'get-orcid-profile': { - return jsonRPC20_ErrorResponse( - id, - ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED - ); - } - case 'owner-link': - // simulate fetching the link record for a user - return jsonRPC20_ResultResponse(id, LINK_RECORD_1); - - case 'info': - // simulate getting service info - return jsonRPC20_ResultResponse(id, SERVICE_INFO_1); - - default: - return ''; - } - } - default: - return ''; - } - } - ); -} - describe('The HomeLinkedController component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); beforeEach(() => { fetchMock.resetMocks(); fetchMock.enableMocks(); + debugLogSpy = jest.spyOn(console, 'debug'); }); it('renders normally for a normal user', async () => { @@ -165,4 +77,89 @@ describe('The HomeLinkedController component', () => { await expect(container).toHaveTextContent('Authorization Required'); }); }); + + it('throws an impossible error if called without authentication', async () => { + const { container } = render( + { + return
{error.message}
; + }} + onError={() => { + // noop + }} + > + + + + + +
+ ); + + await waitFor(() => { + expect(container).toHaveTextContent( + 'Impossible - username is not defined' + ); + }); + }); + + it('responds as expected to the remove link button being pressed', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + + + ); + + // Now poke around and make sure things are there. + await waitFor(async () => { + expect(screen.queryByText('Loading ORCID Link')).toBeVisible(); + }); + + await waitFor(async () => { + // Ensure some expected fields are rendered. + expect( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + screen.queryByText(PROFILE_1.nameGroup.fields!.creditName!) + ).toBeVisible(); + expect(screen.queryByText('5/1/24')).toBeVisible(); + }); + + // First need to open the manage tab + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + await user.click(tab); + + await waitFor(() => { + expect( + screen.queryByText('Remove your KBase ORCID® Link') + ).not.toBeNull(); + }); + + // Now find and click the Remove button + const button = await screen.findByText('Remove KBase ORCID® Link …'); + expect(button).toBeVisible(); + await user.click(button); + + // Now the dialog should be displayed. + await waitFor(() => { + const title = screen.queryByText('Confirm Removal of ORCID® Link'); + expect(title).toBeVisible(); + }); + + const confirmButton = await screen.findByText( + 'Yes, go ahead and remove this link' + ); + expect(confirmButton).toBeVisible(); + await user.click(confirmButton); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('WILL REMOVE LINK'); + }); + }); }); diff --git a/src/features/orcidlink/HomeLinked/index.tsx b/src/features/orcidlink/HomeLinked/index.tsx index 95d34cac..209ed186 100644 --- a/src/features/orcidlink/HomeLinked/index.tsx +++ b/src/features/orcidlink/HomeLinked/index.tsx @@ -12,8 +12,11 @@ export interface HomeLinkedControllerProps { export default function HomeLinkedController({ info, }: HomeLinkedControllerProps) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const username = useAppSelector(authUsername)!; + const username = useAppSelector(authUsername); + + if (typeof username === 'undefined') { + throw new Error('Impossible - username is not defined'); + } const { data, error, isError, isFetching, isSuccess } = orcidlinkAPI.useOrcidlinkLinkedUserInfoQuery( @@ -21,6 +24,18 @@ export default function HomeLinkedController({ { refetchOnMountOrArgChange: true } ); + const removeLink = () => { + // This console output is only for this intermediate state of the code, to facilitate testing. + // eslint-disable-next-line no-console + console.debug('WILL REMOVE LINK'); + }; + + const toggleShowInProfile = () => { + // This console output is only for this intermediate state of the code, to facilitate testing. + // eslint-disable-next-line no-console + console.debug('TOGGLE SHOW IN PROFILE'); + }; + // Renderers function renderState() { if (isError) { @@ -28,7 +43,13 @@ export default function HomeLinkedController({ } else if (isSuccess) { const { linkRecord, profile } = data; return ( - + ); } } diff --git a/src/features/orcidlink/HomeLinked/view.test.tsx b/src/features/orcidlink/HomeLinked/view.test.tsx index 4f1b0801..4188cc44 100644 --- a/src/features/orcidlink/HomeLinked/view.test.tsx +++ b/src/features/orcidlink/HomeLinked/view.test.tsx @@ -1,34 +1,56 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router-dom'; +import { noOp } from '../../common'; import { LINK_RECORD_1, PROFILE_1, SERVICE_INFO_1 } from '../test/data'; import HomeLinked from './view'; describe('The HomeLinked Component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + debugLogSpy = jest.spyOn(console, 'debug'); + }); + it('renders with minimal props', async () => { const info = SERVICE_INFO_1; const linkRecord = LINK_RECORD_1; const profile = PROFILE_1; render( - + ); const creditName = 'Foo B. Bar'; const realName = 'Foo Bar'; - // Part of the profile should be available + // The profile fields should be displayed. expect(await screen.findByText(creditName)).toBeVisible(); expect(await screen.findByText(realName)).toBeVisible(); }); it('tab content switches when tabs are selected', async () => { + const user = userEvent.setup(); const info = SERVICE_INFO_1; const linkRecord = LINK_RECORD_1; const profile = PROFILE_1; - render( - + ); @@ -48,20 +70,65 @@ describe('The HomeLinked Component', () => { expect(await screen.findByText(creditName)).toBeVisible(); expect(await screen.findByText(realName)).toBeVisible(); - act(() => { - manageTab.click(); - }); + await user.click(manageTab); waitFor(async () => { expect(await screen.findByText(removeAreaTitle)).toBeVisible(); }); - act(() => { - overviewTab.click(); - }); + await user.click(overviewTab); waitFor(async () => { expect(await screen.findByText(overviewAreaTitle)).toBeVisible(); }); }); + + it('"show in user profile" toggle calls the function passed down to it', async () => { + const user = userEvent.setup(); + const info = SERVICE_INFO_1; + const linkRecord = LINK_RECORD_1; + const profile = PROFILE_1; + + const toggleShowInProfile = () => { + // eslint-disable-next-line no-console + console.debug('TOGGLE SHOW IN PROFILE'); + }; + + render( + + + + ); + + const creditName = 'Foo B. Bar'; + const removeAreaTitle = 'Remove your KBase ORCID® Link'; + const manageTabLabel = 'Manage Your Link'; + + const manageTab = await screen.findByText(manageTabLabel); + + // Part of the profile should be available on initial tab. + expect(await screen.findByText(creditName)).toBeVisible(); + + // Now select the manage tab + await user.click(manageTab); + + waitFor(async () => { + expect(await screen.findByText(removeAreaTitle)).toBeVisible(); + }); + + // And finally, click the toggle + const toggleControl = await screen.findByText('Yes'); + + await user.click(toggleControl); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('TOGGLE SHOW IN PROFILE'); + }); + }); }); diff --git a/src/features/orcidlink/HomeLinked/view.tsx b/src/features/orcidlink/HomeLinked/view.tsx index afe9b16e..fa7683ee 100644 --- a/src/features/orcidlink/HomeLinked/view.tsx +++ b/src/features/orcidlink/HomeLinked/view.tsx @@ -6,18 +6,23 @@ import { ORCIDProfile, } from '../../../common/api/orcidLinkCommon'; import TabPanel from '../common/TabPanel'; +import ManageTab from './ManageTab'; import OverviewTab from './OverviewTab'; export interface HomeLinkedProps { info: InfoResult; linkRecord: LinkRecordPublic; profile: ORCIDProfile; + removeLink: () => void; + toggleShowInProfile: () => void; } export default function HomeLinked({ info, linkRecord, profile, + removeLink, + toggleShowInProfile, }: HomeLinkedProps) { const [tab, setTab] = useState(0); @@ -37,7 +42,13 @@ export default function HomeLinked({ -
MANAGE TAB
+
); diff --git a/src/features/orcidlink/HomeUnlinked.tsx b/src/features/orcidlink/HomeUnlinked.tsx index 9d0580a3..0963db30 100644 --- a/src/features/orcidlink/HomeUnlinked.tsx +++ b/src/features/orcidlink/HomeUnlinked.tsx @@ -12,6 +12,8 @@ import { Typography, Unstable_Grid2 as Grid, } from '@mui/material'; +import { Link } from 'react-router-dom'; +import MoreInformation from './common/MoreInformation'; export default function Unlinked() { return ( @@ -19,6 +21,7 @@ export default function Unlinked() { + You do not currently have a link from your KBase account to an @@ -29,27 +32,46 @@ export default function Unlinked() { - + + + - + - NOTES HERE + + A KBase ORCID® Link gives KBase limited access to your ORCID® + account while you are logged into KBase. + +

+ If you don't have an ORCID® account, you may create one before + creating the link, or even "on the fly" while creating a link. +

+

+ You can only create a KBase ORCID® Link from this page. It will be + stored at KBase until you remove it. +

+

+ The link will only be used when you are signed in to KBase. In + addition, any tool that uses the link will alert you before using + it, and will explain how it will use it. +

+ - LINKS TO MORE INFORMATION HERE +
diff --git a/src/features/orcidlink/common/CreditName.test.tsx b/src/features/orcidlink/common/CreditName.test.tsx index 5f36f895..71c91536 100644 --- a/src/features/orcidlink/common/CreditName.test.tsx +++ b/src/features/orcidlink/common/CreditName.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; +import 'core-js/actual/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; import CreditName from './CreditName'; diff --git a/src/features/orcidlink/common/LoadingOverlay.module.scss b/src/features/orcidlink/common/LoadingOverlay.module.scss index 6958b70b..8c476927 100644 --- a/src/features/orcidlink/common/LoadingOverlay.module.scss +++ b/src/features/orcidlink/common/LoadingOverlay.module.scss @@ -8,6 +8,6 @@ margin-top: 8rem; } -.loading-title { +.title { margin-left: 0.5rem; } diff --git a/src/features/orcidlink/common/LoadingOverlay.tsx b/src/features/orcidlink/common/LoadingOverlay.tsx index 1b436c12..b47184af 100644 --- a/src/features/orcidlink/common/LoadingOverlay.tsx +++ b/src/features/orcidlink/common/LoadingOverlay.tsx @@ -1,28 +1,6 @@ import { Alert, AlertTitle, CircularProgress, Modal } from '@mui/material'; import styles from './LoadingOverlay.module.scss'; -export interface LoadingAlertProps { - title: string; - description: string; -} - -/** - * A wrapper around MUI Alert to show a loading indicator (spinner) and message, - * with a description. - */ -export function LoadingAlert({ title, description }: LoadingAlertProps) { - return ( -
- }> - - {title} - -

{description}

-
-
- ); -} - export interface LoadingOverlayProps { open: boolean; } @@ -34,7 +12,14 @@ export interface LoadingOverlayProps { export default function LoadingOverlay({ open }: LoadingOverlayProps) { return ( - +
+ }> + + Loading... + +

Loading ORCID Link

+
+
); } diff --git a/src/features/orcidlink/common/MoreInformation.tsx b/src/features/orcidlink/common/MoreInformation.tsx new file mode 100644 index 00000000..e2e526ec --- /dev/null +++ b/src/features/orcidlink/common/MoreInformation.tsx @@ -0,0 +1,34 @@ +import { List, ListItem, ListItemText } from '@mui/material'; + +/** + * Implements a list of information resources about ORCID and KBase ORCID linking, + * each with a link and short description. + */ +export default function MoreInformation() { + return ( + + + + About KBase ORCID® Links + + } + /> + + + + About ORCID® + + } + /> + + + ); +} diff --git a/src/features/orcidlink/common/ORCIDIdLink.test.tsx b/src/features/orcidlink/common/ORCIDIdLink.test.tsx index 00eec477..c3fb831d 100644 --- a/src/features/orcidlink/common/ORCIDIdLink.test.tsx +++ b/src/features/orcidlink/common/ORCIDIdLink.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; import { ORCIDIdLink } from './ORCIDIdLink'; describe('The renderORCIDId render function', () => { diff --git a/src/features/orcidlink/common/ORCIDIdLink.tsx b/src/features/orcidlink/common/ORCIDIdLink.tsx index 59e9cfb0..9e2ec914 100644 --- a/src/features/orcidlink/common/ORCIDIdLink.tsx +++ b/src/features/orcidlink/common/ORCIDIdLink.tsx @@ -1,4 +1,4 @@ -import { image } from '../images'; +import { ORCID_ICON_URL } from '../constants'; import styles from './ORCIDIdLink.module.scss'; export interface ORCIDIdLinkProps { @@ -18,7 +18,7 @@ export function ORCIDIdLink({ url, orcidId }: ORCIDIdLinkProps) { rel="noreferrer" className={styles.main} > - ORCID Icon + ORCID Icon {url}/{orcidId} ); diff --git a/src/features/orcidlink/common/RealName.test.tsx b/src/features/orcidlink/common/RealName.test.tsx index c5d95fcc..f7af59ed 100644 --- a/src/features/orcidlink/common/RealName.test.tsx +++ b/src/features/orcidlink/common/RealName.test.tsx @@ -1,5 +1,5 @@ import { render } from '@testing-library/react'; -import 'core-js/stable/structured-clone'; +import 'core-js/actual/structured-clone'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import { PROFILE_1 } from '../test/data'; import RealName from './RealName'; @@ -15,6 +15,8 @@ describe('the renderRealName render function ', () => { it('renders just the first name if no last name', () => { const profile = structuredClone(PROFILE_1); + + // We know how the test profile is populated. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion profile.nameGroup.fields!.lastName = null; diff --git a/src/features/orcidlink/common/RealName.tsx b/src/features/orcidlink/common/RealName.tsx index cc376ffb..3043c66f 100644 --- a/src/features/orcidlink/common/RealName.tsx +++ b/src/features/orcidlink/common/RealName.tsx @@ -1,4 +1,3 @@ -import { Typography } from '@mui/material'; import { ORCIDProfile } from '../../../common/api/orcidLinkCommon'; import PrivateField from './PrivateField'; @@ -17,10 +16,10 @@ export default function RealName({ profile }: RealNameProps) { const { firstName, lastName } = profile.nameGroup.fields; if (lastName) { return ( - + {firstName} {lastName} - + ); } - return {firstName}; + return {firstName}; } diff --git a/src/features/orcidlink/common/Scopes.test.tsx b/src/features/orcidlink/common/Scopes.test.tsx index 9e3a3bb9..a3f928ed 100644 --- a/src/features/orcidlink/common/Scopes.test.tsx +++ b/src/features/orcidlink/common/Scopes.test.tsx @@ -1,4 +1,5 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import Scopes from './Scopes'; describe('The Scopes component', () => { @@ -11,15 +12,17 @@ describe('The Scopes component', () => { }); it('renders scope description when scope is selected', async () => { + const user = userEvent.setup(); const scopes = '/read-limited'; + render(); + const buttonText = 'Allows KBase to read your information with visibility set to Trusted Organizations.'; const button = await screen.findByText(buttonText); expect(button).toBeVisible(); - act(() => { - button.click(); - }); + await user.click(button); + const revealedContentSample = 'Allows KBase to read any information from your record'; waitFor(async () => { diff --git a/src/features/orcidlink/constants.ts b/src/features/orcidlink/constants.ts index 94e77506..4724bf42 100644 --- a/src/features/orcidlink/constants.ts +++ b/src/features/orcidlink/constants.ts @@ -66,3 +66,14 @@ export const SCOPE_HELP: { [K in ORCIDScope]: ScopeHelp } = { ], }, }; + +function image_url(filename: string): string { + return `${process.env.PUBLIC_URL}/assets/features/orcidlink/images/${filename}`; +} + +export const ORCID_ICON_URL = image_url('ORCID-iD_icon-vector.svg'); +export const ORCID_SIGN_IN_SCREENSHOT_URL = image_url('ORCID-sign-in.png'); + +export const ORCID_LABEL = 'ORCID®'; + +export const ORCID_LINK_LABEL = `KBase ${ORCID_LABEL} Link`; diff --git a/src/features/orcidlink/images.ts b/src/features/orcidlink/images.ts deleted file mode 100644 index c6aa70c3..00000000 --- a/src/features/orcidlink/images.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Constructs urls for images used in the orcidlink ui - * - * Note that the usual technique, importing the image, which creates a url back - * into the web app, does not work with the current state of Europa dependencies. - * Therefore, we use hardcoded image paths, combined with the PUBLIC_URL. - * When Europa deps are updated, or it is switched to vite, the image imports - * should be re-enabled. - */ - -const ORCID_ICON = '/assets/images/ORCID-iD_icon-vector.svg'; - -export type ImageName = 'orcidIcon'; - -export function image(name: ImageName): string { - switch (name) { - case 'orcidIcon': - return `${process.env.PUBLIC_URL}${ORCID_ICON}`; - } -} diff --git a/src/features/orcidlink/index.test.tsx b/src/features/orcidlink/index.test.tsx new file mode 100644 index 00000000..72b496a7 --- /dev/null +++ b/src/features/orcidlink/index.test.tsx @@ -0,0 +1,110 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'jest-fetch-mock'; +import { Provider } from 'react-redux'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { createTestStore } from '../../app/store'; +import MainView from './index'; +import { INITIAL_STORE_STATE } from './test/data'; +import { setupMockRegularUser } from './test/mocks'; + +describe('The Main Component', () => { + let debugLogSpy: jest.SpyInstance; + beforeEach(() => { + jest.resetAllMocks(); + }); + beforeEach(() => { + fetchMock.resetMocks(); + fetchMock.enableMocks(); + debugLogSpy = jest.spyOn(console, 'debug'); + }); + + it('renders with minimal props', async () => { + setupMockRegularUser(); + + render( + + + + } /> + + {/* */} + + + ); + + const creditName = 'Foo B. Bar'; + const realName = 'Foo Bar'; + + // Part of the profile should be available + expect(await screen.findByText(creditName)).toBeVisible(); + expect(await screen.findByText(realName)).toBeVisible(); + }); + + it('can switch to the "manage your link" tab', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + } /> + + + + ); + + // Matches test data (see the setup function above) + const creditName = 'Foo B. Bar'; + // Matches what would be synthesized from the test data + const realName = 'Foo Bar'; + + // Part of the profile should be available + expect(await screen.findByText(creditName)).toBeVisible(); + expect(await screen.findByText(realName)).toBeVisible(); + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + + await user.click(tab); + + await waitFor(() => { + expect( + screen.queryByText('Remove your KBase ORCID® Link') + ).not.toBeNull(); + expect(screen.queryByText('Settings')).not.toBeNull(); + }); + }); + + it('the "Show in User Profile?" switch calls the prop function we pass', async () => { + const user = userEvent.setup(); + setupMockRegularUser(); + + render( + + + + } /> + + + + ); + + const tab = await screen.findByText('Manage Your Link'); + expect(tab).not.toBeNull(); + await user.click(tab); + + await waitFor(() => { + expect(screen.queryByText('Settings')).not.toBeNull(); + }); + + const toggleControl = await screen.findByText('Yes'); + + await user.click(toggleControl); + + await waitFor(() => { + expect(debugLogSpy).toHaveBeenCalledWith('TOGGLE SHOW IN PROFILE'); + }); + }); +}); diff --git a/src/features/orcidlink/test/mocks.ts b/src/features/orcidlink/test/mocks.ts index 1e515560..d8ab8956 100644 --- a/src/features/orcidlink/test/mocks.ts +++ b/src/features/orcidlink/test/mocks.ts @@ -1,6 +1,13 @@ +import { MockResponseInit } from 'jest-fetch-mock/types'; import { JSONRPC20Error } from '../../../common/api/utils/kbaseBaseQuery'; +import { + LINK_RECORD_1, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED, + PROFILE_1, + SERVICE_INFO_1, +} from './data'; -export function jsonRPC20_ResultResponse(id: string, result: unknown) { +export function jsonrpc20_resultResponse(id: string, result: unknown) { return { body: JSON.stringify({ jsonrpc: '2.0', @@ -14,7 +21,7 @@ export function jsonRPC20_ResultResponse(id: string, result: unknown) { }; } -export function jsonRPC20_ErrorResponse(id: string, error: JSONRPC20Error) { +export function jsonrpc20_errorResponse(id: string, error: JSONRPC20Error) { return { body: JSON.stringify({ jsonrpc: '2.0', @@ -29,7 +36,7 @@ export function jsonRPC20_ErrorResponse(id: string, error: JSONRPC20Error) { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function rest_response(result: any, status = 200) { +export function restResponse(result: any, status = 200) { return { body: JSON.stringify(result), status, @@ -40,7 +47,7 @@ export function rest_response(result: any, status = 200) { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mockIsLinked(body: any) { +export function mockIsLinkedResponse(body: any) { const username = body['params']['username']; const result = (() => { @@ -53,10 +60,103 @@ export function mockIsLinked(body: any) { throw new Error('Invalid test value for username'); } })(); - return jsonRPC20_ResultResponse(body['id'], result); + return jsonrpc20_resultResponse(body['id'], result); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function mockIsLinked_not(body: any) { - return jsonRPC20_ResultResponse(body['id'], false); +export function mockIsLinkedNotResponse(body: any) { + return jsonrpc20_resultResponse(body['id'], false); +} + +export function setupMockRegularUser() { + fetchMock.mockResponse( + async (request): Promise => { + const { pathname } = new URL(request.url); + // put a little delay in here so that we have a better + // chance of catching temporary conditions, like loading. + await new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 300); + }); + switch (pathname) { + // Mocks for the orcidlink api + case '/services/orcidlink/api/v1': { + if (request.method !== 'POST') { + return ''; + } + const body = await request.json(); + const id = body['id']; + switch (body['method']) { + case 'is-linked': + // In this mock, user "foo" is linked, user "bar" is not. + return jsonrpc20_resultResponse(id, mockIsLinkedResponse(body)); + case 'get-orcid-profile': + // simulate fetching an orcid profile + return jsonrpc20_resultResponse(id, PROFILE_1); + case 'owner-link': + // simulate fetching the link record for a user + return jsonrpc20_resultResponse(id, LINK_RECORD_1); + case 'info': + // simulate getting service info. + return jsonrpc20_resultResponse(id, SERVICE_INFO_1); + default: + return ''; + } + } + default: + return ''; + } + } + ); +} + +export function setupMockRegularUserWithError() { + fetchMock.mockResponse( + async (request): Promise => { + const { pathname } = new URL(request.url); + // put a little delay in here so that we have a better + // chance of catching temporary conditions, like loading. + await new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 300); + }); + switch (pathname) { + // Mocks for the orcidlink api + case '/services/orcidlink/api/v1': { + if (request.method !== 'POST') { + return ''; + } + const body = await request.json(); + const id = body['id'] as string; + switch (body['method']) { + case 'is-linked': + return jsonrpc20_errorResponse( + id, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED + ); + case 'get-orcid-profile': { + return jsonrpc20_errorResponse( + id, + ORCIDLINK_IS_LINKED_AUTHORIZATION_REQUIRED + ); + } + case 'owner-link': + // simulate fetching the link record for a user + return jsonrpc20_resultResponse(id, LINK_RECORD_1); + + case 'info': + // simulate getting service info + return jsonrpc20_resultResponse(id, SERVICE_INFO_1); + + default: + return ''; + } + } + default: + return ''; + } + } + ); }