From 6ade2c3ac877e76901d4a82cd9bd7c178f1fa164 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Mon, 28 Oct 2024 12:51:09 -0400 Subject: [PATCH] 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 + }) +})