()
+
+ const { error: getError, apiTokenInfo, isLoading } = useGetApiToken(repository)
+
+ useEffect(() => {
+ setCurrentApiTokenInfo(apiTokenInfo)
+ }, [apiTokenInfo])
+
+ const {
+ recreateToken,
+ isRecreating,
+ error: recreatingError,
+ apiTokenInfo: updatedTokenInfo
+ } = useRecreateApiToken(repository)
+
+ useEffect(() => {
+ if (updatedTokenInfo) {
+ setCurrentApiTokenInfo(updatedTokenInfo)
+ }
+ }, [updatedTokenInfo])
+
+ const handleCreateToken = () => {
+ void recreateToken()
+ }
- // 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: new Date(0)
+ })
+ }
const copyToClipboard = () => {
- navigator.clipboard.writeText(apiToken).catch(
+ navigator.clipboard.writeText(currentApiTokenInfo?.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 +90,27 @@ export const ApiTokenSection = () => {
}}
/>
- {apiToken ? (
+ {currentApiTokenInfo?.apiToken ? (
<>
- {t('expirationDate')}
+ {t('expirationDate')}{' '}
+
- {apiToken}
+ {currentApiTokenInfo.apiToken}
-
@@ -60,8 +120,8 @@ export const ApiTokenSection = () => {
{t('notCreatedApiToken')}
-
-
+
+
{t('createToken')}
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..435eacd1e
--- /dev/null
+++ b/src/sections/account/api-token-section/ApiTokenSectionSkeleton.tsx
@@ -0,0 +1,49 @@
+import { Trans, useTranslation } from 'react-i18next'
+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'
+
+export const ApiTokenSectionSkeleton = () => {
+ const { t } = useTranslation('account', { keyPrefix: 'apiToken' })
+
+ return (
+ <>
+
+
+ )
+ }}
+ />
+
+
+
+
+ {t('expirationDate')}{' '}
+
+
+
+
+
+
+
+
+ {t('copyToClipboard')}
+ {t('recreateToken')}
+ {t('revokeToken')}
+
+
+
+ >
+ )
+}
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..beea77ca8
--- /dev/null
+++ b/src/sections/account/api-token-section/useGetCurrentApiToken.tsx
@@ -0,0 +1,49 @@
+import { useState, useEffect, useCallback } from 'react'
+import { TokenInfo } from '@/users/domain/models/TokenInfo'
+import { UserRepository } from '@/users/domain/repositories/UserRepository'
+import { getCurrentApiToken } from '@/users/domain/useCases/getCurrentApiToken'
+
+interface UseGetApiTokenResult {
+ apiTokenInfo: TokenInfo
+ isLoading: boolean
+ error: string | null
+}
+
+export const useGetApiToken = (repository: UserRepository): UseGetApiTokenResult => {
+ const [apiTokenInfo, setApiTokenInfo] = useState({
+ apiToken: '',
+ expirationDate: new Date(0)
+ })
+ 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
+ ? err.message
+ : 'Something went wrong getting the current api token. Try again later.'
+ setError(
+ errorMessage ===
+ 'There was an error when reading the resource. Reason was: [404] Token not found.'
+ ? null
+ : errorMessage
+ )
+ } finally {
+ setIsLoading(false)
+ }
+ }, [repository])
+
+ useEffect(() => {
+ void fetchTokenInfo()
+ }, [fetchTokenInfo])
+ return { isLoading, error, apiTokenInfo }
+}
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..d957554a2
--- /dev/null
+++ b/src/sections/account/api-token-section/useRecreateApiToken.tsx
@@ -0,0 +1,36 @@
+import { useState } from 'react'
+import { recreateApiToken } from '@/users/domain/useCases/recreateApiToken'
+import { TokenInfo } from '@/users/domain/models/TokenInfo'
+import { UserRepository } from '@/users/domain/repositories/UserRepository'
+
+interface UseRecreateApiTokenResult {
+ recreateToken: () => Promise
+ isRecreating: boolean
+ error: string | null
+ apiTokenInfo: TokenInfo | null
+}
+
+export const useRecreateApiToken = (repository: UserRepository): UseRecreateApiTokenResult => {
+ const [isRecreating, setIsRecreating] = useState(false)
+ const [error, setError] = useState(null)
+ const [apiTokenInfo, setApiTokenInfo] = useState(null)
+
+ 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)
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..4bbb6ec85
--- /dev/null
+++ b/src/sections/account/api-token-section/useRevokeApiToken.tsx
@@ -0,0 +1,32 @@
+import { useState } from 'react'
+import { revokeApiToken } from '@/users/domain/useCases/revokeApiToken'
+import { UserRepository } from '@/users/domain/repositories/UserRepository'
+
+interface UseRevokeApiTokenResult {
+ revokeToken: () => Promise
+ isRevoking: boolean
+ error: string | null
+}
+
+export const useRevokeApiToken = (repository: UserRepository): UseRevokeApiTokenResult => {
+ const [isRevoking, setIsRevoking] = useState(false)
+ const [error, setError] = useState(null)
+
+ const revokeToken = async () => {
+ setIsRevoking(true)
+ setError(null)
+ try {
+ await revokeApiToken(repository)
+ } catch (err) {
+ const errorMessage =
+ err instanceof Error && err.message
+ ? err.message
+ : 'Something went wrong revoking the api token. Try again later.'
+ setError(errorMessage)
+ } finally {
+ setIsRevoking(false)
+ }
+ }
+
+ return { revokeToken, isRevoking, error }
+}
diff --git a/src/stories/account/Account.stories.tsx b/src/stories/account/Account.stories.tsx
index b19475721..3d0817452 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,56 @@ 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: () => (
+
+ )
+}
+
+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/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()
+ }
+}
diff --git a/src/users/domain/models/TokenInfo.ts b/src/users/domain/models/TokenInfo.ts
new file mode 100644
index 000000000..1916f5e19
--- /dev/null
+++ b/src/users/domain/models/TokenInfo.ts
@@ -0,0 +1,4 @@
+export interface TokenInfo {
+ apiToken: string
+ expirationDate: Date
+}
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
new file mode 100644
index 000000000..d3b8dfb08
--- /dev/null
+++ b/src/users/domain/useCases/getCurrentApiToken.ts
@@ -0,0 +1,6 @@
+import { TokenInfo } from '../models/TokenInfo'
+import { UserRepository } from '../repositories/UserRepository'
+
+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
new file mode 100644
index 000000000..9c1029e62
--- /dev/null
+++ b/src/users/domain/useCases/recreateApiToken.ts
@@ -0,0 +1,6 @@
+import { TokenInfo } from '../models/TokenInfo'
+import { UserRepository } from '../repositories/UserRepository'
+
+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
new file mode 100644
index 000000000..47a895045
--- /dev/null
+++ b/src/users/domain/useCases/revokeApiToken.ts
@@ -0,0 +1,5 @@
+import { UserRepository } from '../repositories/UserRepository'
+
+export function revokeApiToken(userRepository: UserRepository): Promise {
+ return userRepository.deleteApiToken()
+}
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/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')
diff --git a/tests/component/sections/account/ApiTokenSection.spec.tsx b/tests/component/sections/account/ApiTokenSection.spec.tsx
index 26ff3220b..83547deee 100644
--- a/tests/component/sections/account/ApiTokenSection.spec.tsx
+++ b/tests/component/sections/account/ApiTokenSection.spec.tsx
@@ -1,8 +1,41 @@
import { ApiTokenSection } from '../../../../src/sections/account/api-token-section/ApiTokenSection'
+import { DateHelper } from '@/shared/helpers/DateHelper'
+import { UserRepository } from '@/users/domain/repositories/UserRepository'
describe('ApiTokenSection', () => {
+ const mockApiTokenInfo = {
+ apiToken: 'mocked-api',
+ expirationDate: new Date('2024-12-31')
+ }
+ const newMockApiTokenInfo = {
+ apiToken: 'new-mocked-api',
+ expirationDate: new Date('2025-12-31')
+ }
+
+ let apiTokenRepository: UserRepository
+
beforeEach(() => {
- cy.mountAuthenticated()
+ apiTokenRepository = {
+ getCurrentApiToken: cy.stub().resolves(mockApiTokenInfo),
+ recreateApiToken: cy.stub().resolves(mockApiTokenInfo),
+ deleteApiToken: cy.stub().resolves(),
+ getAuthenticated: cy.stub().resolves(),
+ removeAuthenticated: cy.stub().resolves()
+ }
+
+ cy.mountAuthenticated()
+ })
+
+ it('should show the loading skeleton while fetching the token', () => {
+ apiTokenRepository.getCurrentApiToken = cy.stub().callsFake(() => {
+ return Cypress.Promise.delay(500).then(() => mockApiTokenInfo)
+ })
+
+ cy.mount()
+ cy.get('[data-testid="loadingSkeleton"]').should('exist')
+
+ cy.wait(500)
+ cy.get('[data-testid="loadingSkeleton"]').should('not.exist')
})
it('should copy the api token to the clipboard', () => {
@@ -20,5 +53,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',
+ DateHelper.toISO8601Format(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',
+ DateHelper.toISO8601Format(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..53768c586
--- /dev/null
+++ b/tests/component/sections/account/useGetApiTokenInfo.spec.tsx
@@ -0,0 +1,85 @@
+import { act, renderHook } from '@testing-library/react'
+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 UserRepository: UserRepository
+
+ const mockTokenInfo: TokenInfo = {
+ apiToken: 'mocked-api-token',
+ expirationDate: new Date('2024-11-05')
+ }
+
+ beforeEach(() => {
+ UserRepository = {} as UserRepository
+ })
+
+ it('should return the API token correctly', async () => {
+ UserRepository.getCurrentApiToken = cy.stub().resolves(mockTokenInfo)
+
+ 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: new Date(0)
+ })
+ })
+
+ await act(() => {
+ expect(result.current.isLoading).to.equal(false)
+ expect(result.current.error).to.equal(null)
+ 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 () => {
+ UserRepository.getCurrentApiToken = cy.stub().rejects(new Error('API Error'))
+
+ const { result } = renderHook(() => useGetApiToken(UserRepository))
+
+ 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 () => {
+ UserRepository.getCurrentApiToken = cy.stub().rejects('Error message')
+
+ const { result } = renderHook(() => useGetApiToken(UserRepository))
+
+ 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(
+ '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
new file mode 100644
index 000000000..c3b354e76
--- /dev/null
+++ b/tests/component/sections/account/useRecreateApiTokenInfo.spec.tsx
@@ -0,0 +1,98 @@
+import { act, renderHook } from '@testing-library/react'
+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 UserRepository: UserRepository
+
+ const mockTokenInfo: TokenInfo = {
+ apiToken: 'new-mocked-api-token',
+ expirationDate: new Date('2024-12-31')
+ }
+
+ beforeEach(() => {
+ UserRepository = {} as UserRepository
+ })
+
+ it('should return the API token correctly', async () => {
+ UserRepository.recreateApiToken = cy.stub().resolves(mockTokenInfo)
+
+ 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(() => {
+ void result.current.recreateToken()
+ })
+
+ 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.'
+ UserRepository.recreateApiToken = cy.stub().rejects(new Error(errorMessage))
+
+ 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(() => {
+ void result.current.recreateToken()
+ })
+
+ 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 () => {
+ UserRepository.recreateApiToken = cy.stub().rejects('Unexpected error message')
+
+ 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(() => {
+ void result.current.recreateToken()
+ })
+
+ 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(
+ '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
new file mode 100644
index 000000000..c412b685e
--- /dev/null
+++ b/tests/component/sections/account/useRevokeApiTokenInfo.spec.tsx
@@ -0,0 +1,58 @@
+import { act, renderHook } from '@testing-library/react'
+import { UserRepository } from '@/users/domain/repositories/UserRepository'
+import { useRevokeApiToken } from '@/sections/account/api-token-section/useRevokeApiToken'
+
+describe('useRevokeApiToken', () => {
+ let UserRepository: UserRepository
+
+ beforeEach(() => {
+ UserRepository = {} as UserRepository
+ })
+
+ it('should revoke the API token successfully', async () => {
+ UserRepository.deleteApiToken = cy.stub().resolves()
+
+ const { result } = renderHook(() => useRevokeApiToken(UserRepository))
+
+ 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.'
+ UserRepository.deleteApiToken = cy.stub().rejects(new Error(errorMessage))
+
+ 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(errorMessage)
+ })
+
+ it('should handle non-error rejection gracefully', async () => {
+ UserRepository.deleteApiToken = cy.stub().rejects('Unexpected error')
+
+ 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(
+ '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
new file mode 100644
index 000000000..162b2a329
--- /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 { UserJSDataverseRepository } from '../../../../src/users/infrastructure/repositories/UserJSDataverseRepository'
+
+import { TestsUtils } from '../../shared/TestsUtils'
+
+chai.use(chaiAsPromised)
+const expect = chai.expect
+
+const userRepository = new UserJSDataverseRepository()
+
+describe('API Token Info JSDataverse Repository', () => {
+ before(() => TestsUtils.setup())
+ beforeEach(() => TestsUtils.login())
+ it('revoke the API token', async () => {
+ await expect(userRepository.deleteApiToken()).to.be.fulfilled
+ })
+
+ it('create or recreate the API token and return the new token info', async () => {
+ 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('Date')
+ })
+
+ it('fetch the current API token', async () => {
+ 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('Date')
+ })
+})