diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3a2c92..c15a4d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel - New Use Case: [Get a Template](./docs/useCases.md#get-a-template) under Templates. - New Use Case: [Delete a Template](./docs/useCases.md#delete-a-template) under Templates. - New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access). +- Guestbooks: Added use cases and repository support for guestbook creation, listing, and enabling/disabling. +- Guestbooks: Added dataset-level guestbook assignment and removal support via `assignDatasetGuestbook` (`PUT /api/datasets/{identifier}/guestbook`) and `removeDatasetGuestbook` (`DELETE /api/datasets/{identifier}/guestbook`). +- Datasets/Guestbooks: Added `guestbookId` in `getDataset` responses. +- Access: Added signed-URL GET use cases for `access/datafile`, `access/datafiles`, `access/dataset`, and `access/dataset/{id}/versions/{versionId}` endpoints using `?signed=true` (authenticated users only). +- Access: Added a dedicated `access` module for guestbook-at-request and download terms/guestbook submission endpoints. ### Changed diff --git a/docs/useCases.md b/docs/useCases.md index f77a40e1..72ea5a23 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -123,6 +123,26 @@ The different use cases currently available in the package are classified below, - [Get External Tools](#get-external-tools) - [Get Dataset External Tool Resolved](#get-dataset-external-tool-resolved) - [Get File External Tool Resolved](#get-file-external-tool-resolved) +- [Guestbooks](#Guestbooks) + - [Guestbooks read use cases](#guestbooks-read-use-cases) + - [Get a Guestbook](#get-a-guestbook) + - [Get Guestbooks By Collection Id](#get-guestbooks-by-collection-id) + - [Guestbooks write use cases](#guestbooks-write-use-cases) + - [Create a Guestbook](#create-a-guestbook) + - [Set Guestbook Enabled](#set-guestbook-enabled) + - [Assign Dataset Guestbook](#assign-dataset-guestbook) + - [Remove Dataset Guestbook](#remove-dataset-guestbook) +- [Access](#Access) + - [Access read use cases](#access-read-use-cases) + - [Get Signed Datafile Download URL](#get-signed-datafile-download-url) + - [Get Signed Datafiles Download URL](#get-signed-datafiles-download-url) + - [Get Signed Dataset Download URL](#get-signed-dataset-download-url) + - [Get Signed Dataset Version Download URL](#get-signed-dataset-version-download-url) + - [Access write use cases](#access-write-use-cases) + - [Submit Guestbook For Datafile Download](#submit-guestbook-for-datafile-download) + - [Submit Guestbook For Datafiles Download](#submit-guestbook-for-datafiles-download) + - [Submit Guestbook For Dataset Download](#submit-guestbook-for-dataset-download) + - [Submit Guestbook For Dataset Version Download](#submit-guestbook-for-dataset-version-download) ## Collections @@ -2767,3 +2787,312 @@ getFileExternalToolResolved ``` _See [use case](../src/externalTools/domain/useCases/GetfileExternalToolResolved.ts) implementation_. + +## Guestbooks + +### Guestbooks Read Use Cases + +#### Get a Guestbook + +Returns a [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) by its id. + +##### Example call: + +```typescript +import { getGuestbook } from '@iqss/dataverse-client-javascript' + +const guestbookId = 123 + +getGuestbook.execute(guestbookId).then((guestbook: Guestbook) => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/GetGuestbook.ts) implementation_. + +#### Get Guestbooks By Collection Id + +Returns all [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) entries available for a collection. + +##### Example call: + +```typescript +import { getGuestbooksByCollectionId } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' + +getGuestbooksByCollectionId.execute(collectionIdOrAlias).then((guestbooks: Guestbook[]) => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts) implementation_. + +### Guestbooks Write Use Cases + +#### Create a Guestbook + +Creates a guestbook on a collection using [CreateGuestbookDTO](../src/guestbooks/domain/dtos/CreateGuestbookDTO.ts). + +##### Example call: + +```typescript +import { createGuestbook } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' +const guestbook: CreateGuestbookDTO = { + name: 'my test guestbook', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + email: 'test@gmail.com', + institution: 'Harvard University', + position: 'Researcher', + customQuestions: [ + { + question: 'Describe yourself', + required: false, + displayOrder: 1, + type: 'textarea', + hidden: false + } + ] +} + +createGuestbook.execute(guestbook, collectionIdOrAlias).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/CreateGuestbook.ts) implementation_. + +#### Set Guestbook Enabled + +Enables or disables a guestbook in a collection. + +##### Example call: + +```typescript +import { setGuestbookEnabled } from '@iqss/dataverse-client-javascript' + +const collectionIdOrAlias = 'root' +const guestbookId = 123 + +setGuestbookEnabled.execute(collectionIdOrAlias, guestbookId, false).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/SetGuestbookEnabled.ts) implementation_. + +#### Assign Dataset Guestbook + +Assigns a guestbook to a dataset using `PUT /api/datasets/{identifier}/guestbook`. + +##### Example call: + +```typescript +import { assignDatasetGuestbook } from '@iqss/dataverse-client-javascript' + +const datasetIdOrPersistentId = 123 +const guestbookId = 456 + +assignDatasetGuestbook.execute(datasetIdOrPersistentId, guestbookId).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts) implementation_. + +#### Remove Dataset Guestbook + +Removes the guestbook assignment for a dataset using `DELETE /api/datasets/{identifier}/guestbook`. + +##### Example call: + +```typescript +import { removeDatasetGuestbook } from '@iqss/dataverse-client-javascript' + +const datasetIdOrPersistentId = 123 + +removeDatasetGuestbook.execute(datasetIdOrPersistentId).then(() => { + /* ... */ +}) +``` + +_See [use case](../src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts) implementation_. + +## Access + +### Access Read Use Cases + +#### Get Signed Datafile Download URL + +Returns a signed URL for downloading a single datafile. + +##### Example call: + +```typescript +import { getSignedDatafileDownloadUrl } from '@iqss/dataverse-client-javascript' + +getSignedDatafileDownloadUrl.execute(10).then((signedUrl: string) => { + /* ... */ +}) +``` + +_See [use case](../src/access/domain/useCases/GetSignedDatafileDownloadUrl.ts) implementation_. + +#### Get Signed Datafiles Download URL + +Returns a signed URL for downloading multiple datafiles. + +##### Example call: + +```typescript +import { getSignedDatafilesDownloadUrl } from '@iqss/dataverse-client-javascript' + +getSignedDatafilesDownloadUrl.execute([10, 11]).then((signedUrl: string) => { + /* ... */ +}) +``` + +_See [use case](../src/access/domain/useCases/GetSignedDatafilesDownloadUrl.ts) implementation_. + +#### Get Signed Dataset Download URL + +Returns a signed URL for downloading a dataset. + +##### Example call: + +```typescript +import { getSignedDatasetDownloadUrl } from '@iqss/dataverse-client-javascript' + +getSignedDatasetDownloadUrl.execute(10).then((signedUrl: string) => { + /* ... */ +}) +``` + +_See [use case](../src/access/domain/useCases/GetSignedDatasetDownloadUrl.ts) implementation_. + +#### Get Signed Dataset Version Download URL + +Returns a signed URL for downloading a dataset version. + +##### Example call: + +```typescript +import { getSignedDatasetVersionDownloadUrl } from '@iqss/dataverse-client-javascript' + +getSignedDatasetVersionDownloadUrl.execute(10, '1.0').then((signedUrl: string) => { + /* ... */ +}) +``` + +_See [use case](../src/access/domain/useCases/GetSignedDatasetVersionDownloadUrl.ts) implementation_. + +### Access Write Use Cases + +#### Submit Guestbook For Datafile Download + +Submits guestbook answers for a datafile and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatafileDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatafileDownload + .execute(10, { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] } + ] + } + }) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts) implementation_. + +#### Submit Guestbook For Datafiles Download + +Submits guestbook answers for multiple files and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatafilesDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatafilesDownload + .execute([10, 11], { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts) implementation_. + +#### Submit Guestbook For Dataset Download + +Submits guestbook answers for dataset download and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatasetDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatasetDownload + .execute('doi:10.5072/FK2/XXXXXX', { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts) implementation_. + +#### Submit Guestbook For Dataset Version Download + +Submits guestbook answers for a specific dataset version and returns a signed URL. + +##### Example call: + +```typescript +import { submitGuestbookForDatasetVersionDownload } from '@iqss/dataverse-client-javascript' + +submitGuestbookForDatasetVersionDownload + .execute(10, ':latest', { + guestbookResponse: { + answers: [ + { id: 123, value: 'Good' }, + { id: 124, value: ['Multi', 'Line'] }, + { id: 125, value: 'Yellow' } + ] + } + }) + .then((signedUrl: string) => { + /* ... */ + }) +``` + +_See [use case](../src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts) implementation_. diff --git a/src/access/domain/dtos/GuestbookResponseDTO.ts b/src/access/domain/dtos/GuestbookResponseDTO.ts new file mode 100644 index 00000000..7e29d562 --- /dev/null +++ b/src/access/domain/dtos/GuestbookResponseDTO.ts @@ -0,0 +1,10 @@ +export interface GuestbookAnswerDTO { + id: number | string + value: string | string[] +} + +export interface GuestbookResponseDTO { + guestbookResponse: { + answers: GuestbookAnswerDTO[] + } +} diff --git a/src/access/domain/repositories/IAccessRepository.ts b/src/access/domain/repositories/IAccessRepository.ts new file mode 100644 index 00000000..438a3da4 --- /dev/null +++ b/src/access/domain/repositories/IAccessRepository.ts @@ -0,0 +1,32 @@ +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' + +export interface IAccessRepository { + getSignedDatafileDownloadUrl(fileId: number | string): Promise + + getSignedDatafilesDownloadUrl(fileIds: string | Array): Promise + + getSignedDatasetDownloadUrl(datasetId: number | string): Promise + + getSignedDatasetVersionDownloadUrl(datasetId: number | string, versionId: string): Promise + + submitGuestbookForDatafileDownload( + fileId: number | string, + guestbookResponse: GuestbookResponseDTO + ): Promise + + submitGuestbookForDatafilesDownload( + fileIds: string | Array, + guestbookResponse: GuestbookResponseDTO + ): Promise + + submitGuestbookForDatasetDownload( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO + ): Promise + + submitGuestbookForDatasetVersionDownload( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO + ): Promise +} diff --git a/src/access/domain/useCases/GetSignedDatafileDownloadUrl.ts b/src/access/domain/useCases/GetSignedDatafileDownloadUrl.ts new file mode 100644 index 00000000..9a3b273f --- /dev/null +++ b/src/access/domain/useCases/GetSignedDatafileDownloadUrl.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class GetSignedDatafileDownloadUrl implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Returns a signed URL for downloading a single datafile. + * + * @param {number | string} fileId - Datafile identifier (numeric id or persistent id). + * @returns {Promise} - Signed URL for the download. + */ + async execute(fileId: number | string): Promise { + return await this.accessRepository.getSignedDatafileDownloadUrl(fileId) + } +} diff --git a/src/access/domain/useCases/GetSignedDatafilesDownloadUrl.ts b/src/access/domain/useCases/GetSignedDatafilesDownloadUrl.ts new file mode 100644 index 00000000..f9ac5a02 --- /dev/null +++ b/src/access/domain/useCases/GetSignedDatafilesDownloadUrl.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class GetSignedDatafilesDownloadUrl implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Returns a signed URL for downloading multiple datafiles. + * + * @param {string | Array} fileIds - Comma-separated ids or array of datafile ids. + * @returns {Promise} - Signed URL for the download. + */ + async execute(fileIds: string | Array): Promise { + return await this.accessRepository.getSignedDatafilesDownloadUrl(fileIds) + } +} diff --git a/src/access/domain/useCases/GetSignedDatasetDownloadUrl.ts b/src/access/domain/useCases/GetSignedDatasetDownloadUrl.ts new file mode 100644 index 00000000..2328419e --- /dev/null +++ b/src/access/domain/useCases/GetSignedDatasetDownloadUrl.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class GetSignedDatasetDownloadUrl implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Returns a signed URL for downloading all files in a dataset. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @returns {Promise} - Signed URL for the download. + */ + async execute(datasetId: number | string): Promise { + return await this.accessRepository.getSignedDatasetDownloadUrl(datasetId) + } +} diff --git a/src/access/domain/useCases/GetSignedDatasetVersionDownloadUrl.ts b/src/access/domain/useCases/GetSignedDatasetVersionDownloadUrl.ts new file mode 100644 index 00000000..08c8b444 --- /dev/null +++ b/src/access/domain/useCases/GetSignedDatasetVersionDownloadUrl.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class GetSignedDatasetVersionDownloadUrl implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Returns a signed URL for downloading all files in a specific dataset version. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @param {string} versionId - Dataset version id. + * @returns {Promise} - Signed URL for the download. + */ + async execute(datasetId: number | string, versionId: string): Promise { + return await this.accessRepository.getSignedDatasetVersionDownloadUrl(datasetId, versionId) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts new file mode 100644 index 00000000..357da78f --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatafileDownload.ts @@ -0,0 +1,18 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatafileDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for a single datafile download request and returns a signed URL. + * + * @param {number | string} fileId - Datafile identifier (numeric id or persistent id). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @returns {Promise} - Signed URL for the download. + */ + async execute(fileId: number | string, guestbookResponse: GuestbookResponseDTO): Promise { + return await this.accessRepository.submitGuestbookForDatafileDownload(fileId, guestbookResponse) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts new file mode 100644 index 00000000..d682da8d --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatafilesDownload.ts @@ -0,0 +1,24 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatafilesDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for multiple datafiles download request and returns a signed URL. + * + * @param {string | Array} fileIds - Comma-separated string or array of file ids. + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + fileIds: string | Array, + guestbookResponse: GuestbookResponseDTO + ): Promise { + return await this.accessRepository.submitGuestbookForDatafilesDownload( + fileIds, + guestbookResponse + ) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts new file mode 100644 index 00000000..1ac3113e --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatasetDownload.ts @@ -0,0 +1,24 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatasetDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for dataset download request and returns a signed URL. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO + ): Promise { + return await this.accessRepository.submitGuestbookForDatasetDownload( + datasetId, + guestbookResponse + ) + } +} diff --git a/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts b/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts new file mode 100644 index 00000000..3d811f14 --- /dev/null +++ b/src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload.ts @@ -0,0 +1,27 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponseDTO } from '../dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../repositories/IAccessRepository' + +export class SubmitGuestbookForDatasetVersionDownload implements UseCase { + constructor(private readonly accessRepository: IAccessRepository) {} + + /** + * Submits a guestbook response for a specific dataset version download request and returns a signed URL. + * + * @param {number | string} datasetId - Dataset identifier (numeric id or persistent id). + * @param {string} versionId - Dataset version identifier (for example, ':latest' or '1.0'). + * @param {GuestbookResponseDTO} guestbookResponse - Guestbook response payload. + * @returns {Promise} - Signed URL for the download. + */ + async execute( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO + ): Promise { + return await this.accessRepository.submitGuestbookForDatasetVersionDownload( + datasetId, + versionId, + guestbookResponse + ) + } +} diff --git a/src/access/index.ts b/src/access/index.ts new file mode 100644 index 00000000..628e9cc3 --- /dev/null +++ b/src/access/index.ts @@ -0,0 +1,37 @@ +import { AccessRepository } from './infra/repositories/AccessRepository' +import { GetSignedDatafileDownloadUrl } from './domain/useCases/GetSignedDatafileDownloadUrl' +import { GetSignedDatafilesDownloadUrl } from './domain/useCases/GetSignedDatafilesDownloadUrl' +import { GetSignedDatasetDownloadUrl } from './domain/useCases/GetSignedDatasetDownloadUrl' +import { GetSignedDatasetVersionDownloadUrl } from './domain/useCases/GetSignedDatasetVersionDownloadUrl' +import { SubmitGuestbookForDatafileDownload } from './domain/useCases/SubmitGuestbookForDatafileDownload' +import { SubmitGuestbookForDatafilesDownload } from './domain/useCases/SubmitGuestbookForDatafilesDownload' +import { SubmitGuestbookForDatasetDownload } from './domain/useCases/SubmitGuestbookForDatasetDownload' +import { SubmitGuestbookForDatasetVersionDownload } from './domain/useCases/SubmitGuestbookForDatasetVersionDownload' + +const accessRepository = new AccessRepository() + +const getSignedDatafileDownloadUrl = new GetSignedDatafileDownloadUrl(accessRepository) +const getSignedDatafilesDownloadUrl = new GetSignedDatafilesDownloadUrl(accessRepository) +const getSignedDatasetDownloadUrl = new GetSignedDatasetDownloadUrl(accessRepository) +const getSignedDatasetVersionDownloadUrl = new GetSignedDatasetVersionDownloadUrl(accessRepository) +const submitGuestbookForDatafileDownload = new SubmitGuestbookForDatafileDownload(accessRepository) +const submitGuestbookForDatafilesDownload = new SubmitGuestbookForDatafilesDownload( + accessRepository +) +const submitGuestbookForDatasetDownload = new SubmitGuestbookForDatasetDownload(accessRepository) +const submitGuestbookForDatasetVersionDownload = new SubmitGuestbookForDatasetVersionDownload( + accessRepository +) + +export { + getSignedDatafileDownloadUrl, + getSignedDatafilesDownloadUrl, + getSignedDatasetDownloadUrl, + getSignedDatasetVersionDownloadUrl, + submitGuestbookForDatafileDownload, + submitGuestbookForDatafilesDownload, + submitGuestbookForDatasetDownload, + submitGuestbookForDatasetVersionDownload +} + +export { GuestbookResponseDTO } from './domain/dtos/GuestbookResponseDTO' diff --git a/src/access/infra/repositories/AccessRepository.ts b/src/access/infra/repositories/AccessRepository.ts new file mode 100644 index 00000000..429725f0 --- /dev/null +++ b/src/access/infra/repositories/AccessRepository.ts @@ -0,0 +1,137 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { GuestbookResponseDTO } from '../../domain/dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../../domain/repositories/IAccessRepository' + +export class AccessRepository extends ApiRepository implements IAccessRepository { + private readonly accessResourceName = 'access' + + public async getSignedDatafileDownloadUrl(fileId: number | string): Promise { + const endpoint = this.buildApiEndpoint(`${this.accessResourceName}/datafile`, undefined, fileId) + return this.doGet(endpoint, true, { signed: true }) + .then((response) => response.data.data.signedUrl) + .catch((error) => { + throw error + }) + } + + public async getSignedDatafilesDownloadUrl( + fileIds: string | Array + ): Promise { + return this.doGet( + this.buildApiEndpoint( + this.accessResourceName, + `datafiles/${Array.isArray(fileIds) ? fileIds.join(',') : fileIds}` + ), + true, + { signed: true } + ) + .then((response) => response.data.data.signedUrl) + .catch((error) => { + throw error + }) + } + + public async getSignedDatasetDownloadUrl(datasetId: number | string): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + undefined, + datasetId + ) + return this.doGet(endpoint, true, { signed: true }) + .then((response) => response.data.data.signedUrl) + .catch((error) => { + throw error + }) + } + + public async getSignedDatasetVersionDownloadUrl( + datasetId: number | string, + versionId: string + ): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + `versions/${versionId}`, + datasetId + ) + return this.doGet(endpoint, true, { signed: true }) + .then((response) => response.data.data.signedUrl) + .catch((error) => { + throw error + }) + } + + public async submitGuestbookForDatafileDownload( + fileId: number | string, + guestbookResponse: GuestbookResponseDTO + ): Promise { + const endpoint = this.buildApiEndpoint(`${this.accessResourceName}/datafile`, undefined, fileId) + return this.doPost(endpoint, guestbookResponse, { signed: true }) + .then((response) => { + const signedUrl = response.data.data.signedUrl + return signedUrl + }) + .catch((error) => { + throw error + }) + } + + public async submitGuestbookForDatafilesDownload( + fileIds: string | Array, + guestbookResponse: GuestbookResponseDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint( + this.accessResourceName, + `datafiles/${Array.isArray(fileIds) ? fileIds.join(',') : fileIds}` + ), + guestbookResponse, + { signed: true } + ) + .then((response) => { + const signedUrl = response.data.data.signedUrl + return signedUrl + }) + .catch((error) => { + throw error + }) + } + + public async submitGuestbookForDatasetDownload( + datasetId: number | string, + guestbookResponse: GuestbookResponseDTO + ): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + undefined, + datasetId + ) + return this.doPost(endpoint, guestbookResponse, { signed: true }) + .then((response) => { + const signedUrl = response.data.data.signedUrl + return signedUrl + }) + .catch((error) => { + throw error + }) + } + + public async submitGuestbookForDatasetVersionDownload( + datasetId: number | string, + versionId: string, + guestbookResponse: GuestbookResponseDTO + ): Promise { + const endpoint = this.buildApiEndpoint( + `${this.accessResourceName}/dataset`, + `versions/${versionId}`, + datasetId + ) + return this.doPost(endpoint, guestbookResponse, { signed: true }) + .then((response) => { + const signedUrl = response.data.data.signedUrl + return signedUrl + }) + .catch((error) => { + throw error + }) + } +} diff --git a/src/datasets/domain/models/Dataset.ts b/src/datasets/domain/models/Dataset.ts index e1ccfb25..ebd302f1 100644 --- a/src/datasets/domain/models/Dataset.ts +++ b/src/datasets/domain/models/Dataset.ts @@ -12,6 +12,7 @@ export interface Dataset { alternativePersistentId?: string publicationDate?: string citationDate?: string + guestbookId?: number metadataBlocks: DatasetMetadataBlocks isPartOf: DvObjectOwnerNode datasetType?: string diff --git a/src/datasets/infra/repositories/transformers/DatasetPayload.ts b/src/datasets/infra/repositories/transformers/DatasetPayload.ts index b0535677..347bc5d1 100644 --- a/src/datasets/infra/repositories/transformers/DatasetPayload.ts +++ b/src/datasets/infra/repositories/transformers/DatasetPayload.ts @@ -17,6 +17,7 @@ export interface DatasetPayload { alternativePersistentId?: string publicationDate?: string citationDate?: string + guestbookId?: number fileAccessRequest: boolean termsOfAccess?: string dataAccessPlace?: string diff --git a/src/datasets/infra/repositories/transformers/datasetTransformers.ts b/src/datasets/infra/repositories/transformers/datasetTransformers.ts index 7f186fb2..1c1e31d8 100644 --- a/src/datasets/infra/repositories/transformers/datasetTransformers.ts +++ b/src/datasets/infra/repositories/transformers/datasetTransformers.ts @@ -296,6 +296,9 @@ export const transformVersionPayloadToDataset = ( if ('citationDate' in versionPayload) { datasetModel.citationDate = versionPayload.citationDate } + if ('guestbookId' in versionPayload) { + datasetModel.guestbookId = versionPayload.guestbookId + } if ('datasetType' in versionPayload) { datasetModel.datasetType = versionPayload.datasetType } diff --git a/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts b/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts new file mode 100644 index 00000000..47028593 --- /dev/null +++ b/src/guestbooks/domain/dtos/CreateGuestbookDTO.ts @@ -0,0 +1,28 @@ +export type CreateGuestbookQuestionTypeDTO = 'text' | 'textarea' | 'options' + +export interface CreateGuestbookOptionDTO { + value: string + displayOrder: number +} + +export interface CreateGuestbookCustomQuestionDTO { + question: string + required: boolean + displayOrder: number + type: CreateGuestbookQuestionTypeDTO + hidden: boolean + optionValues?: CreateGuestbookOptionDTO[] +} + +export interface CreateGuestbookDTO { + name: string + enabled: boolean + emailRequired: boolean + nameRequired: boolean + institutionRequired: boolean + positionRequired: boolean + email: string + institution: string + position: string + customQuestions: CreateGuestbookCustomQuestionDTO[] +} diff --git a/src/guestbooks/domain/models/Guestbook.ts b/src/guestbooks/domain/models/Guestbook.ts new file mode 100644 index 00000000..e51cedb4 --- /dev/null +++ b/src/guestbooks/domain/models/Guestbook.ts @@ -0,0 +1,31 @@ +export type GuestbookQuestionType = 'text' | 'textarea' | 'options' + +export interface GuestbookOption { + value: string + displayOrder: number +} + +export interface GuestbookCustomQuestion { + question: string + required: boolean + displayOrder: number + type: GuestbookQuestionType + hidden: boolean + optionValues?: GuestbookOption[] +} + +export interface Guestbook { + id: number + name: string + enabled: boolean + emailRequired: boolean + nameRequired: boolean + institutionRequired: boolean + positionRequired: boolean + email: string + institution: string + position: string + customQuestions: GuestbookCustomQuestion[] + createTime: string + dataverseId: number +} diff --git a/src/guestbooks/domain/repositories/IGuestbooksRepository.ts b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts new file mode 100644 index 00000000..df8981b4 --- /dev/null +++ b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts @@ -0,0 +1,18 @@ +import { CreateGuestbookDTO } from '../dtos/CreateGuestbookDTO' +import { Guestbook } from '../models/Guestbook' + +export interface IGuestbooksRepository { + createGuestbook( + collectionIdOrAlias: number | string, + guestbook: CreateGuestbookDTO + ): Promise + getGuestbook(guestbookId: number): Promise + getGuestbooksByCollectionId(collectionIdOrAlias: number | string): Promise + setGuestbookEnabled( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise + assignDatasetGuestbook(datasetId: number | string, guestbookId: number): Promise + removeDatasetGuestbook(datasetId: number | string): Promise +} diff --git a/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts b/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts new file mode 100644 index 00000000..dbc880a4 --- /dev/null +++ b/src/guestbooks/domain/useCases/AssignDatasetGuestbook.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class AssignDatasetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Assigns a guestbook to a dataset. + * + * @param {number | string} datasetId - Dataset identifier (persistent id or numeric id). + * @param {number} guestbookId - Guestbook numeric identifier. + * @returns {Promise} + */ + async execute(datasetId: number | string, guestbookId: number): Promise { + return await this.guestbooksRepository.assignDatasetGuestbook(datasetId, guestbookId) + } +} diff --git a/src/guestbooks/domain/useCases/CreateGuestbook.ts b/src/guestbooks/domain/useCases/CreateGuestbook.ts new file mode 100644 index 00000000..977d1232 --- /dev/null +++ b/src/guestbooks/domain/useCases/CreateGuestbook.ts @@ -0,0 +1,18 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { CreateGuestbookDTO } from '../dtos/CreateGuestbookDTO' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class CreateGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Creates a guestbook for the given collection. + * + * @param {CreateGuestbookDTO} guestbook - Guestbook creation payload. + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @returns {Promise} + */ + async execute(guestbook: CreateGuestbookDTO, collectionIdOrAlias: number | string) { + return await this.guestbooksRepository.createGuestbook(collectionIdOrAlias, guestbook) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbook.ts b/src/guestbooks/domain/useCases/GetGuestbook.ts new file mode 100644 index 00000000..7ef85940 --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbook.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' +import { Guestbook } from '../models/Guestbook' + +export class GetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns a guestbook by id. + * + * @param {number} guestbookId - Guestbook identifier. + * @returns {Promise} + */ + async execute(guestbookId: number): Promise { + return await this.guestbooksRepository.getGuestbook(guestbookId) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts new file mode 100644 index 00000000..003bdb07 --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' +import { Guestbook } from '../models/Guestbook' + +export class GetGuestbooksByCollectionId implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns all guestbooks available for a given collection. + * + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @returns {Promise} + */ + async execute(collectionIdOrAlias: number | string): Promise { + return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias) + } +} diff --git a/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts b/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts new file mode 100644 index 00000000..c0b0c78f --- /dev/null +++ b/src/guestbooks/domain/useCases/RemoveDatasetGuestbook.ts @@ -0,0 +1,16 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class RemoveDatasetGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Removes guestbook assignment from a dataset. + * + * @param {number | string} datasetId - Dataset identifier (persistent id or numeric id). + * @returns {Promise} + */ + async execute(datasetId: number | string): Promise { + return await this.guestbooksRepository.removeDatasetGuestbook(datasetId) + } +} diff --git a/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts b/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts new file mode 100644 index 00000000..85e55138 --- /dev/null +++ b/src/guestbooks/domain/useCases/SetGuestbookEnabled.ts @@ -0,0 +1,26 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class SetGuestbookEnabled implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Enables or disables a guestbook in a collection. + * + * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @param {number} guestbookId - Guestbook identifier. + * @param {boolean} enabled - Desired enabled state. + * @returns {Promise} + */ + async execute( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise { + return await this.guestbooksRepository.setGuestbookEnabled( + collectionIdOrAlias, + guestbookId, + enabled + ) + } +} diff --git a/src/guestbooks/index.ts b/src/guestbooks/index.ts new file mode 100644 index 00000000..29d22988 --- /dev/null +++ b/src/guestbooks/index.ts @@ -0,0 +1,32 @@ +import { GuestbooksRepository } from './infra/repositories/GuestbooksRepository' +import { CreateGuestbook } from './domain/useCases/CreateGuestbook' +import { GetGuestbook } from './domain/useCases/GetGuestbook' +import { GetGuestbooksByCollectionId } from './domain/useCases/GetGuestbooksByCollectionId' +import { SetGuestbookEnabled } from './domain/useCases/SetGuestbookEnabled' +import { AssignDatasetGuestbook } from './domain/useCases/AssignDatasetGuestbook' +import { RemoveDatasetGuestbook } from './domain/useCases/RemoveDatasetGuestbook' + +const guestbooksRepository = new GuestbooksRepository() + +const createGuestbook = new CreateGuestbook(guestbooksRepository) +const getGuestbook = new GetGuestbook(guestbooksRepository) +const getGuestbooksByCollectionId = new GetGuestbooksByCollectionId(guestbooksRepository) +const setGuestbookEnabled = new SetGuestbookEnabled(guestbooksRepository) +const assignDatasetGuestbook = new AssignDatasetGuestbook(guestbooksRepository) +const removeDatasetGuestbook = new RemoveDatasetGuestbook(guestbooksRepository) + +export { + createGuestbook, + getGuestbook, + getGuestbooksByCollectionId, + setGuestbookEnabled, + assignDatasetGuestbook, + removeDatasetGuestbook +} + +export { + CreateGuestbookDTO, + CreateGuestbookCustomQuestionDTO, + CreateGuestbookOptionDTO +} from './domain/dtos/CreateGuestbookDTO' +export { Guestbook, GuestbookCustomQuestion, GuestbookOption } from './domain/models/Guestbook' diff --git a/src/guestbooks/infra/repositories/GuestbooksRepository.ts b/src/guestbooks/infra/repositories/GuestbooksRepository.ts new file mode 100644 index 00000000..76eb5662 --- /dev/null +++ b/src/guestbooks/infra/repositories/GuestbooksRepository.ts @@ -0,0 +1,84 @@ +import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' +import { CreateGuestbookDTO } from '../../domain/dtos/CreateGuestbookDTO' +import { Guestbook } from '../../domain/models/Guestbook' +import { IGuestbooksRepository } from '../../domain/repositories/IGuestbooksRepository' + +export class GuestbooksRepository extends ApiRepository implements IGuestbooksRepository { + private readonly guestbooksResourceName: string = 'guestbooks' + private readonly datasetsResourceName: string = 'datasets' + + public async createGuestbook( + collectionIdOrAlias: number | string, + guestbook: CreateGuestbookDTO + ): Promise { + return this.doPost( + this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}`), + guestbook + ) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async getGuestbook(guestbookId: number): Promise { + return this.doGet( + this.buildApiEndpoint(this.guestbooksResourceName, undefined, guestbookId), + true + ) + .then((response) => response.data.data as Guestbook) + .catch((error) => { + throw error + }) + } + + public async getGuestbooksByCollectionId( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet( + this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}/list`), + true + ) + .then((response) => response.data.data as Guestbook[]) + .catch((error) => { + throw error + }) + } + + public async setGuestbookEnabled( + collectionIdOrAlias: number | string, + guestbookId: number, + enabled: boolean + ): Promise { + const endpoint = this.buildApiEndpoint( + this.guestbooksResourceName, + `${collectionIdOrAlias}/${guestbookId}/enabled` + ) + return this.doPut(endpoint, enabled) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async assignDatasetGuestbook( + datasetId: number | string, + guestbookId: number + ): Promise { + const endpoint = this.buildApiEndpoint(this.datasetsResourceName, 'guestbook', datasetId) + return this.doPut(endpoint, guestbookId.toString()) + .then(() => undefined) + .catch((error) => { + throw error + }) + } + + public async removeDatasetGuestbook(datasetId: number | string): Promise { + const endpoint = this.buildApiEndpoint(this.datasetsResourceName, 'guestbook', datasetId) + return this.doDelete(endpoint) + .then(() => undefined) + .catch((error) => { + throw error + }) + } +} diff --git a/src/index.ts b/src/index.ts index 578f1924..efe54b0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,3 +13,5 @@ export * from './search' export * from './licenses' export * from './externalTools' export * from './templates' +export * from './guestbooks' +export * from './access' diff --git a/test/environment/.env b/test/environment/.env index e7b54bde..a1b87c15 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=17 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.8.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=12001-api-support-termofuse-guestbook DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/integration/access/AccessRepository.test.ts b/test/integration/access/AccessRepository.test.ts new file mode 100644 index 00000000..aa90b043 --- /dev/null +++ b/test/integration/access/AccessRepository.test.ts @@ -0,0 +1,228 @@ +import { AccessRepository } from '../../../src/access/infra/repositories/AccessRepository' +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { TestConstants } from '../../testHelpers/TestConstants' +import { GuestbookResponseDTO } from '../../../src/access/domain/dtos/GuestbookResponseDTO' +import { + CreatedDatasetIdentifiers, + createDataset, + DatasetNotNumberedVersion, + ReadError, + WriteError +} from '../../../src' +import { uploadFileViaApi, testTextFile1Name } from '../../testHelpers/files/filesHelper' +import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository' +import { FileOrderCriteria } from '../../../src/files/domain/models/FileCriteria' +import { deletePublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' + +describe('AccessRepository', () => { + const sut: AccessRepository = new AccessRepository() + const filesRepository: FilesRepository = new FilesRepository() + let testDatasetIds: CreatedDatasetIdentifiers + let testFileId: number + + const guestbookResponse: GuestbookResponseDTO = { + guestbookResponse: { + answers: [{ id: 1, value: 'question 1' }] + } + } + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + + try { + testDatasetIds = await createDataset.execute(TestConstants.TEST_NEW_DATASET_DTO) + await uploadFileViaApi(testDatasetIds.numericId, testTextFile1Name) + const filesSubset = await filesRepository.getDatasetFiles( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + FileOrderCriteria.NAME_AZ + ) + testFileId = filesSubset.files[0].id + } catch (error) { + throw new Error('Tests beforeAll(): Error while setting up access integration test data.') + } + }) + + afterAll(async () => { + try { + await deletePublishedDatasetViaApi(testDatasetIds.persistentId) + } catch (error) { + throw new Error('Tests afterAll(): Error while cleaning up access integration test data.') + } + }) + + describe('submitGuestbookForDatafileDownload', () => { + test('should return signed url for datafile download', async () => { + const actual = await sut.submitGuestbookForDatafileDownload(testFileId, guestbookResponse) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when datafile does not exist', async () => { + const nonExistentId = 999999999 + await expect( + sut.submitGuestbookForDatafileDownload(nonExistentId, guestbookResponse) + ).rejects.toThrow(WriteError) + }) + }) + + describe('getSignedDatafileDownloadUrl', () => { + test('should return signed url for datafile download', async () => { + const actual = await sut.getSignedDatafileDownloadUrl(testFileId) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when datafile does not exist', async () => { + const nonExistentId = 999999999 + await expect(sut.getSignedDatafileDownloadUrl(nonExistentId)).rejects.toThrow(ReadError) + }) + }) + + describe('getSignedDatafilesDownloadUrl', () => { + test('should return signed url for datafiles download', async () => { + const actual = await sut.getSignedDatafilesDownloadUrl([testFileId]) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when one of the datafiles does not exist', async () => { + const nonExistentId = 999999999 + await expect(sut.getSignedDatafilesDownloadUrl([testFileId, nonExistentId])).rejects.toThrow( + ReadError + ) + }) + }) + + describe('getSignedDatasetDownloadUrl', () => { + test('should return signed url for dataset download', async () => { + const actual = await sut.getSignedDatasetDownloadUrl(testDatasetIds.numericId) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return signed url for dataset download by persistent id', async () => { + const actual = await sut.getSignedDatasetDownloadUrl(testDatasetIds.persistentId) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when dataset does not exist', async () => { + const nonExistentId = 999999999 + await expect(sut.getSignedDatasetDownloadUrl(nonExistentId)).rejects.toThrow(ReadError) + }) + }) + + describe('getSignedDatasetVersionDownloadUrl', () => { + test('should return signed url for dataset version download', async () => { + const actual = await sut.getSignedDatasetVersionDownloadUrl( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST + ) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when dataset version does not exist', async () => { + const nonExistentId = 999999999 + await expect( + sut.getSignedDatasetVersionDownloadUrl(nonExistentId, DatasetNotNumberedVersion.LATEST) + ).rejects.toThrow(ReadError) + }) + }) + + describe('signed URL requests by guest users', () => { + beforeEach(() => { + ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, undefined) + }) + + afterEach(() => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return error when guest requests signed datafile url', async () => { + await expect(sut.getSignedDatafileDownloadUrl(testFileId)).rejects.toThrow(ReadError) + }) + + test('should return error when guest requests signed datafiles url', async () => { + await expect(sut.getSignedDatafilesDownloadUrl([testFileId])).rejects.toThrow(ReadError) + }) + + test('should return error when guest requests signed dataset url', async () => { + await expect(sut.getSignedDatasetDownloadUrl(testDatasetIds.numericId)).rejects.toThrow( + ReadError + ) + }) + + test('should return error when guest requests signed dataset version url', async () => { + await expect( + sut.getSignedDatasetVersionDownloadUrl( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST + ) + ).rejects.toThrow(ReadError) + }) + }) + + describe('submitGuestbookForDatafilesDownload', () => { + test('should return signed url for datafiles download', async () => { + const actual = await sut.submitGuestbookForDatafilesDownload([testFileId], guestbookResponse) + + expect(actual).toEqual(expect.any(String)) + expect(actual.length).toBeGreaterThan(0) + }) + }) + + describe('submitGuestbookForDatasetDownload', () => { + test('should return signed url for dataset download', async () => { + const actual = await sut.submitGuestbookForDatasetDownload( + testDatasetIds.numericId, + guestbookResponse + ) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when dataset does not exist', async () => { + const nonExistentId = 999999999 + await expect( + sut.submitGuestbookForDatasetDownload(nonExistentId, guestbookResponse) + ).rejects.toThrow(WriteError) + }) + }) + + describe('submitGuestbookForDatasetVersionDownload', () => { + test('should return signed url for dataset version download', async () => { + const actual = await sut.submitGuestbookForDatasetVersionDownload( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + guestbookResponse + ) + + expect(actual).toEqual(expect.any(String)) + }) + + test('should return error when dataset version does not exist', async () => { + const nonExistentId = 999999999 + await expect( + sut.submitGuestbookForDatasetVersionDownload( + nonExistentId, + DatasetNotNumberedVersion.LATEST, + guestbookResponse + ) + ).rejects.toThrow(WriteError) + }) + }) +}) diff --git a/test/integration/guestbooks/GuestbooksRepository.test.ts b/test/integration/guestbooks/GuestbooksRepository.test.ts new file mode 100644 index 00000000..9d020f05 --- /dev/null +++ b/test/integration/guestbooks/GuestbooksRepository.test.ts @@ -0,0 +1,233 @@ +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { ApiConfig, ReadError, WriteError } from '../../../src' +import { GuestbooksRepository } from '../../../src/guestbooks/infra/repositories/GuestbooksRepository' +import { CreateGuestbookDTO } from '../../../src/guestbooks/domain/dtos/CreateGuestbookDTO' +import { TestConstants } from '../../testHelpers/TestConstants' +import { + createDataset, + CreatedDatasetIdentifiers, + DatasetNotNumberedVersion, + getDataset +} from '../../../src/datasets' +import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { + createCollectionViaApi, + deleteCollectionViaApi +} from '../../testHelpers/collections/collectionHelper' +import { CollectionPayload } from '../../../src/collections/infra/repositories/transformers/CollectionPayload' + +describe('GuestbooksRepository', () => { + const sut = new GuestbooksRepository() + const testCollectionAlias = 'testGuestbooksRepository' + let testCollectionId: number + let createdGuestbookId: number + + const createGuestbookDTO: CreateGuestbookDTO = { + name: 'my test guestbook', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + email: 'test@gmail.com', + institution: 'Harvard University', + position: 'Researcher', + customQuestions: [ + { + question: "how's your day", + required: true, + displayOrder: 0, + type: 'text', + hidden: false + }, + { + question: 'Describe yourself', + required: false, + displayOrder: 1, + type: 'textarea', + hidden: false + }, + { + question: 'What color car do you drive', + required: true, + displayOrder: 2, + type: 'options', + hidden: false, + optionValues: [ + { value: 'Red', displayOrder: 0 }, + { value: 'White', displayOrder: 1 }, + { value: 'Yellow', displayOrder: 2 }, + { value: 'Purple', displayOrder: 3 } + ] + } + ] + } + + beforeAll(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + + await createCollectionViaApi(testCollectionAlias).then( + (collectionPayload: CollectionPayload) => (testCollectionId = collectionPayload.id) + ) + }) + + afterAll(async () => { + await deleteCollectionViaApi(testCollectionAlias) + }) + + describe('createGuestbook', () => { + test('should create guestbook for collection', async () => { + const actual = await sut.createGuestbook(testCollectionId, createGuestbookDTO) + expect(actual).toBeUndefined() + }) + + test('should create guestbook for collection by collection alias', async () => { + const actual = await sut.createGuestbook(testCollectionAlias, createGuestbookDTO) + expect(actual).toBeUndefined() + }) + + test('should return error when collection does not exist', async () => { + await expect(sut.createGuestbook(999999, createGuestbookDTO)).rejects.toThrow(WriteError) + }) + }) + + describe('getGuestbooksByCollectionId', () => { + test('should list guestbooks for collection', async () => { + await sut.createGuestbook(testCollectionId, createGuestbookDTO) + const actual = await sut.getGuestbooksByCollectionId(testCollectionId) + expect(actual.length).toBeGreaterThan(0) + createdGuestbookId = actual[0].id as number + }) + + test('should list guestbooks for collection by collection alias', async () => { + await sut.createGuestbook(testCollectionAlias, createGuestbookDTO) + const actual = await sut.getGuestbooksByCollectionId(testCollectionAlias) + expect(actual.length).toBeGreaterThan(0) + }) + + test('should return error when collection does not exist', async () => { + await expect(sut.getGuestbooksByCollectionId(999999)).rejects.toThrow(ReadError) + }) + }) + + describe('getGuestbook', () => { + test('should get guestbook by id', async () => { + await sut.createGuestbook(testCollectionId, createGuestbookDTO) + const actual = await sut.getGuestbook(createdGuestbookId as number) + expect(actual.id).toBe(createdGuestbookId) + expect(actual.name).toBe(createGuestbookDTO.name) + }) + + test('should return error when guestbook does not exist', async () => { + await expect(sut.getGuestbook(999999)).rejects.toThrow(ReadError) + }) + }) + + describe('setGuestbookEnabled', () => { + test('should disable guestbook', async () => { + await sut.createGuestbook(testCollectionId, createGuestbookDTO) + + await sut.setGuestbookEnabled(testCollectionId, createdGuestbookId as number, false) + const actual = await sut.getGuestbook(createdGuestbookId as number) + + expect(actual.enabled).toBe(false) + }) + + test('should enable guestbook', async () => { + await sut.setGuestbookEnabled(testCollectionId, createdGuestbookId as number, true) + const actual = await sut.getGuestbook(createdGuestbookId as number) + + expect(actual.enabled).toBe(true) + }) + + test('should return error when guestbook does not exist', async () => { + await expect(sut.setGuestbookEnabled(testCollectionId, 999999, false)).rejects.toThrow( + WriteError + ) + }) + }) + + describe('assignDatasetGuestbook / removeDatasetGuestbook', () => { + let testDatasetIds: CreatedDatasetIdentifiers + let assignableGuestbookId: number + + beforeAll(async () => { + await sut.createGuestbook(testCollectionId, { + ...createGuestbookDTO, + name: 'assign/remove guestbook test' + }) + + const guestbooks = await sut.getGuestbooksByCollectionId(testCollectionId) + const assignableGuestbook = guestbooks.find( + (guestbook) => guestbook.name === 'assign/remove guestbook test' + ) + assignableGuestbookId = assignableGuestbook?.id as number + + testDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + }) + + afterAll(async () => { + await deleteUnpublishedDatasetViaApi(testDatasetIds.numericId) + }) + + describe('assignDatasetGuestbook', () => { + test('should assign guestbook to dataset by numeric id', async () => { + const actual = await sut.assignDatasetGuestbook( + testDatasetIds.numericId, + assignableGuestbookId + ) + expect(actual).toBeUndefined() + }) + + test('should return guestbookId in dataset response after assigning guestbook', async () => { + await sut.assignDatasetGuestbook(testDatasetIds.numericId, assignableGuestbookId) + + const dataset = await getDataset.execute( + testDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST + ) + + expect(dataset.guestbookId).toBe(assignableGuestbookId) + }) + + test('should assign guestbook to dataset by persistent id', async () => { + const actual = await sut.assignDatasetGuestbook( + testDatasetIds.persistentId, + assignableGuestbookId + ) + expect(actual).toBeUndefined() + }) + + test('should return error when assigning guestbook to non-existent dataset', async () => { + await expect(sut.assignDatasetGuestbook(999999, assignableGuestbookId)).rejects.toThrow( + WriteError + ) + }) + }) + + describe('removeDatasetGuestbook', () => { + test('should remove guestbook from dataset by numeric id', async () => { + await sut.assignDatasetGuestbook(testDatasetIds.numericId, assignableGuestbookId) + const actual = await sut.removeDatasetGuestbook(testDatasetIds.numericId) + expect(actual).toBeUndefined() + }) + + test('should remove guestbook from dataset by persistent id', async () => { + await sut.assignDatasetGuestbook(testDatasetIds.numericId, assignableGuestbookId) + const actual = await sut.removeDatasetGuestbook(testDatasetIds.persistentId) + expect(actual).toBeUndefined() + }) + + test('should return error when removing guestbook from non-existent dataset', async () => { + await expect(sut.removeDatasetGuestbook(999999)).rejects.toThrow(WriteError) + }) + }) + }) +}) diff --git a/test/testHelpers/datasets/datasetHelper.ts b/test/testHelpers/datasets/datasetHelper.ts index d9f9405c..2b253a59 100644 --- a/test/testHelpers/datasets/datasetHelper.ts +++ b/test/testHelpers/datasets/datasetHelper.ts @@ -39,7 +39,8 @@ const DATAVERSE_API_REQUEST_HEADERS = { export const createDatasetModel = ( license?: DatasetLicense, - addOptionalParameters = false + addOptionalParameters = false, + guestbookId?: number ): Dataset => { const datasetModel: Dataset = { id: 1, @@ -119,12 +120,16 @@ export const createDatasetModel = ( datasetModel.publicationDate = '2021-01-01' datasetModel.citationDate = '2021-01-01' } + if (guestbookId !== undefined) { + datasetModel.guestbookId = guestbookId + } return datasetModel } export const createDatasetVersionPayload = ( license?: DatasetLicense, - addOptionalProperties = false + addOptionalProperties = false, + guestbookId?: number ): DatasetPayload => { const datasetPayload: DatasetPayload = { id: 19, @@ -256,6 +261,9 @@ export const createDatasetVersionPayload = ( datasetPayload.publicationDate = '2021-01-01' datasetPayload.citationDate = '2021-01-01' } + if (guestbookId !== undefined) { + datasetPayload.guestbookId = guestbookId + } return datasetPayload } diff --git a/test/unit/access/SubmitGuestbookDownloads.test.ts b/test/unit/access/SubmitGuestbookDownloads.test.ts new file mode 100644 index 00000000..d921c4e4 --- /dev/null +++ b/test/unit/access/SubmitGuestbookDownloads.test.ts @@ -0,0 +1,117 @@ +import { WriteError } from '../../../src' +import { GuestbookResponseDTO } from '../../../src/access/domain/dtos/GuestbookResponseDTO' +import { IAccessRepository } from '../../../src/access/domain/repositories/IAccessRepository' +import { GetSignedDatafileDownloadUrl } from '../../../src/access/domain/useCases/GetSignedDatafileDownloadUrl' +import { GetSignedDatafilesDownloadUrl } from '../../../src/access/domain/useCases/GetSignedDatafilesDownloadUrl' +import { GetSignedDatasetDownloadUrl } from '../../../src/access/domain/useCases/GetSignedDatasetDownloadUrl' +import { GetSignedDatasetVersionDownloadUrl } from '../../../src/access/domain/useCases/GetSignedDatasetVersionDownloadUrl' +import { SubmitGuestbookForDatafileDownload } from '../../../src/access/domain/useCases/SubmitGuestbookForDatafileDownload' +import { SubmitGuestbookForDatafilesDownload } from '../../../src/access/domain/useCases/SubmitGuestbookForDatafilesDownload' +import { SubmitGuestbookForDatasetDownload } from '../../../src/access/domain/useCases/SubmitGuestbookForDatasetDownload' +import { SubmitGuestbookForDatasetVersionDownload } from '../../../src/access/domain/useCases/SubmitGuestbookForDatasetVersionDownload' + +describe('access download use cases', () => { + const guestbookResponse: GuestbookResponseDTO = { + guestbookResponse: { + answers: [{ id: 1, value: 'question 1' }] + } + } + + test('should get signed datafile download url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.getSignedDatafileDownloadUrl = jest.fn().mockResolvedValue('https://signed.datafile') + const sut = new GetSignedDatafileDownloadUrl(repository) + + const actual = await sut.execute(1) + + expect(repository.getSignedDatafileDownloadUrl).toHaveBeenCalledWith(1) + expect(actual).toEqual('https://signed.datafile') + }) + + test('should get signed datafiles download url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.getSignedDatafilesDownloadUrl = jest + .fn() + .mockResolvedValue('https://signed.datafiles') + const sut = new GetSignedDatafilesDownloadUrl(repository) + + const actual = await sut.execute([1, 2]) + + expect(repository.getSignedDatafilesDownloadUrl).toHaveBeenCalledWith([1, 2]) + expect(actual).toEqual('https://signed.datafiles') + }) + + test('should get signed dataset download url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.getSignedDatasetDownloadUrl = jest.fn().mockResolvedValue('https://signed.dataset') + const sut = new GetSignedDatasetDownloadUrl(repository) + + const actual = await sut.execute('doi:10.5072/FK2/TEST') + + expect(repository.getSignedDatasetDownloadUrl).toHaveBeenCalledWith('doi:10.5072/FK2/TEST') + expect(actual).toEqual('https://signed.dataset') + }) + + test('should throw WriteError when signed dataset version download url fails', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.getSignedDatasetVersionDownloadUrl = jest.fn().mockRejectedValue(new WriteError()) + const sut = new GetSignedDatasetVersionDownloadUrl(repository) + + await expect(sut.execute(10, '2.0')).rejects.toThrow(WriteError) + }) + + test('should submit datafile download and return signed url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.submitGuestbookForDatafileDownload = jest + .fn() + .mockResolvedValue('https://signed.datafile') + const sut = new SubmitGuestbookForDatafileDownload(repository) + + const actual = await sut.execute(1, guestbookResponse) + + expect(repository.submitGuestbookForDatafileDownload).toHaveBeenCalledWith(1, guestbookResponse) + expect(actual).toEqual('https://signed.datafile') + }) + + test('should submit datafiles download and return signed url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.submitGuestbookForDatafilesDownload = jest + .fn() + .mockResolvedValue('https://signed.datafiles') + const sut = new SubmitGuestbookForDatafilesDownload(repository) + + const actual = await sut.execute([1, 2], guestbookResponse) + + expect(repository.submitGuestbookForDatafilesDownload).toHaveBeenCalledWith( + [1, 2], + guestbookResponse + ) + expect(actual).toEqual('https://signed.datafiles') + }) + + test('should submit dataset download and return signed url', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.submitGuestbookForDatasetDownload = jest + .fn() + .mockResolvedValue('https://signed.dataset') + const sut = new SubmitGuestbookForDatasetDownload(repository) + + const actual = await sut.execute('doi:10.5072/FK2/TEST', guestbookResponse) + + expect(repository.submitGuestbookForDatasetDownload).toHaveBeenCalledWith( + 'doi:10.5072/FK2/TEST', + guestbookResponse + ) + expect(actual).toEqual('https://signed.dataset') + }) + + test('should throw WriteError when dataset version download fails', async () => { + const repository: IAccessRepository = {} as IAccessRepository + repository.submitGuestbookForDatasetVersionDownload = jest + .fn() + .mockRejectedValue(new WriteError()) + const sut = new SubmitGuestbookForDatasetVersionDownload(repository) + + await expect(sut.execute(10, '2.0', guestbookResponse)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/guestbooks/AssignGuestbook.test.ts b/test/unit/guestbooks/AssignGuestbook.test.ts new file mode 100644 index 00000000..678c7644 --- /dev/null +++ b/test/unit/guestbooks/AssignGuestbook.test.ts @@ -0,0 +1,25 @@ +import { WriteError } from '../../../src' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { AssignDatasetGuestbook } from '../../../src/guestbooks/domain/useCases/AssignDatasetGuestbook' + +describe('AssignDatasetGuestbook', () => { + test('should return undefined when assign a guestbook to a dataset is successful', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.assignDatasetGuestbook = jest.fn().mockResolvedValue(undefined) + const sut = new AssignDatasetGuestbook(repository) + + const actual = await sut.execute(1, 123) + + expect(repository.assignDatasetGuestbook).toHaveBeenCalledWith(1, 123) + expect(actual).toBeUndefined() + }) + + test('should throw WriteError when repository raises an error', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.assignDatasetGuestbook = jest.fn().mockRejectedValue(new WriteError()) + const sut = new AssignDatasetGuestbook(repository) + + await expect(sut.execute(1, 123)).rejects.toThrow(WriteError) + expect(repository.assignDatasetGuestbook).toHaveBeenCalledWith(1, 123) + }) +}) diff --git a/test/unit/guestbooks/CreateGuestbook.test.ts b/test/unit/guestbooks/CreateGuestbook.test.ts new file mode 100644 index 00000000..fc507917 --- /dev/null +++ b/test/unit/guestbooks/CreateGuestbook.test.ts @@ -0,0 +1,67 @@ +import { WriteError } from '../../../src' +import { CreateGuestbookDTO } from '../../../src/guestbooks/domain/dtos/CreateGuestbookDTO' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { CreateGuestbook } from '../../../src/guestbooks/domain/useCases/CreateGuestbook' + +describe('CreateGuestbook', () => { + const createGuestbookDTO: CreateGuestbookDTO = { + name: 'my test guestbook', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + email: 'test@gmail.com', + institution: 'Harvard University', + position: 'Researcher', + customQuestions: [ + { + question: "how's your day", + required: true, + displayOrder: 0, + type: 'text', + hidden: false + }, + { + question: 'Describe yourself', + required: false, + displayOrder: 1, + type: 'textarea', + hidden: false + }, + { + question: 'What color car do you drive', + required: true, + displayOrder: 2, + type: 'options', + hidden: false, + optionValues: [ + { value: 'Red', displayOrder: 0 }, + { value: 'White', displayOrder: 1 }, + { value: 'Yellow', displayOrder: 2 }, + { value: 'Purple', displayOrder: 3 } + ] + } + ] + } + const collectionId = 'testCollection' + + test('should create guestbook for collection', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.createGuestbook = jest.fn().mockResolvedValue(undefined) + + const sut = new CreateGuestbook(repository) + const actual = await sut.execute(createGuestbookDTO, collectionId) + + expect(repository.createGuestbook).toHaveBeenCalledWith(collectionId, createGuestbookDTO) + expect(actual).toBeUndefined() + }) + + test('should throw WriteError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.createGuestbook = jest.fn().mockRejectedValue(new WriteError()) + const sut = new CreateGuestbook(repository) + + await expect(sut.execute(createGuestbookDTO, collectionId)).rejects.toThrow(WriteError) + }) +}) diff --git a/test/unit/guestbooks/GetGuestbook.test.ts b/test/unit/guestbooks/GetGuestbook.test.ts new file mode 100644 index 00000000..2a4b1cad --- /dev/null +++ b/test/unit/guestbooks/GetGuestbook.test.ts @@ -0,0 +1,41 @@ +import { ReadError } from '../../../src' +import { Guestbook } from '../../../src/guestbooks/domain/models/Guestbook' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { GetGuestbook } from '../../../src/guestbooks/domain/useCases/GetGuestbook' + +describe('execute', () => { + const guestbook: Guestbook = { + id: 12, + name: 'test', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + email: 'test@gmail.com', + institution: 'Harvard University', + position: 'Researcher', + customQuestions: [], + createTime: '2024-01-01T00:00:00Z', + dataverseId: 34 + } + + test('should return guestbook', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbook = jest.fn().mockResolvedValue(guestbook) + + const sut = new GetGuestbook(repository) + const actual = await sut.execute(12) + + expect(repository.getGuestbook).toHaveBeenCalledWith(12) + expect(actual).toEqual(guestbook) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbook = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetGuestbook(repository) + + await expect(sut.execute(111111)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts b/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts new file mode 100644 index 00000000..047ad096 --- /dev/null +++ b/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts @@ -0,0 +1,44 @@ +import { ReadError } from '../../../src' +import { Guestbook } from '../../../src/guestbooks/domain/models/Guestbook' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { GetGuestbooksByCollectionId } from '../../../src/guestbooks/domain/useCases/GetGuestbooksByCollectionId' + +describe('GetGuestbooksByCollectionId', () => { + const guestbooks: Guestbook[] = [ + { + id: 12, + name: 'test', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + email: 'test@gmail.com', + institution: 'Harvard University', + position: 'Researcher', + customQuestions: [], + createTime: '2024-01-01T00:00:00Z', + dataverseId: 10 + } + ] + const collectionId = 'collectionAlias' + + test('should return guestbooks for collection', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbooksByCollectionId = jest.fn().mockResolvedValue(guestbooks) + + const sut = new GetGuestbooksByCollectionId(repository) + const actual = await sut.execute(collectionId) + + expect(repository.getGuestbooksByCollectionId).toHaveBeenCalledWith(collectionId) + expect(actual).toEqual(guestbooks) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbooksByCollectionId = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetGuestbooksByCollectionId(repository) + + await expect(sut.execute(collectionId)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/RemoveGuestbook.test.ts b/test/unit/guestbooks/RemoveGuestbook.test.ts new file mode 100644 index 00000000..53a5e889 --- /dev/null +++ b/test/unit/guestbooks/RemoveGuestbook.test.ts @@ -0,0 +1,25 @@ +import { WriteError } from '../../../src' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { RemoveDatasetGuestbook } from '../../../src/guestbooks/domain/useCases/RemoveDatasetGuestbook' + +describe('removeDatasetGuestbook', () => { + test('should return undefined when removing guestbook is successful', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.removeDatasetGuestbook = jest.fn().mockResolvedValue(undefined) + const sut = new RemoveDatasetGuestbook(repository) + + const actual = await sut.execute(1) + + expect(repository.removeDatasetGuestbook).toHaveBeenCalledWith(1) + expect(actual).toBeUndefined() + }) + + test('should throw WriteError when repository raises an error', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.removeDatasetGuestbook = jest.fn().mockRejectedValue(new WriteError()) + const sut = new RemoveDatasetGuestbook(repository) + + await expect(sut.execute('doi:10.5072/FK2/ABCDEF')).rejects.toThrow(WriteError) + expect(repository.removeDatasetGuestbook).toHaveBeenCalledWith('doi:10.5072/FK2/ABCDEF') + }) +}) diff --git a/test/unit/guestbooks/SetGuestbookEnabled.test.ts b/test/unit/guestbooks/SetGuestbookEnabled.test.ts new file mode 100644 index 00000000..8bfae9ac --- /dev/null +++ b/test/unit/guestbooks/SetGuestbookEnabled.test.ts @@ -0,0 +1,24 @@ +import { WriteError } from '../../../src' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { SetGuestbookEnabled } from '../../../src/guestbooks/domain/useCases/SetGuestbookEnabled' + +describe('execute', () => { + test('should set enabled status', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.setGuestbookEnabled = jest.fn().mockResolvedValue(undefined) + const sut = new SetGuestbookEnabled(repository) + + const actual = await sut.execute('collectionAlias', 12, false) + + expect(repository.setGuestbookEnabled).toHaveBeenCalledWith('collectionAlias', 12, false) + expect(actual).toBeUndefined() + }) + + test('should throw WriteError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.setGuestbookEnabled = jest.fn().mockRejectedValue(new WriteError()) + const sut = new SetGuestbookEnabled(repository) + + await expect(sut.execute('collectionAlias', 999, true)).rejects.toThrow(WriteError) + }) +})