From 6ade2c3ac877e76901d4a82cd9bd7c178f1fa164 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 28 Oct 2024 12:51:09 -0400 Subject: [PATCH 1/8] feat:current api usecase integration --- src/sections/account/Account.tsx | 9 +- .../api-token-section/ApiTokenSection.tsx | 85 +++++++++++++--- .../ApiTokenSectionSkeleton.tsx | 51 ++++++++++ .../useGetCurrentApiToken.tsx | 43 +++++++++ .../api-token-section/useRecreateApiToken.tsx | 47 +++++++++ .../api-token-section/useRevokeApiToken.tsx | 30 ++++++ src/shared/helpers/DateHelper.ts | 3 + src/users/domain/models/TokenInfo.ts | 4 + .../repositories/ApiTokenInfoRepository.tsx | 7 ++ .../domain/useCases/getCurrentApiToken.ts | 6 ++ src/users/domain/useCases/recreateApiToken.ts | 6 ++ src/users/domain/useCases/revokeApiToken.ts | 5 + .../ApiTokenInfoJSDataverseRepository.tsx | 37 +++++++ .../sections/account/ApiTokenSection.spec.tsx | 57 ++++++++++- .../account/useGetApiTokenInfo.spec.tsx | 69 +++++++++++++ .../account/useRecreateApiTokenInfo.spec.tsx | 96 +++++++++++++++++++ .../account/useRevokeApiTokenInfo.spec.tsx | 56 +++++++++++ .../ApiTokenInfoJSDataverseRepository.spec.ts | 36 +++++++ 18 files changed, 626 insertions(+), 21 deletions(-) create mode 100644 src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx create mode 100644 src/sections/account/api-token-section/useGetCurrentApiToken.tsx create mode 100644 src/sections/account/api-token-section/useRecreateApiToken.tsx create mode 100644 src/sections/account/api-token-section/useRevokeApiToken.tsx create mode 100644 src/users/domain/models/TokenInfo.ts create mode 100644 src/users/domain/repositories/ApiTokenInfoRepository.tsx create mode 100644 src/users/domain/useCases/getCurrentApiToken.ts create mode 100644 src/users/domain/useCases/recreateApiToken.ts create mode 100644 src/users/domain/useCases/revokeApiToken.ts create mode 100644 src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx create mode 100644 tests/component/sections/account/useGetApiTokenInfo.spec.tsx create mode 100644 tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx create mode 100644 tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx create mode 100644 tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 06d5a9fbb..298f15b8e 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -6,11 +6,7 @@ import { AccountHelper, AccountPanelTabKey } from './AccountHelper' import { ApiTokenSection } from './api-token-section/ApiTokenSection' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import styles from './Account.module.scss' -import { - DvObjectType, - UpwardHierarchyNode -} from '../../shared/hierarchy/domain/models/UpwardHierarchyNode' -import { ROOT_COLLECTION_ALIAS } from '../../collection/domain/models/Collection' +import { ApiTokenInfoJSDataverseRepository } from '@/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository' const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS @@ -21,6 +17,7 @@ interface AccountProps { export const Account = ({ defaultActiveTabKey }: AccountProps) => { const { t } = useTranslation('account') const { setIsLoading } = useLoading() + const repository = new ApiTokenInfoJSDataverseRepository() const rootHierarchy = new UpwardHierarchyNode( 'Root', @@ -55,7 +52,7 @@ export const Account = ({ defaultActiveTabKey }: AccountProps) => {
- +
diff --git a/src/sections/account/api-token-section/ApiTokenSection.tsx b/src/sections/account/api-token-section/ApiTokenSection.tsx index 7ead9d935..e9f2c7a4b 100644 --- a/src/sections/account/api-token-section/ApiTokenSection.tsx +++ b/src/sections/account/api-token-section/ApiTokenSection.tsx @@ -2,22 +2,80 @@ import { Trans, useTranslation } from 'react-i18next' import { Button } from '@iqss/dataverse-design-system' import accountStyles from '../Account.module.scss' import styles from './ApiTokenSection.module.scss' +import { useEffect, useState } from 'react' +import { Alert } from '@iqss/dataverse-design-system' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import ApiTokenSectionSkeleton from './ApiTokenSectionSkeleton' +import { useGetApiToken } from './useGetCurrentApiToken' +import { useRecreateApiToken } from './useRecreateApiToken' +import { useRevokeApiToken } from './useRevokeApiToken' +interface ApiTokenSectionProps { + repository: ApiTokenInfoRepository +} -export const ApiTokenSection = () => { +export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { const { t } = useTranslation('account', { keyPrefix: 'apiToken' }) + const [currentApiTokenInfo, setCurrentApiTokenInfo] = useState() + + const { error, apiTokenInfo, isLoading } = useGetApiToken(repository) + + const getError = + error !== 'There was an error when reading the resource. Reason was: [404] Token not found.' + ? error + : null + + useEffect(() => { + setCurrentApiTokenInfo(apiTokenInfo) + }, [apiTokenInfo]) + + const { + initiateRecreateToken, + isRecreating, + error: recreatingError, + apiTokenInfo: updatedTokenInfo + } = useRecreateApiToken(repository) + + useEffect(() => { + if (updatedTokenInfo) { + setCurrentApiTokenInfo(updatedTokenInfo) + } + }, [updatedTokenInfo]) + + const handleCreateToken = () => { + initiateRecreateToken() + } - // TODO: When we have the use cases we need to mock stub to unit test this with or without token - const apiToken = '999fff-666rrr-this-is-not-a-real-token-123456' - const expirationDate = '2025-09-04' + const { revokeToken, isRevoking, error: revokingError } = useRevokeApiToken(repository) + + const handleRevokeToken = async () => { + await revokeToken() + setCurrentApiTokenInfo({ + apiToken: '', + expirationDate: '' + }) + } const copyToClipboard = () => { - navigator.clipboard.writeText(apiToken).catch( + navigator.clipboard.writeText(apiTokenInfo.apiToken).catch( /* istanbul ignore next */ (error) => { console.error('Failed to copy text:', error) } ) } + if (isLoading || isRecreating || isRevoking) { + return + } + + if (getError || recreatingError || revokingError) { + return ( + + {getError || recreatingError || revokingError} + + ) + } + return ( <>

@@ -35,22 +93,25 @@ export const ApiTokenSection = () => { }} />

- {apiToken ? ( + {currentApiTokenInfo?.apiToken ? ( <>

- {t('expirationDate')} + {t('expirationDate')}{' '} +

- {apiToken} + {currentApiTokenInfo.apiToken}
- -
@@ -60,8 +121,8 @@ export const ApiTokenSection = () => {
{t('notCreatedApiToken')}
-
-
diff --git a/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx b/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx new file mode 100644 index 000000000..073687f1f --- /dev/null +++ b/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx @@ -0,0 +1,51 @@ +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' +import { Trans, useTranslation } from 'react-i18next' +import accountStyles from '../Account.module.scss' +import { Button } from '@iqss/dataverse-design-system' +import styles from './ApiTokenSection.module.scss' + +const ApiTokenSectionSkeleton = () => { + const { t } = useTranslation('account', { keyPrefix: 'apiToken' }) + + return ( + <> +

+ + ) + }} + /> +

+ +
+

+ {t('expirationDate')}{' '} + +

+
+ + + +
+
+ + + +
+
+
+ + ) +} +export default ApiTokenSectionSkeleton diff --git a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx new file mode 100644 index 000000000..78dfd2ea0 --- /dev/null +++ b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx @@ -0,0 +1,43 @@ +import { useState, useEffect, useCallback } from 'react' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { getCurrentApiToken } from '@/users/domain/useCases/getCurrentApiToken' + +interface UseGetApiTokenResult { + apiTokenInfo: TokenInfo + isLoading: boolean + error: string | null +} + +export const useGetApiToken = (repository: ApiTokenInfoRepository): UseGetApiTokenResult => { + const [apiTokenInfo, setApiTokenInfo] = useState({ + apiToken: '', + expirationDate: '' + }) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchTokenInfo = useCallback(async () => { + try { + setIsLoading(true) + const tokenInfo = await getCurrentApiToken(repository) + setApiTokenInfo({ + apiToken: tokenInfo.apiToken, + expirationDate: tokenInfo.expirationDate + }) + setError(null) + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch API token.' + console.error(errorMessage) + setError(errorMessage) + } finally { + setIsLoading(false) + } + }, [repository]) + + useEffect(() => { + void fetchTokenInfo() + }, [fetchTokenInfo]) + + return { error, apiTokenInfo, isLoading } +} diff --git a/src/sections/account/api-token-section/useRecreateApiToken.tsx b/src/sections/account/api-token-section/useRecreateApiToken.tsx new file mode 100644 index 000000000..eb0711597 --- /dev/null +++ b/src/sections/account/api-token-section/useRecreateApiToken.tsx @@ -0,0 +1,47 @@ +import { useState, useEffect } from 'react' +import { recreateApiToken } from '@/users/domain/useCases/recreateApiToken' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' + +interface UseRecreateApiTokenResult { + initiateRecreateToken: () => void + isRecreating: boolean + error: string | null + apiTokenInfo: TokenInfo | null +} + +export const useRecreateApiToken = ( + repository: ApiTokenInfoRepository +): UseRecreateApiTokenResult => { + const [isRecreating, setIsRecreating] = useState(false) + const [error, setError] = useState(null) + const [apiTokenInfo, setApiTokenInfo] = useState(null) + const [shouldRecreate, setShouldRecreate] = useState(false) + + const initiateRecreateToken = () => { + setShouldRecreate(true) + } + + useEffect(() => { + const recreateToken = async () => { + setIsRecreating(true) + setError(null) + + try { + const newTokenInfo = await recreateApiToken(repository) + setApiTokenInfo(newTokenInfo) + } catch (err) { + console.error('Error recreating token:', err) + setError('Failed to recreate API token.') + } finally { + setIsRecreating(false) + setShouldRecreate(false) + } + } + if (shouldRecreate) { + void recreateToken() + } + }, [shouldRecreate, repository]) + + return { initiateRecreateToken, isRecreating, error, apiTokenInfo } +} diff --git a/src/sections/account/api-token-section/useRevokeApiToken.tsx b/src/sections/account/api-token-section/useRevokeApiToken.tsx new file mode 100644 index 000000000..618ed0ec8 --- /dev/null +++ b/src/sections/account/api-token-section/useRevokeApiToken.tsx @@ -0,0 +1,30 @@ +import { useState, useCallback } from 'react' +import { revokeApiToken } from '@/users/domain/useCases/revokeApiToken' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' + +interface UseRevokeApiTokenResult { + revokeToken: () => Promise + isRevoking: boolean + error: string | null +} + +export const useRevokeApiToken = (repository: ApiTokenInfoRepository): UseRevokeApiTokenResult => { + const [isRevoking, setIsRevoking] = useState(false) + const [error, setError] = useState(null) + + const revokeToken = useCallback(async () => { + setIsRevoking(true) + setError(null) + + try { + await revokeApiToken(repository) + } catch (err) { + console.error('There was an error revoking Api token:', err) + setError('Failed to revoke API token.') + } finally { + setIsRevoking(false) + } + }, [repository]) + + return { revokeToken, isRevoking, error } +} diff --git a/src/shared/helpers/DateHelper.ts b/src/shared/helpers/DateHelper.ts index 44ffacf85..f842cd76d 100644 --- a/src/shared/helpers/DateHelper.ts +++ b/src/shared/helpers/DateHelper.ts @@ -20,4 +20,7 @@ export class DateHelper { day: '2-digit' }) } + static toISO8601Format(date: Date): string { + return date.toISOString().split('T')[0] + } } diff --git a/src/users/domain/models/TokenInfo.ts b/src/users/domain/models/TokenInfo.ts new file mode 100644 index 000000000..761a983c3 --- /dev/null +++ b/src/users/domain/models/TokenInfo.ts @@ -0,0 +1,4 @@ +export interface TokenInfo { + apiToken: string + expirationDate: string +} diff --git a/src/users/domain/repositories/ApiTokenInfoRepository.tsx b/src/users/domain/repositories/ApiTokenInfoRepository.tsx new file mode 100644 index 000000000..1e53d0b55 --- /dev/null +++ b/src/users/domain/repositories/ApiTokenInfoRepository.tsx @@ -0,0 +1,7 @@ +import { TokenInfo } from '../.././domain/models/TokenInfo' + +export interface ApiTokenInfoRepository { + getCurrentApiToken(): Promise + recreateApiToken(): Promise + deleteApiToken(): Promise +} diff --git a/src/users/domain/useCases/getCurrentApiToken.ts b/src/users/domain/useCases/getCurrentApiToken.ts new file mode 100644 index 000000000..4ae7da442 --- /dev/null +++ b/src/users/domain/useCases/getCurrentApiToken.ts @@ -0,0 +1,6 @@ +import { TokenInfo } from '../models/TokenInfo' +import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' + +export function getCurrentApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { + return apiTokenRepository.getCurrentApiToken() +} diff --git a/src/users/domain/useCases/recreateApiToken.ts b/src/users/domain/useCases/recreateApiToken.ts new file mode 100644 index 000000000..a61992e7f --- /dev/null +++ b/src/users/domain/useCases/recreateApiToken.ts @@ -0,0 +1,6 @@ +import { TokenInfo } from '../models/TokenInfo' +import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' + +export function recreateApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { + return apiTokenRepository.recreateApiToken() +} diff --git a/src/users/domain/useCases/revokeApiToken.ts b/src/users/domain/useCases/revokeApiToken.ts new file mode 100644 index 000000000..8899b9b49 --- /dev/null +++ b/src/users/domain/useCases/revokeApiToken.ts @@ -0,0 +1,5 @@ +import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' + +export function revokeApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { + return apiTokenRepository.deleteApiToken() +} diff --git a/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx b/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx new file mode 100644 index 000000000..c274ca900 --- /dev/null +++ b/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx @@ -0,0 +1,37 @@ +import { TokenInfo } from '../../domain/models/TokenInfo' +import { ApiTokenInfoRepository } from '../../domain/repositories/ApiTokenInfoRepository' +import { DateHelper } from '@/shared/helpers/DateHelper' +import { + getCurrentApiToken, + recreateCurrentApiToken, + deleteCurrentApiToken +} from '@iqss/dataverse-client-javascript' + +interface ApiTokenInfoPayload { + apiToken: string + expirationDate: Date +} + +export class ApiTokenInfoJSDataverseRepository implements ApiTokenInfoRepository { + getCurrentApiToken(): Promise { + return getCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { + return { + apiToken: apiTokenInfo.apiToken, + expirationDate: DateHelper.toISO8601Format(apiTokenInfo.expirationDate) + } + }) + } + + recreateApiToken(): Promise { + return recreateCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { + return { + apiToken: apiTokenInfo.apiToken, + expirationDate: DateHelper.toISO8601Format(apiTokenInfo.expirationDate) + } + }) + } + + deleteApiToken(): Promise { + return deleteCurrentApiToken.execute() + } +} diff --git a/tests/component/sections/account/ApiTokenSection.spec.tsx b/tests/component/sections/account/ApiTokenSection.spec.tsx index 26ff3220b..0116a392a 100644 --- a/tests/component/sections/account/ApiTokenSection.spec.tsx +++ b/tests/component/sections/account/ApiTokenSection.spec.tsx @@ -1,8 +1,37 @@ import { ApiTokenSection } from '../../../../src/sections/account/api-token-section/ApiTokenSection' - +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' describe('ApiTokenSection', () => { + const mockApiTokenInfo = { + apiToken: 'mocked-api', + expirationDate: '2024-12-31' + } + const newMockApiTokenInfo = { + apiToken: 'new-mocked-api', + expirationDate: '2025-12-31' + } + + let apiTokenRepository: ApiTokenInfoRepository + beforeEach(() => { - cy.mountAuthenticated() + apiTokenRepository = { + getCurrentApiToken: cy.stub().resolves(mockApiTokenInfo), + recreateApiToken: cy.stub().resolves(mockApiTokenInfo), + deleteApiToken: cy.stub().resolves() + } + cy.mountAuthenticated() + }) + + it('should show the loading skeleton while fetching the token', () => { + // Simulate a delayed API response + apiTokenRepository.getCurrentApiToken = cy.stub().callsFake(() => { + return Cypress.Promise.delay(500).then(() => mockApiTokenInfo) + }) + + cy.mount() + cy.get('[data-testid="loadingSkeleton"]').should('exist') // Verify loading skeleton exists + + cy.wait(500) // Wait for the delay to finish + cy.get('[data-testid="loadingSkeleton"]').should('not.exist') // Verify skeleton is gone }) it('should copy the api token to the clipboard', () => { @@ -20,5 +49,27 @@ describe('ApiTokenSection', () => { }) }) - // TODO: When we get the api token from the use case, we could mock the response and test more things. + it('should fetch and display the current API token', () => { + cy.get('[data-testid="api-token"]').should('contain.text', mockApiTokenInfo.apiToken) + cy.get('[data-testid="expiration-date"]').should( + 'contain.text', + mockApiTokenInfo.expirationDate + ) + }) + + it('should recreate and display a new API token', () => { + apiTokenRepository.recreateApiToken = cy.stub().resolves(newMockApiTokenInfo) + cy.get('button').contains('Recreate Token').click() + cy.get('[data-testid="api-token"]').should('contain.text', newMockApiTokenInfo.apiToken) + cy.get('[data-testid="expiration-date"]').should( + 'contain.text', + newMockApiTokenInfo.expirationDate + ) + }) + + it('should revoke the API token and show the create token state when there is no api token', () => { + cy.get('button').contains('Revoke Token').click() + cy.get('[data-testid="noApiToken"]').should('exist') + cy.get('button').contains('Create Token').should('exist') + }) }) diff --git a/tests/component/sections/account/useGetApiTokenInfo.spec.tsx b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx new file mode 100644 index 000000000..1756ae212 --- /dev/null +++ b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx @@ -0,0 +1,69 @@ +import { act, renderHook } from '@testing-library/react' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { useGetApiToken } from '@/sections/account/api-token-section/useGetCurrentApiToken' +import { TokenInfo } from '@/users/domain/models/TokenInfo' + +describe('useGetApiToken', () => { + let apiTokenInfoRepository: ApiTokenInfoRepository + + const mockTokenInfo: TokenInfo = { + apiToken: 'mocked-api-token', + expirationDate: '2024-12-31' + } + + beforeEach(() => { + apiTokenInfoRepository = {} as ApiTokenInfoRepository + }) + + it('should return the API token correctly', async () => { + apiTokenInfoRepository.getCurrentApiToken = cy.stub().resolves(mockTokenInfo) + + const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + await act(() => { + expect(result.current.isLoading).to.equal(true) + expect(result.current.error).to.equal(null) + return expect(result.current.apiTokenInfo).to.deep.equal({ + apiToken: '', + expirationDate: '' + }) + }) + + await act(() => { + expect(result.current.isLoading).to.equal(false) + expect(result.current.error).to.equal(null) + return expect(result.current.apiTokenInfo).to.deep.equal(mockTokenInfo) + }) + }) + + describe('Error Handling', () => { + it('should handle error correctly when an error is thrown', async () => { + apiTokenInfoRepository.getCurrentApiToken = cy.stub().rejects(new Error('API Error')) + + const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(true) + return expect(result.current.error).to.equal(null) + }) + await act(() => { + expect(result.current.isLoading).to.deep.equal(false) + return expect(result.current.error).to.equal('API Error') + }) + }) + + it('should return correct error message when there is not an error type catched', async () => { + apiTokenInfoRepository.getCurrentApiToken = cy.stub().rejects('Error message') + + const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + + await act(() => { + expect(result.current.isLoading).to.deep.equal(true) + return expect(result.current.error).to.equal(null) + }) + await act(() => { + expect(result.current.isLoading).to.deep.equal(false) + return expect(result.current.error).to.deep.equal('Failed to fetch API token.') + }) + }) + }) +}) diff --git a/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx b/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx new file mode 100644 index 000000000..9d07db839 --- /dev/null +++ b/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx @@ -0,0 +1,96 @@ +import { act, renderHook } from '@testing-library/react' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { useRecreateApiToken } from '@/sections/account/api-token-section/useRecreateApiToken' +import { TokenInfo } from '@/users/domain/models/TokenInfo' + +describe('useRecreateApiToken', () => { + let apiTokenInfoRepository: ApiTokenInfoRepository + + const mockTokenInfo: TokenInfo = { + apiToken: 'new-mocked-api-token', + expirationDate: '2024-12-31' + } + + beforeEach(() => { + apiTokenInfoRepository = {} as ApiTokenInfoRepository + }) + + it('should return the API token correctly', async () => { + apiTokenInfoRepository.recreateApiToken = cy.stub().resolves(mockTokenInfo) + + const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal(null) + expect(result.current.apiTokenInfo).to.deep.equal(null) + + act(() => { + result.current.initiateRecreateToken() + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(true) + expect(result.current.error).to.equal(null) + return expect(result.current.apiTokenInfo).to.equal(null) + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal(null) + return expect(result.current.apiTokenInfo).to.deep.equal(mockTokenInfo) + }) + }) + + describe('Error Handling', () => { + it('should handle error correctly when an error is thrown', async () => { + const errorMessage = 'Failed to recreate API token.' + apiTokenInfoRepository.recreateApiToken = cy.stub().rejects(new Error(errorMessage)) + + const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal(null) + expect(result.current.apiTokenInfo).to.equal(null) + + act(() => { + result.current.initiateRecreateToken() + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(true) + return expect(result.current.error).to.equal(null) + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal('Failed to recreate API token.') + return expect(result.current.apiTokenInfo).to.equal(null) + }) + }) + + it('should return correct error message when there is not an error type catched', async () => { + apiTokenInfoRepository.recreateApiToken = cy.stub().rejects('Unexpected error message') + + const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal(null) + expect(result.current.apiTokenInfo).to.equal(null) + + act(() => { + result.current.initiateRecreateToken() + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(true) + return expect(result.current.error).to.equal(null) + }) + + await act(() => { + expect(result.current.isRecreating).to.equal(false) + expect(result.current.error).to.equal('Failed to recreate API token.') + return expect(result.current.apiTokenInfo).to.equal(null) + }) + }) + }) +}) diff --git a/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx b/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx new file mode 100644 index 000000000..57cf26b0b --- /dev/null +++ b/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx @@ -0,0 +1,56 @@ +import { act, renderHook } from '@testing-library/react' +import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { useRevokeApiToken } from '@/sections/account/api-token-section/useRevokeApiToken' + +describe('useRevokeApiToken', () => { + let apiTokenInfoRepository: ApiTokenInfoRepository + + beforeEach(() => { + apiTokenInfoRepository = {} as ApiTokenInfoRepository + }) + + it('should revoke the API token successfully', async () => { + apiTokenInfoRepository.deleteApiToken = cy.stub().resolves() + + const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + + expect(result.current.isRevoking).to.equal(false) + expect(result.current.error).to.equal(null) + + await act(async () => { + await result.current.revokeToken() + }) + + expect(result.current.isRevoking).to.equal(false) + expect(result.current.error).to.equal(null) + }) + + describe('Error Handling', () => { + it('should handle error correctly when an error is thrown', async () => { + const errorMessage = 'API token revocation failed.' + apiTokenInfoRepository.deleteApiToken = cy.stub().rejects(new Error(errorMessage)) + + const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + + await act(async () => { + await result.current.revokeToken() + }) + + expect(result.current.isRevoking).to.equal(false) + expect(result.current.error).to.equal('Failed to revoke API token.') + }) + + it('should handle non-error rejection gracefully', async () => { + apiTokenInfoRepository.deleteApiToken = cy.stub().rejects('Unexpected error') + + const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + + await act(async () => { + await result.current.revokeToken() + }) + + expect(result.current.isRevoking).to.equal(false) + expect(result.current.error).to.equal('Failed to revoke API token.') + }) + }) +}) diff --git a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts new file mode 100644 index 000000000..663fdb575 --- /dev/null +++ b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts @@ -0,0 +1,36 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { ApiTokenInfoJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository' + +import { TestsUtils } from '../../shared/TestsUtils' + +chai.use(chaiAsPromised) +const expect = chai.expect + +const apiTokenInfoRepository = new ApiTokenInfoJSDataverseRepository() +describe('API Token Info JSDataverse Repository', () => { + before(() => TestsUtils.setup()) + beforeEach(() => TestsUtils.login()) + + it('create or recreate the API token and return the new token info', async () => { + const recreatedTokenInfo = await apiTokenInfoRepository.recreateApiToken() + if (!recreatedTokenInfo) { + throw new Error('Failed to recreate API token') + } + expect(recreatedTokenInfo).to.have.property('apiToken').that.is.a('string') + expect(recreatedTokenInfo).to.have.property('expirationDate').that.is.a('string') + }) + + it('fetche the current API token', async () => { + const tokenInfo = await apiTokenInfoRepository.getCurrentApiToken() + if (!tokenInfo) { + throw new Error('API Token not found') + } + expect(tokenInfo).to.have.property('apiToken').that.is.a('string') + expect(tokenInfo).to.have.property('expirationDate').that.is.a('string') + }) + + it('revoke the API token', async () => { + await expect(apiTokenInfoRepository.deleteApiToken()).to.be.fulfilled + }) +}) From 742c88d44be04540702fd7e49cfb6987720230c2 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 28 Oct 2024 13:43:52 -0400 Subject: [PATCH 2/8] fix: add some missing code in account page --- src/sections/account/Account.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 298f15b8e..30c3ebb42 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -6,6 +6,11 @@ import { AccountHelper, AccountPanelTabKey } from './AccountHelper' import { ApiTokenSection } from './api-token-section/ApiTokenSection' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import styles from './Account.module.scss' +import { + DvObjectType, + UpwardHierarchyNode +} from '../../shared/hierarchy/domain/models/UpwardHierarchyNode' +import { ROOT_COLLECTION_ALIAS } from '../../collection/domain/models/Collection' import { ApiTokenInfoJSDataverseRepository } from '@/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository' const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS From c60e345cc12fc15921107b39a42c0a64c016948a Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 29 Oct 2024 15:27:06 -0400 Subject: [PATCH 3/8] fix: change the order of 'revoke' in e2e test --- .../account/ApiTokenInfoJSDataverseRepository.spec.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts index 663fdb575..0683b3b70 100644 --- a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts @@ -11,6 +11,9 @@ const apiTokenInfoRepository = new ApiTokenInfoJSDataverseRepository() describe('API Token Info JSDataverse Repository', () => { before(() => TestsUtils.setup()) beforeEach(() => TestsUtils.login()) + it('revoke the API token', async () => { + await expect(apiTokenInfoRepository.deleteApiToken()).to.be.fulfilled + }) it('create or recreate the API token and return the new token info', async () => { const recreatedTokenInfo = await apiTokenInfoRepository.recreateApiToken() @@ -21,7 +24,7 @@ describe('API Token Info JSDataverse Repository', () => { expect(recreatedTokenInfo).to.have.property('expirationDate').that.is.a('string') }) - it('fetche the current API token', async () => { + it('fetch the current API token', async () => { const tokenInfo = await apiTokenInfoRepository.getCurrentApiToken() if (!tokenInfo) { throw new Error('API Token not found') @@ -29,8 +32,4 @@ describe('API Token Info JSDataverse Repository', () => { expect(tokenInfo).to.have.property('apiToken').that.is.a('string') expect(tokenInfo).to.have.property('expirationDate').that.is.a('string') }) - - it('revoke the API token', async () => { - await expect(apiTokenInfoRepository.deleteApiToken()).to.be.fulfilled - }) }) From 6eff4e0803cfbf6ebe3a227f37a1f8991b329076 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 30 Oct 2024 10:29:14 -0400 Subject: [PATCH 4/8] chore: formatting --- .../account/api-token-section/useGetCurrentApiToken.tsx | 2 +- .../account/api-token-section/useRecreateApiToken.tsx | 1 + tests/component/sections/account/ApiTokenSection.spec.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx index 78dfd2ea0..ecf66aacc 100644 --- a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx +++ b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx @@ -39,5 +39,5 @@ export const useGetApiToken = (repository: ApiTokenInfoRepository): UseGetApiTok void fetchTokenInfo() }, [fetchTokenInfo]) - return { error, apiTokenInfo, isLoading } + return { isLoading, error, apiTokenInfo } } diff --git a/src/sections/account/api-token-section/useRecreateApiToken.tsx b/src/sections/account/api-token-section/useRecreateApiToken.tsx index eb0711597..bf48ce54d 100644 --- a/src/sections/account/api-token-section/useRecreateApiToken.tsx +++ b/src/sections/account/api-token-section/useRecreateApiToken.tsx @@ -38,6 +38,7 @@ export const useRecreateApiToken = ( setShouldRecreate(false) } } + if (shouldRecreate) { void recreateToken() } diff --git a/tests/component/sections/account/ApiTokenSection.spec.tsx b/tests/component/sections/account/ApiTokenSection.spec.tsx index 0116a392a..5b10d781e 100644 --- a/tests/component/sections/account/ApiTokenSection.spec.tsx +++ b/tests/component/sections/account/ApiTokenSection.spec.tsx @@ -18,20 +18,20 @@ describe('ApiTokenSection', () => { recreateApiToken: cy.stub().resolves(mockApiTokenInfo), deleteApiToken: cy.stub().resolves() } + cy.mountAuthenticated() }) it('should show the loading skeleton while fetching the token', () => { - // Simulate a delayed API response apiTokenRepository.getCurrentApiToken = cy.stub().callsFake(() => { return Cypress.Promise.delay(500).then(() => mockApiTokenInfo) }) cy.mount() - cy.get('[data-testid="loadingSkeleton"]').should('exist') // Verify loading skeleton exists + cy.get('[data-testid="loadingSkeleton"]').should('exist') - cy.wait(500) // Wait for the delay to finish - cy.get('[data-testid="loadingSkeleton"]').should('not.exist') // Verify skeleton is gone + cy.wait(500) + cy.get('[data-testid="loadingSkeleton"]').should('not.exist') }) it('should copy the api token to the clipboard', () => { From 5a8146e48ada257588f4f3afcdc57e5284e8a619 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 5 Nov 2024 21:15:27 -0500 Subject: [PATCH 5/8] fix: change according to reviews --- src/sections/account/Account.tsx | 8 +-- .../api-token-section/ApiTokenSection.tsx | 30 ++++++----- .../ApiTokenSectionSkeleton.tsx | 8 ++- .../CreateUserRepositoryFactory.tsx | 15 ++++++ .../useGetCurrentApiToken.tsx | 12 +++-- .../api-token-section/useRecreateApiToken.tsx | 52 +++++++------------ .../api-token-section/useRevokeApiToken.tsx | 18 ++++--- src/users/domain/models/TokenInfo.ts | 2 +- .../repositories/ApiTokenInfoRepository.tsx | 7 --- .../domain/repositories/UserRepository.tsx | 4 ++ .../domain/useCases/getCurrentApiToken.ts | 6 +-- src/users/domain/useCases/recreateApiToken.ts | 6 +-- src/users/domain/useCases/revokeApiToken.ts | 6 +-- .../ApiTokenInfoJSDataverseRepository.tsx | 37 ------------- .../repositories/UserJSDataverseRepository.ts | 35 ++++++++++++- .../sections/account/ApiTokenSection.spec.tsx | 18 ++++--- .../account/useGetApiTokenInfo.spec.tsx | 42 ++++++++++----- .../account/useRecreateApiTokenInfo.spec.tsx | 30 ++++++----- .../account/useRevokeApiTokenInfo.spec.tsx | 24 +++++---- .../ApiTokenInfoJSDataverseRepository.spec.ts | 15 +++--- 20 files changed, 198 insertions(+), 177 deletions(-) create mode 100644 src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx delete mode 100644 src/users/domain/repositories/ApiTokenInfoRepository.tsx delete mode 100644 src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 6bd8e884f..32ee78e3b 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -1,9 +1,8 @@ import { useTranslation } from 'react-i18next' import { Tabs } from '@iqss/dataverse-design-system' import { AccountHelper, AccountPanelTabKey } from './AccountHelper' -import { ApiTokenSection } from './api-token-section/ApiTokenSection' +import { createUserRepository } from './api-token-section/CreateUserRepositoryFactory' import styles from './Account.module.scss' -import { ApiTokenInfoJSDataverseRepository } from '@/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository' const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS @@ -13,7 +12,6 @@ interface AccountProps { export const Account = ({ defaultActiveTabKey }: AccountProps) => { const { t } = useTranslation('account') - const repository = new ApiTokenInfoJSDataverseRepository() return (
@@ -35,9 +33,7 @@ export const Account = ({ defaultActiveTabKey }: AccountProps) => {
-
- -
+
{createUserRepository.create()}
diff --git a/src/sections/account/api-token-section/ApiTokenSection.tsx b/src/sections/account/api-token-section/ApiTokenSection.tsx index e9f2c7a4b..c0877d868 100644 --- a/src/sections/account/api-token-section/ApiTokenSection.tsx +++ b/src/sections/account/api-token-section/ApiTokenSection.tsx @@ -1,17 +1,19 @@ import { Trans, useTranslation } from 'react-i18next' -import { Button } from '@iqss/dataverse-design-system' -import accountStyles from '../Account.module.scss' -import styles from './ApiTokenSection.module.scss' import { useEffect, useState } from 'react' -import { Alert } from '@iqss/dataverse-design-system' -import { TokenInfo } from '@/users/domain/models/TokenInfo' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' -import ApiTokenSectionSkeleton from './ApiTokenSectionSkeleton' import { useGetApiToken } from './useGetCurrentApiToken' import { useRecreateApiToken } from './useRecreateApiToken' import { useRevokeApiToken } from './useRevokeApiToken' +import { ApiTokenSectionSkeleton } from './ApiTokenSectionSkeleton' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { DateHelper } from '@/shared/helpers/DateHelper' +import { UserRepository } from '@/users/domain/repositories/UserRepository' +import { Button } from '@iqss/dataverse-design-system' +import { Alert } from '@iqss/dataverse-design-system' +import accountStyles from '../Account.module.scss' +import styles from './ApiTokenSection.module.scss' + interface ApiTokenSectionProps { - repository: ApiTokenInfoRepository + repository: UserRepository } export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { @@ -30,7 +32,7 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { }, [apiTokenInfo]) const { - initiateRecreateToken, + recreateToken, isRecreating, error: recreatingError, apiTokenInfo: updatedTokenInfo @@ -43,7 +45,7 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { }, [updatedTokenInfo]) const handleCreateToken = () => { - initiateRecreateToken() + void recreateToken() } const { revokeToken, isRevoking, error: revokingError } = useRevokeApiToken(repository) @@ -52,7 +54,7 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { await revokeToken() setCurrentApiTokenInfo({ apiToken: '', - expirationDate: '' + expirationDate: new Date(0) }) } @@ -97,8 +99,10 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { <>

{t('expirationDate')}{' '} -

diff --git a/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx b/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx index 073687f1f..435eacd1e 100644 --- a/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx +++ b/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx @@ -1,11 +1,10 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' -import 'react-loading-skeleton/dist/skeleton.css' import { Trans, useTranslation } from 'react-i18next' -import accountStyles from '../Account.module.scss' +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' import { Button } from '@iqss/dataverse-design-system' +import accountStyles from '../Account.module.scss' import styles from './ApiTokenSection.module.scss' -const ApiTokenSectionSkeleton = () => { +export const ApiTokenSectionSkeleton = () => { const { t } = useTranslation('account', { keyPrefix: 'apiToken' }) return ( @@ -48,4 +47,3 @@ const ApiTokenSectionSkeleton = () => { ) } -export default ApiTokenSectionSkeleton diff --git a/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx b/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx new file mode 100644 index 000000000..76d27a728 --- /dev/null +++ b/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx @@ -0,0 +1,15 @@ +import { ReactElement } from 'react' +import { ApiTokenSection } from './ApiTokenSection' +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' + +const userRepository = new UserJSDataverseRepository() + +export class createUserRepository { + static create(): ReactElement { + return + } +} + +function CreateUserRepository() { + return +} diff --git a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx index ecf66aacc..1e2f0f571 100644 --- a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx +++ b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' import { TokenInfo } from '@/users/domain/models/TokenInfo' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { getCurrentApiToken } from '@/users/domain/useCases/getCurrentApiToken' interface UseGetApiTokenResult { @@ -9,10 +9,10 @@ interface UseGetApiTokenResult { error: string | null } -export const useGetApiToken = (repository: ApiTokenInfoRepository): UseGetApiTokenResult => { +export const useGetApiToken = (repository: UserRepository): UseGetApiTokenResult => { const [apiTokenInfo, setApiTokenInfo] = useState({ apiToken: '', - expirationDate: '' + expirationDate: new Date(0) }) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) @@ -27,8 +27,10 @@ export const useGetApiToken = (repository: ApiTokenInfoRepository): UseGetApiTok }) setError(null) } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to fetch API token.' - console.error(errorMessage) + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong getting the current api token. Try again later.' setError(errorMessage) } finally { setIsLoading(false) diff --git a/src/sections/account/api-token-section/useRecreateApiToken.tsx b/src/sections/account/api-token-section/useRecreateApiToken.tsx index bf48ce54d..d957554a2 100644 --- a/src/sections/account/api-token-section/useRecreateApiToken.tsx +++ b/src/sections/account/api-token-section/useRecreateApiToken.tsx @@ -1,48 +1,36 @@ -import { useState, useEffect } from 'react' +import { useState } from 'react' import { recreateApiToken } from '@/users/domain/useCases/recreateApiToken' import { TokenInfo } from '@/users/domain/models/TokenInfo' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' interface UseRecreateApiTokenResult { - initiateRecreateToken: () => void + recreateToken: () => Promise isRecreating: boolean error: string | null apiTokenInfo: TokenInfo | null } -export const useRecreateApiToken = ( - repository: ApiTokenInfoRepository -): UseRecreateApiTokenResult => { +export const useRecreateApiToken = (repository: UserRepository): UseRecreateApiTokenResult => { const [isRecreating, setIsRecreating] = useState(false) const [error, setError] = useState(null) const [apiTokenInfo, setApiTokenInfo] = useState(null) - const [shouldRecreate, setShouldRecreate] = useState(false) - const initiateRecreateToken = () => { - setShouldRecreate(true) - } - - useEffect(() => { - const recreateToken = async () => { - setIsRecreating(true) - setError(null) - - try { - const newTokenInfo = await recreateApiToken(repository) - setApiTokenInfo(newTokenInfo) - } catch (err) { - console.error('Error recreating token:', err) - setError('Failed to recreate API token.') - } finally { - setIsRecreating(false) - setShouldRecreate(false) - } + const recreateToken = async () => { + setIsRecreating(true) + setError(null) + try { + const newTokenInfo = await recreateApiToken(repository) + setApiTokenInfo(newTokenInfo) + } catch (err) { + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong creating the api token. Try again later.' + setError(errorMessage) + } finally { + setIsRecreating(false) } + } - if (shouldRecreate) { - void recreateToken() - } - }, [shouldRecreate, repository]) - - return { initiateRecreateToken, isRecreating, error, apiTokenInfo } + return { recreateToken, isRecreating, error, apiTokenInfo } } diff --git a/src/sections/account/api-token-section/useRevokeApiToken.tsx b/src/sections/account/api-token-section/useRevokeApiToken.tsx index 618ed0ec8..4bbb6ec85 100644 --- a/src/sections/account/api-token-section/useRevokeApiToken.tsx +++ b/src/sections/account/api-token-section/useRevokeApiToken.tsx @@ -1,6 +1,6 @@ -import { useState, useCallback } from 'react' +import { useState } from 'react' import { revokeApiToken } from '@/users/domain/useCases/revokeApiToken' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' interface UseRevokeApiTokenResult { revokeToken: () => Promise @@ -8,23 +8,25 @@ interface UseRevokeApiTokenResult { error: string | null } -export const useRevokeApiToken = (repository: ApiTokenInfoRepository): UseRevokeApiTokenResult => { +export const useRevokeApiToken = (repository: UserRepository): UseRevokeApiTokenResult => { const [isRevoking, setIsRevoking] = useState(false) const [error, setError] = useState(null) - const revokeToken = useCallback(async () => { + const revokeToken = async () => { setIsRevoking(true) setError(null) - try { await revokeApiToken(repository) } catch (err) { - console.error('There was an error revoking Api token:', err) - setError('Failed to revoke API token.') + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong revoking the api token. Try again later.' + setError(errorMessage) } finally { setIsRevoking(false) } - }, [repository]) + } return { revokeToken, isRevoking, error } } diff --git a/src/users/domain/models/TokenInfo.ts b/src/users/domain/models/TokenInfo.ts index 761a983c3..1916f5e19 100644 --- a/src/users/domain/models/TokenInfo.ts +++ b/src/users/domain/models/TokenInfo.ts @@ -1,4 +1,4 @@ export interface TokenInfo { apiToken: string - expirationDate: string + expirationDate: Date } diff --git a/src/users/domain/repositories/ApiTokenInfoRepository.tsx b/src/users/domain/repositories/ApiTokenInfoRepository.tsx deleted file mode 100644 index 1e53d0b55..000000000 --- a/src/users/domain/repositories/ApiTokenInfoRepository.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { TokenInfo } from '../.././domain/models/TokenInfo' - -export interface ApiTokenInfoRepository { - getCurrentApiToken(): Promise - recreateApiToken(): Promise - deleteApiToken(): Promise -} diff --git a/src/users/domain/repositories/UserRepository.tsx b/src/users/domain/repositories/UserRepository.tsx index 796037f72..77295dedc 100644 --- a/src/users/domain/repositories/UserRepository.tsx +++ b/src/users/domain/repositories/UserRepository.tsx @@ -1,6 +1,10 @@ import { User } from '../models/User' +import { TokenInfo } from '../.././domain/models/TokenInfo' export interface UserRepository { getAuthenticated: () => Promise removeAuthenticated: () => Promise + getCurrentApiToken: () => Promise + recreateApiToken: () => Promise + deleteApiToken: () => Promise } diff --git a/src/users/domain/useCases/getCurrentApiToken.ts b/src/users/domain/useCases/getCurrentApiToken.ts index 4ae7da442..d3b8dfb08 100644 --- a/src/users/domain/useCases/getCurrentApiToken.ts +++ b/src/users/domain/useCases/getCurrentApiToken.ts @@ -1,6 +1,6 @@ import { TokenInfo } from '../models/TokenInfo' -import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' +import { UserRepository } from '../repositories/UserRepository' -export function getCurrentApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { - return apiTokenRepository.getCurrentApiToken() +export function getCurrentApiToken(userRepository: UserRepository): Promise { + return userRepository.getCurrentApiToken() } diff --git a/src/users/domain/useCases/recreateApiToken.ts b/src/users/domain/useCases/recreateApiToken.ts index a61992e7f..9c1029e62 100644 --- a/src/users/domain/useCases/recreateApiToken.ts +++ b/src/users/domain/useCases/recreateApiToken.ts @@ -1,6 +1,6 @@ import { TokenInfo } from '../models/TokenInfo' -import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' +import { UserRepository } from '../repositories/UserRepository' -export function recreateApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { - return apiTokenRepository.recreateApiToken() +export function recreateApiToken(userRepository: UserRepository): Promise { + return userRepository.recreateApiToken() } diff --git a/src/users/domain/useCases/revokeApiToken.ts b/src/users/domain/useCases/revokeApiToken.ts index 8899b9b49..47a895045 100644 --- a/src/users/domain/useCases/revokeApiToken.ts +++ b/src/users/domain/useCases/revokeApiToken.ts @@ -1,5 +1,5 @@ -import { ApiTokenInfoRepository } from '../repositories/ApiTokenInfoRepository' +import { UserRepository } from '../repositories/UserRepository' -export function revokeApiToken(apiTokenRepository: ApiTokenInfoRepository): Promise { - return apiTokenRepository.deleteApiToken() +export function revokeApiToken(userRepository: UserRepository): Promise { + return userRepository.deleteApiToken() } diff --git a/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx b/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx deleted file mode 100644 index c274ca900..000000000 --- a/src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { TokenInfo } from '../../domain/models/TokenInfo' -import { ApiTokenInfoRepository } from '../../domain/repositories/ApiTokenInfoRepository' -import { DateHelper } from '@/shared/helpers/DateHelper' -import { - getCurrentApiToken, - recreateCurrentApiToken, - deleteCurrentApiToken -} from '@iqss/dataverse-client-javascript' - -interface ApiTokenInfoPayload { - apiToken: string - expirationDate: Date -} - -export class ApiTokenInfoJSDataverseRepository implements ApiTokenInfoRepository { - getCurrentApiToken(): Promise { - return getCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { - return { - apiToken: apiTokenInfo.apiToken, - expirationDate: DateHelper.toISO8601Format(apiTokenInfo.expirationDate) - } - }) - } - - recreateApiToken(): Promise { - return recreateCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { - return { - apiToken: apiTokenInfo.apiToken, - expirationDate: DateHelper.toISO8601Format(apiTokenInfo.expirationDate) - } - }) - } - - deleteApiToken(): Promise { - return deleteCurrentApiToken.execute() - } -} diff --git a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts index 4307885e3..a4c3639fe 100644 --- a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts +++ b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts @@ -1,10 +1,19 @@ import { User } from '../../domain/models/User' +import { TokenInfo } from '../../domain/models/TokenInfo' +import { UserRepository } from '../../domain/repositories/UserRepository' import { AuthenticatedUser, - getCurrentAuthenticatedUser + getCurrentAuthenticatedUser, + getCurrentApiToken, + recreateCurrentApiToken, + deleteCurrentApiToken } from '@iqss/dataverse-client-javascript/dist/users' import { logout, ReadError, WriteError } from '@iqss/dataverse-client-javascript' -import { UserRepository } from '../../domain/repositories/UserRepository' + +interface ApiTokenInfoPayload { + apiToken: string + expirationDate: Date +} export class UserJSDataverseRepository implements UserRepository { getAuthenticated(): Promise { @@ -31,4 +40,26 @@ export class UserJSDataverseRepository implements UserRepository { throw new Error(error.message) }) } + + getCurrentApiToken(): Promise { + return getCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { + return { + apiToken: apiTokenInfo.apiToken, + expirationDate: apiTokenInfo.expirationDate + } + }) + } + + recreateApiToken(): Promise { + return recreateCurrentApiToken.execute().then((apiTokenInfo: ApiTokenInfoPayload) => { + return { + apiToken: apiTokenInfo.apiToken, + expirationDate: apiTokenInfo.expirationDate + } + }) + } + + deleteApiToken(): Promise { + return deleteCurrentApiToken.execute() + } } diff --git a/tests/component/sections/account/ApiTokenSection.spec.tsx b/tests/component/sections/account/ApiTokenSection.spec.tsx index 5b10d781e..83547deee 100644 --- a/tests/component/sections/account/ApiTokenSection.spec.tsx +++ b/tests/component/sections/account/ApiTokenSection.spec.tsx @@ -1,22 +1,26 @@ import { ApiTokenSection } from '../../../../src/sections/account/api-token-section/ApiTokenSection' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { DateHelper } from '@/shared/helpers/DateHelper' +import { UserRepository } from '@/users/domain/repositories/UserRepository' + describe('ApiTokenSection', () => { const mockApiTokenInfo = { apiToken: 'mocked-api', - expirationDate: '2024-12-31' + expirationDate: new Date('2024-12-31') } const newMockApiTokenInfo = { apiToken: 'new-mocked-api', - expirationDate: '2025-12-31' + expirationDate: new Date('2025-12-31') } - let apiTokenRepository: ApiTokenInfoRepository + let apiTokenRepository: UserRepository beforeEach(() => { apiTokenRepository = { getCurrentApiToken: cy.stub().resolves(mockApiTokenInfo), recreateApiToken: cy.stub().resolves(mockApiTokenInfo), - deleteApiToken: cy.stub().resolves() + deleteApiToken: cy.stub().resolves(), + getAuthenticated: cy.stub().resolves(), + removeAuthenticated: cy.stub().resolves() } cy.mountAuthenticated() @@ -53,7 +57,7 @@ describe('ApiTokenSection', () => { cy.get('[data-testid="api-token"]').should('contain.text', mockApiTokenInfo.apiToken) cy.get('[data-testid="expiration-date"]').should( 'contain.text', - mockApiTokenInfo.expirationDate + DateHelper.toISO8601Format(mockApiTokenInfo.expirationDate) ) }) @@ -63,7 +67,7 @@ describe('ApiTokenSection', () => { cy.get('[data-testid="api-token"]').should('contain.text', newMockApiTokenInfo.apiToken) cy.get('[data-testid="expiration-date"]').should( 'contain.text', - newMockApiTokenInfo.expirationDate + DateHelper.toISO8601Format(newMockApiTokenInfo.expirationDate) ) }) diff --git a/tests/component/sections/account/useGetApiTokenInfo.spec.tsx b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx index 1756ae212..53768c586 100644 --- a/tests/component/sections/account/useGetApiTokenInfo.spec.tsx +++ b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx @@ -1,45 +1,59 @@ import { act, renderHook } from '@testing-library/react' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' import { useGetApiToken } from '@/sections/account/api-token-section/useGetCurrentApiToken' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { DateHelper } from '@/shared/helpers/DateHelper' describe('useGetApiToken', () => { - let apiTokenInfoRepository: ApiTokenInfoRepository + let UserRepository: UserRepository const mockTokenInfo: TokenInfo = { apiToken: 'mocked-api-token', - expirationDate: '2024-12-31' + expirationDate: new Date('2024-11-05') } beforeEach(() => { - apiTokenInfoRepository = {} as ApiTokenInfoRepository + UserRepository = {} as UserRepository }) it('should return the API token correctly', async () => { - apiTokenInfoRepository.getCurrentApiToken = cy.stub().resolves(mockTokenInfo) + UserRepository.getCurrentApiToken = cy.stub().resolves(mockTokenInfo) - const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useGetApiToken(UserRepository)) await act(() => { expect(result.current.isLoading).to.equal(true) expect(result.current.error).to.equal(null) + return expect(result.current.apiTokenInfo).to.deep.equal({ apiToken: '', - expirationDate: '' + expirationDate: new Date(0) }) }) await act(() => { expect(result.current.isLoading).to.equal(false) expect(result.current.error).to.equal(null) - return expect(result.current.apiTokenInfo).to.deep.equal(mockTokenInfo) + const apiTokenInfo = { + ...result.current.apiTokenInfo, + expirationDate: DateHelper.toISO8601Format(result.current.apiTokenInfo.expirationDate) + } + console.log( + 'test', + DateHelper.toISO8601Format(result.current.apiTokenInfo.expirationDate), + DateHelper.toISO8601Format(mockTokenInfo.expirationDate) + ) + return expect(apiTokenInfo).to.deep.equal({ + apiToken: mockTokenInfo.apiToken, + expirationDate: DateHelper.toISO8601Format(mockTokenInfo.expirationDate) + }) }) }) describe('Error Handling', () => { it('should handle error correctly when an error is thrown', async () => { - apiTokenInfoRepository.getCurrentApiToken = cy.stub().rejects(new Error('API Error')) + UserRepository.getCurrentApiToken = cy.stub().rejects(new Error('API Error')) - const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useGetApiToken(UserRepository)) await act(() => { expect(result.current.isLoading).to.deep.equal(true) @@ -52,9 +66,9 @@ describe('useGetApiToken', () => { }) it('should return correct error message when there is not an error type catched', async () => { - apiTokenInfoRepository.getCurrentApiToken = cy.stub().rejects('Error message') + UserRepository.getCurrentApiToken = cy.stub().rejects('Error message') - const { result } = renderHook(() => useGetApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useGetApiToken(UserRepository)) await act(() => { expect(result.current.isLoading).to.deep.equal(true) @@ -62,7 +76,9 @@ describe('useGetApiToken', () => { }) await act(() => { expect(result.current.isLoading).to.deep.equal(false) - return expect(result.current.error).to.deep.equal('Failed to fetch API token.') + return expect(result.current.error).to.deep.equal( + 'Something went wrong getting the current api token. Try again later.' + ) }) }) }) diff --git a/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx b/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx index 9d07db839..c3b354e76 100644 --- a/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx +++ b/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx @@ -1,31 +1,31 @@ import { act, renderHook } from '@testing-library/react' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { useRecreateApiToken } from '@/sections/account/api-token-section/useRecreateApiToken' import { TokenInfo } from '@/users/domain/models/TokenInfo' describe('useRecreateApiToken', () => { - let apiTokenInfoRepository: ApiTokenInfoRepository + let UserRepository: UserRepository const mockTokenInfo: TokenInfo = { apiToken: 'new-mocked-api-token', - expirationDate: '2024-12-31' + expirationDate: new Date('2024-12-31') } beforeEach(() => { - apiTokenInfoRepository = {} as ApiTokenInfoRepository + UserRepository = {} as UserRepository }) it('should return the API token correctly', async () => { - apiTokenInfoRepository.recreateApiToken = cy.stub().resolves(mockTokenInfo) + UserRepository.recreateApiToken = cy.stub().resolves(mockTokenInfo) - const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRecreateApiToken(UserRepository)) expect(result.current.isRecreating).to.equal(false) expect(result.current.error).to.equal(null) expect(result.current.apiTokenInfo).to.deep.equal(null) act(() => { - result.current.initiateRecreateToken() + void result.current.recreateToken() }) await act(() => { @@ -44,16 +44,16 @@ describe('useRecreateApiToken', () => { describe('Error Handling', () => { it('should handle error correctly when an error is thrown', async () => { const errorMessage = 'Failed to recreate API token.' - apiTokenInfoRepository.recreateApiToken = cy.stub().rejects(new Error(errorMessage)) + UserRepository.recreateApiToken = cy.stub().rejects(new Error(errorMessage)) - const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRecreateApiToken(UserRepository)) expect(result.current.isRecreating).to.equal(false) expect(result.current.error).to.equal(null) expect(result.current.apiTokenInfo).to.equal(null) act(() => { - result.current.initiateRecreateToken() + void result.current.recreateToken() }) await act(() => { @@ -69,16 +69,16 @@ describe('useRecreateApiToken', () => { }) it('should return correct error message when there is not an error type catched', async () => { - apiTokenInfoRepository.recreateApiToken = cy.stub().rejects('Unexpected error message') + UserRepository.recreateApiToken = cy.stub().rejects('Unexpected error message') - const { result } = renderHook(() => useRecreateApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRecreateApiToken(UserRepository)) expect(result.current.isRecreating).to.equal(false) expect(result.current.error).to.equal(null) expect(result.current.apiTokenInfo).to.equal(null) act(() => { - result.current.initiateRecreateToken() + void result.current.recreateToken() }) await act(() => { @@ -88,7 +88,9 @@ describe('useRecreateApiToken', () => { await act(() => { expect(result.current.isRecreating).to.equal(false) - expect(result.current.error).to.equal('Failed to recreate API token.') + expect(result.current.error).to.equal( + 'Something went wrong creating the api token. Try again later.' + ) return expect(result.current.apiTokenInfo).to.equal(null) }) }) diff --git a/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx b/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx index 57cf26b0b..c412b685e 100644 --- a/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx +++ b/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx @@ -1,18 +1,18 @@ import { act, renderHook } from '@testing-library/react' -import { ApiTokenInfoRepository } from '@/users/domain/repositories/ApiTokenInfoRepository' +import { UserRepository } from '@/users/domain/repositories/UserRepository' import { useRevokeApiToken } from '@/sections/account/api-token-section/useRevokeApiToken' describe('useRevokeApiToken', () => { - let apiTokenInfoRepository: ApiTokenInfoRepository + let UserRepository: UserRepository beforeEach(() => { - apiTokenInfoRepository = {} as ApiTokenInfoRepository + UserRepository = {} as UserRepository }) it('should revoke the API token successfully', async () => { - apiTokenInfoRepository.deleteApiToken = cy.stub().resolves() + UserRepository.deleteApiToken = cy.stub().resolves() - const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRevokeApiToken(UserRepository)) expect(result.current.isRevoking).to.equal(false) expect(result.current.error).to.equal(null) @@ -28,29 +28,31 @@ describe('useRevokeApiToken', () => { describe('Error Handling', () => { it('should handle error correctly when an error is thrown', async () => { const errorMessage = 'API token revocation failed.' - apiTokenInfoRepository.deleteApiToken = cy.stub().rejects(new Error(errorMessage)) + UserRepository.deleteApiToken = cy.stub().rejects(new Error(errorMessage)) - const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRevokeApiToken(UserRepository)) await act(async () => { await result.current.revokeToken() }) expect(result.current.isRevoking).to.equal(false) - expect(result.current.error).to.equal('Failed to revoke API token.') + expect(result.current.error).to.equal(errorMessage) }) it('should handle non-error rejection gracefully', async () => { - apiTokenInfoRepository.deleteApiToken = cy.stub().rejects('Unexpected error') + UserRepository.deleteApiToken = cy.stub().rejects('Unexpected error') - const { result } = renderHook(() => useRevokeApiToken(apiTokenInfoRepository)) + const { result } = renderHook(() => useRevokeApiToken(UserRepository)) await act(async () => { await result.current.revokeToken() }) expect(result.current.isRevoking).to.equal(false) - expect(result.current.error).to.equal('Failed to revoke API token.') + expect(result.current.error).to.equal( + 'Something went wrong revoking the api token. Try again later.' + ) }) }) }) diff --git a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts index 0683b3b70..162b2a329 100644 --- a/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/account/ApiTokenInfoJSDataverseRepository.spec.ts @@ -1,35 +1,36 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { ApiTokenInfoJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/ApiTokenInfoJSDataverseRepository' +import { UserJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' import { TestsUtils } from '../../shared/TestsUtils' chai.use(chaiAsPromised) const expect = chai.expect -const apiTokenInfoRepository = new ApiTokenInfoJSDataverseRepository() +const userRepository = new UserJSDataverseRepository() + describe('API Token Info JSDataverse Repository', () => { before(() => TestsUtils.setup()) beforeEach(() => TestsUtils.login()) it('revoke the API token', async () => { - await expect(apiTokenInfoRepository.deleteApiToken()).to.be.fulfilled + await expect(userRepository.deleteApiToken()).to.be.fulfilled }) it('create or recreate the API token and return the new token info', async () => { - const recreatedTokenInfo = await apiTokenInfoRepository.recreateApiToken() + const recreatedTokenInfo = await userRepository.recreateApiToken() if (!recreatedTokenInfo) { throw new Error('Failed to recreate API token') } expect(recreatedTokenInfo).to.have.property('apiToken').that.is.a('string') - expect(recreatedTokenInfo).to.have.property('expirationDate').that.is.a('string') + expect(recreatedTokenInfo).to.have.property('expirationDate').that.is.a('Date') }) it('fetch the current API token', async () => { - const tokenInfo = await apiTokenInfoRepository.getCurrentApiToken() + const tokenInfo = await userRepository.getCurrentApiToken() if (!tokenInfo) { throw new Error('API Token not found') } expect(tokenInfo).to.have.property('apiToken').that.is.a('string') - expect(tokenInfo).to.have.property('expirationDate').that.is.a('string') + expect(tokenInfo).to.have.property('expirationDate').that.is.a('Date') }) }) From 5612bcb983d04a134f727fba35801e840e7d2e6d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 6 Nov 2024 12:35:03 -0500 Subject: [PATCH 6/8] fix: add storybook and fix the factory repo --- src/sections/account/Account.tsx | 10 ++-- src/sections/account/AccountFactory.tsx | 5 +- .../api-token-section/ApiTokenSection.tsx | 7 +-- .../CreateUserRepositoryFactory.tsx | 15 ------ .../useGetCurrentApiToken.tsx | 8 +++- src/stories/account/Account.stories.tsx | 31 ++++++++++++- .../AccountPageMockErrorUserRepository.ts | 46 +++++++++++++++++++ .../AccountPageMockLoadingUserRepository.ts | 25 ++++++++++ .../account/AccountPageMockUserRepository.ts | 39 ++++++++++++++++ 9 files changed, 157 insertions(+), 29 deletions(-) delete mode 100644 src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx create mode 100644 src/stories/account/AccountPageMockErrorUserRepository.ts create mode 100644 src/stories/account/AccountPageMockLoadingUserRepository.ts create mode 100644 src/stories/account/AccountPageMockUserRepository.ts diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 32ee78e3b..a9e4a77a8 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -1,16 +1,18 @@ import { useTranslation } from 'react-i18next' import { Tabs } from '@iqss/dataverse-design-system' import { AccountHelper, AccountPanelTabKey } from './AccountHelper' -import { createUserRepository } from './api-token-section/CreateUserRepositoryFactory' +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { ApiTokenSection } from './api-token-section/ApiTokenSection' import styles from './Account.module.scss' const tabsKeys = AccountHelper.ACCOUNT_PANEL_TABS_KEYS interface AccountProps { defaultActiveTabKey: AccountPanelTabKey + userRepository: UserJSDataverseRepository } -export const Account = ({ defaultActiveTabKey }: AccountProps) => { +export const Account = ({ defaultActiveTabKey, userRepository }: AccountProps) => { const { t } = useTranslation('account') return ( @@ -33,7 +35,9 @@ export const Account = ({ defaultActiveTabKey }: AccountProps) => {
-
{createUserRepository.create()}
+
+ +
diff --git a/src/sections/account/AccountFactory.tsx b/src/sections/account/AccountFactory.tsx index 0ecbab6b5..d4c2a6e20 100644 --- a/src/sections/account/AccountFactory.tsx +++ b/src/sections/account/AccountFactory.tsx @@ -2,6 +2,9 @@ import { ReactElement } from 'react' import { useSearchParams } from 'react-router-dom' import { AccountHelper } from './AccountHelper' import { Account } from './Account' +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' + +const userRepository = new UserJSDataverseRepository() export class AccountFactory { static create(): ReactElement { @@ -13,5 +16,5 @@ function AccountWithSearchParams() { const [searchParams] = useSearchParams() const defaultActiveTabKey = AccountHelper.defineSelectedTabKey(searchParams) - return + return } diff --git a/src/sections/account/api-token-section/ApiTokenSection.tsx b/src/sections/account/api-token-section/ApiTokenSection.tsx index c0877d868..16fa563d6 100644 --- a/src/sections/account/api-token-section/ApiTokenSection.tsx +++ b/src/sections/account/api-token-section/ApiTokenSection.tsx @@ -20,12 +20,7 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { const { t } = useTranslation('account', { keyPrefix: 'apiToken' }) const [currentApiTokenInfo, setCurrentApiTokenInfo] = useState() - const { error, apiTokenInfo, isLoading } = useGetApiToken(repository) - - const getError = - error !== 'There was an error when reading the resource. Reason was: [404] Token not found.' - ? error - : null + const { error: getError, apiTokenInfo, isLoading } = useGetApiToken(repository) useEffect(() => { setCurrentApiTokenInfo(apiTokenInfo) diff --git a/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx b/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx deleted file mode 100644 index 76d27a728..000000000 --- a/src/sections/account/api-token-section/CreateUserRepositoryFactory.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { ReactElement } from 'react' -import { ApiTokenSection } from './ApiTokenSection' -import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' - -const userRepository = new UserJSDataverseRepository() - -export class createUserRepository { - static create(): ReactElement { - return - } -} - -function CreateUserRepository() { - return -} diff --git a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx index 1e2f0f571..beea77ca8 100644 --- a/src/sections/account/api-token-section/useGetCurrentApiToken.tsx +++ b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx @@ -31,7 +31,12 @@ export const useGetApiToken = (repository: UserRepository): UseGetApiTokenResult err instanceof Error && err.message ? err.message : 'Something went wrong getting the current api token. Try again later.' - setError(errorMessage) + setError( + errorMessage === + 'There was an error when reading the resource. Reason was: [404] Token not found.' + ? null + : errorMessage + ) } finally { setIsLoading(false) } @@ -40,6 +45,5 @@ export const useGetApiToken = (repository: UserRepository): UseGetApiTokenResult useEffect(() => { void fetchTokenInfo() }, [fetchTokenInfo]) - return { isLoading, error, apiTokenInfo } } diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index b19475721..cbe1e7d91 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -4,6 +4,9 @@ import { WithI18next } from '../WithI18next' import { WithLayout } from '../WithLayout' import { WithLoggedInUser } from '../WithLoggedInUser' import { AccountHelper } from '../../sections/account/AccountHelper' +import { AccountPageMockUserRepository } from './AccountPageMockUserRepository' +import { AccountPageMockLoadingUserRepository } from './AccountPageMockLoadingUserRepository' +import { AccountPageMockErrorUserRepository } from './AccountPageMockErrorUserRepository' const meta: Meta = { title: 'Pages/Account', @@ -14,9 +17,33 @@ const meta: Meta = { chromatic: { delay: 15000, pauseAnimationAtEnd: true } } } + export default meta type Story = StoryObj -export const APITokenTab: Story = { - render: () => +export const APITokenTabDefault: Story = { + render: () => ( + + ) +} + +export const APITokenTabLoading: Story = { + render: () => ( + + ) +} + +export const APITokenTabError: Story = { + render: () => ( + + ) } diff --git a/src/stories/account/AccountPageMockErrorUserRepository.ts b/src/stories/account/AccountPageMockErrorUserRepository.ts new file mode 100644 index 000000000..7662eb4ed --- /dev/null +++ b/src/stories/account/AccountPageMockErrorUserRepository.ts @@ -0,0 +1,46 @@ +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { User } from '@/users/domain/models/User' +import { FakerHelper } from '@tests/component/shared/FakerHelper' + +export class AccountPageMockErrorUserRepository extends UserJSDataverseRepository { + getAuthenticated(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong getting authentication. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } + + removeAuthenticated(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong removing authentication. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } + + getCurrentApiToken(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong getting the current api token. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } + + recreateApiToken(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong creating the api token. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } + + deleteApiToken(): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong revoking the api token. Try again later.') + }, FakerHelper.loadingTimout()) + }) + } +} diff --git a/src/stories/account/AccountPageMockLoadingUserRepository.ts b/src/stories/account/AccountPageMockLoadingUserRepository.ts new file mode 100644 index 000000000..57668a59f --- /dev/null +++ b/src/stories/account/AccountPageMockLoadingUserRepository.ts @@ -0,0 +1,25 @@ +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { User } from '@/users/domain/models/User' + +export class AccountPageMockLoadingUserRepository extends UserJSDataverseRepository { + getAuthenticated(): Promise { + return new Promise(() => {}) + } + + removeAuthenticated(): Promise { + return new Promise(() => {}) + } + + getCurrentApiToken(): Promise { + return new Promise(() => {}) + } + + recreateApiToken(): Promise { + return new Promise(() => {}) + } + + deleteApiToken(): Promise { + return new Promise(() => {}) + } +} diff --git a/src/stories/account/AccountPageMockUserRepository.ts b/src/stories/account/AccountPageMockUserRepository.ts new file mode 100644 index 000000000..fdbf578b1 --- /dev/null +++ b/src/stories/account/AccountPageMockUserRepository.ts @@ -0,0 +1,39 @@ +import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/UserJSDataverseRepository' +import { TokenInfo } from '@/users/domain/models/TokenInfo' +import { User } from '@/users/domain/models/User' + +export class AccountPageMockUserRepository extends UserJSDataverseRepository { + getAuthenticated(): Promise { + return Promise.resolve({ + displayName: 'mockDisplayName', + persistentId: 'mockPersistentId', + firstName: 'mockFirstName', + lastName: 'mockLastName', + email: 'mockEmail', + affiliation: 'mockAffiliation', + superuser: true + }) + } + + removeAuthenticated(): Promise { + return Promise.resolve() + } + + getCurrentApiToken(): Promise { + return Promise.resolve({ + apiToken: 'mock api token', + expirationDate: new Date() + }) + } + + recreateApiToken(): Promise { + return Promise.resolve({ + apiToken: 'updated mock api token', + expirationDate: new Date() + }) + } + + deleteApiToken(): Promise { + return Promise.resolve() + } +} From cf7d072de94f6654f2b775c93f82d0be975a5a90 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 7 Nov 2024 08:49:19 -0500 Subject: [PATCH 7/8] fix: add another storybook --- src/stories/account/Account.stories.tsx | 23 +++++++++++++++++++ .../sections/account/Account.spec.tsx | 6 ++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx index cbe1e7d91..3d0817452 100644 --- a/src/stories/account/Account.stories.tsx +++ b/src/stories/account/Account.stories.tsx @@ -47,3 +47,26 @@ export const APITokenTabError: Story = { /> ) } + +export const APITokenTabNoToken: Story = { + render: () => { + const noTokenRepository = new AccountPageMockUserRepository() + noTokenRepository.getCurrentApiToken = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + apiToken: '', + expirationDate: new Date() + }) + }, 1_000) + }) + } + + return ( + + ) + } +} diff --git a/tests/component/sections/account/Account.spec.tsx b/tests/component/sections/account/Account.spec.tsx index 3a3176ba4..bd32411fd 100644 --- a/tests/component/sections/account/Account.spec.tsx +++ b/tests/component/sections/account/Account.spec.tsx @@ -1,10 +1,14 @@ import { Account } from '../../../../src/sections/account/Account' import { AccountHelper } from '../../../../src/sections/account/AccountHelper' +import { UserJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/UserJSDataverseRepository' describe('Account', () => { it('should render the component', () => { cy.mountAuthenticated( - + ) cy.get('h1').should('contain.text', 'Account') From 4cedb79bd61adcd00105a3a83dd6636f4f5648f7 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 7 Nov 2024 16:54:24 -0500 Subject: [PATCH 8/8] fix: copy clipboard should work after recreating token --- src/sections/account/api-token-section/ApiTokenSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sections/account/api-token-section/ApiTokenSection.tsx b/src/sections/account/api-token-section/ApiTokenSection.tsx index 16fa563d6..f63edb090 100644 --- a/src/sections/account/api-token-section/ApiTokenSection.tsx +++ b/src/sections/account/api-token-section/ApiTokenSection.tsx @@ -54,7 +54,7 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { } const copyToClipboard = () => { - navigator.clipboard.writeText(apiTokenInfo.apiToken).catch( + navigator.clipboard.writeText(currentApiTokenInfo?.apiToken ?? '').catch( /* istanbul ignore next */ (error) => { console.error('Failed to copy text:', error) }