diff --git a/CHANGELOG.md b/CHANGELOG.md index d042fa65..002b5264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added +- Guestbooks: Added optional `includeStats` support to `getGuestbooksByCollectionId`, returning `usageCount` and `responseCount` when requested. + ### Changed ### Fixed diff --git a/docs/useCases.md b/docs/useCases.md index 4fb7a6f1..78899dfe 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -2932,6 +2932,7 @@ _See [use case](../src/guestbooks/domain/useCases/GetGuestbook.ts) implementatio #### Get Guestbooks By Collection Id Returns all [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) entries available for a collection. +Set `includeStats` to `true` to include `usageCount` and `responseCount` for each guestbook. ##### Example call: @@ -2939,10 +2940,13 @@ Returns all [Guestbook](../src/guestbooks/domain/models/Guestbook.ts) entries av import { getGuestbooksByCollectionId } from '@iqss/dataverse-client-javascript' const collectionIdOrAlias = 'root' +const includeStats = true -getGuestbooksByCollectionId.execute(collectionIdOrAlias).then((guestbooks: Guestbook[]) => { - /* ... */ -}) +getGuestbooksByCollectionId + .execute(collectionIdOrAlias, includeStats) + .then((guestbooks: Guestbook[]) => { + /* ... */ + }) ``` _See [use case](../src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts) implementation_. diff --git a/src/guestbooks/domain/models/Guestbook.ts b/src/guestbooks/domain/models/Guestbook.ts index 2a2f3c5b..595e801e 100644 --- a/src/guestbooks/domain/models/Guestbook.ts +++ b/src/guestbooks/domain/models/Guestbook.ts @@ -25,4 +25,6 @@ export interface Guestbook { customQuestions: GuestbookCustomQuestion[] createTime: string dataverseId: number + usageCount?: number + responseCount?: number } diff --git a/src/guestbooks/domain/models/GuestbookResponse.ts b/src/guestbooks/domain/models/GuestbookResponse.ts new file mode 100644 index 00000000..63af71e9 --- /dev/null +++ b/src/guestbooks/domain/models/GuestbookResponse.ts @@ -0,0 +1,5 @@ +export interface GuestbookResponse { + [key: string]: unknown + guestbookId?: number + dataverseId?: number +} diff --git a/src/guestbooks/domain/repositories/IGuestbooksRepository.ts b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts index 87ce91ab..0b283d95 100644 --- a/src/guestbooks/domain/repositories/IGuestbooksRepository.ts +++ b/src/guestbooks/domain/repositories/IGuestbooksRepository.ts @@ -1,5 +1,6 @@ import { CreateGuestbookDTO } from '../dtos/CreateGuestbookDTO' import { Guestbook } from '../models/Guestbook' +import { GuestbookResponse } from '../models/GuestbookResponse' export interface IGuestbooksRepository { createGuestbook( @@ -7,7 +8,18 @@ export interface IGuestbooksRepository { guestbook: CreateGuestbookDTO ): Promise getGuestbook(guestbookId: number): Promise - getGuestbooksByCollectionId(collectionIdOrAlias: number | string): Promise + getGuestbooksByCollectionId( + collectionIdOrAlias: number | string, + includeStats?: boolean + ): Promise + getGuestbookResponsesByDataverseId( + dataverseId: number | string, + guestbookId?: number + ): Promise + downloadGuestbookResponsesByDataverseId( + dataverseId: number | string, + guestbookId?: number + ): Promise setGuestbookEnabled( collectionIdOrAlias: number | string, guestbookId: number, diff --git a/src/guestbooks/domain/useCases/DownloadGuestbookResponsesByDataverseId.ts b/src/guestbooks/domain/useCases/DownloadGuestbookResponsesByDataverseId.ts new file mode 100644 index 00000000..77595019 --- /dev/null +++ b/src/guestbooks/domain/useCases/DownloadGuestbookResponsesByDataverseId.ts @@ -0,0 +1,20 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class DownloadGuestbookResponsesByDataverseId implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Downloads all guestbook responses for a dataverse collection. + * + * The dataverse can be identified by either its alias/identifier or numeric database id. + * The returned string is the raw response body from the Dataverse API, which is typically + * saved by callers as a CSV file or printed directly. + * + * @param {number | string} dataverseId - Dataverse alias/identifier or numeric database id. + * @returns {Promise} Raw response body returned by the Dataverse API. + */ + async execute(dataverseId: number | string): Promise { + return await this.guestbooksRepository.downloadGuestbookResponsesByDataverseId(dataverseId) + } +} diff --git a/src/guestbooks/domain/useCases/DownloadGuestbookResponsesOfAGuestbook.ts b/src/guestbooks/domain/useCases/DownloadGuestbookResponsesOfAGuestbook.ts new file mode 100644 index 00000000..03e62e70 --- /dev/null +++ b/src/guestbooks/domain/useCases/DownloadGuestbookResponsesOfAGuestbook.ts @@ -0,0 +1,24 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class DownloadGuestbookResponsesOfAGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Downloads guestbook responses for one guestbook in a dataverse collection. + * + * The dataverse can be identified by either its alias/identifier or numeric database id. + * The returned string is the raw response body from the Dataverse API, which is typically + * saved by callers as a CSV file or printed directly. + * + * @param {number | string} dataverseId - Dataverse alias/identifier or numeric database id. + * @param {number} guestbookId - Guestbook identifier to restrict the export. + * @returns {Promise} Raw response body returned by the Dataverse API. + */ + async execute(dataverseId: number | string, guestbookId: number): Promise { + return await this.guestbooksRepository.downloadGuestbookResponsesByDataverseId( + dataverseId, + guestbookId + ) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbookResponsesByDataverseId.ts b/src/guestbooks/domain/useCases/GetGuestbookResponsesByDataverseId.ts new file mode 100644 index 00000000..34670dee --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbookResponsesByDataverseId.ts @@ -0,0 +1,17 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponse } from '../models/GuestbookResponse' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class GetGuestbookResponsesByDataverseId implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns all guestbook responses for a dataverse collection. + * + * @param {number | string} dataverseId - Dataverse identifier. + * @returns {Promise} + */ + async execute(dataverseId: number | string): Promise { + return await this.guestbooksRepository.getGuestbookResponsesByDataverseId(dataverseId) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbookResponsesOfAGuestbook.ts b/src/guestbooks/domain/useCases/GetGuestbookResponsesOfAGuestbook.ts new file mode 100644 index 00000000..1985e4b9 --- /dev/null +++ b/src/guestbooks/domain/useCases/GetGuestbookResponsesOfAGuestbook.ts @@ -0,0 +1,21 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { GuestbookResponse } from '../models/GuestbookResponse' +import { IGuestbooksRepository } from '../repositories/IGuestbooksRepository' + +export class GetGuestbookResponsesOfAGuestbook implements UseCase { + constructor(private readonly guestbooksRepository: IGuestbooksRepository) {} + + /** + * Returns guestbook responses for one guestbook in a dataverse collection. + * + * @param {number | string} dataverseId - Dataverse identifier. + * @param {number} guestbookId - Guestbook identifier filter. + * @returns {Promise} + */ + async execute(dataverseId: number | string, guestbookId: number): Promise { + return await this.guestbooksRepository.getGuestbookResponsesByDataverseId( + dataverseId, + guestbookId + ) + } +} diff --git a/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts index 003bdb07..4ac84756 100644 --- a/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts +++ b/src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts @@ -9,9 +9,17 @@ export class GetGuestbooksByCollectionId implements UseCase { * Returns all guestbooks available for a given collection. * * @param {number | string} collectionIdOrAlias - Collection identifier (numeric id or alias). + * @param {boolean} [includeStats=false] - Include usage and response counts for each guestbook. * @returns {Promise} */ - async execute(collectionIdOrAlias: number | string): Promise { - return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias) + async execute(collectionIdOrAlias: number | string, includeStats = false): Promise { + if (!includeStats) { + return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias) + } + + return await this.guestbooksRepository.getGuestbooksByCollectionId( + collectionIdOrAlias, + includeStats + ) } } diff --git a/src/guestbooks/index.ts b/src/guestbooks/index.ts index 29d22988..3d4a1cb4 100644 --- a/src/guestbooks/index.ts +++ b/src/guestbooks/index.ts @@ -1,6 +1,10 @@ import { GuestbooksRepository } from './infra/repositories/GuestbooksRepository' import { CreateGuestbook } from './domain/useCases/CreateGuestbook' +import { DownloadGuestbookResponsesByDataverseId } from './domain/useCases/DownloadGuestbookResponsesByDataverseId' +import { DownloadGuestbookResponsesOfAGuestbook } from './domain/useCases/DownloadGuestbookResponsesOfAGuestbook' import { GetGuestbook } from './domain/useCases/GetGuestbook' +import { GetGuestbookResponsesByDataverseId } from './domain/useCases/GetGuestbookResponsesByDataverseId' +import { GetGuestbookResponsesOfAGuestbook } from './domain/useCases/GetGuestbookResponsesOfAGuestbook' import { GetGuestbooksByCollectionId } from './domain/useCases/GetGuestbooksByCollectionId' import { SetGuestbookEnabled } from './domain/useCases/SetGuestbookEnabled' import { AssignDatasetGuestbook } from './domain/useCases/AssignDatasetGuestbook' @@ -9,7 +13,19 @@ import { RemoveDatasetGuestbook } from './domain/useCases/RemoveDatasetGuestbook const guestbooksRepository = new GuestbooksRepository() const createGuestbook = new CreateGuestbook(guestbooksRepository) +const downloadGuestbookResponsesByDataverseId = new DownloadGuestbookResponsesByDataverseId( + guestbooksRepository +) +const downloadGuestbookResponsesOfAGuestbook = new DownloadGuestbookResponsesOfAGuestbook( + guestbooksRepository +) const getGuestbook = new GetGuestbook(guestbooksRepository) +const getGuestbookResponsesByDataverseId = new GetGuestbookResponsesByDataverseId( + guestbooksRepository +) +const getGuestbookResponsesOfAGuestbook = new GetGuestbookResponsesOfAGuestbook( + guestbooksRepository +) const getGuestbooksByCollectionId = new GetGuestbooksByCollectionId(guestbooksRepository) const setGuestbookEnabled = new SetGuestbookEnabled(guestbooksRepository) const assignDatasetGuestbook = new AssignDatasetGuestbook(guestbooksRepository) @@ -17,7 +33,11 @@ const removeDatasetGuestbook = new RemoveDatasetGuestbook(guestbooksRepository) export { createGuestbook, + downloadGuestbookResponsesByDataverseId, + downloadGuestbookResponsesOfAGuestbook, getGuestbook, + getGuestbookResponsesByDataverseId, + getGuestbookResponsesOfAGuestbook, getGuestbooksByCollectionId, setGuestbookEnabled, assignDatasetGuestbook, @@ -30,3 +50,4 @@ export { CreateGuestbookOptionDTO } from './domain/dtos/CreateGuestbookDTO' export { Guestbook, GuestbookCustomQuestion, GuestbookOption } from './domain/models/Guestbook' +export { GuestbookResponse } from './domain/models/GuestbookResponse' diff --git a/src/guestbooks/infra/repositories/GuestbooksRepository.ts b/src/guestbooks/infra/repositories/GuestbooksRepository.ts index 6f9812e6..c4e9c92a 100644 --- a/src/guestbooks/infra/repositories/GuestbooksRepository.ts +++ b/src/guestbooks/infra/repositories/GuestbooksRepository.ts @@ -1,11 +1,13 @@ import { ApiRepository } from '../../../core/infra/repositories/ApiRepository' import { CreateGuestbookDTO } from '../../domain/dtos/CreateGuestbookDTO' import { Guestbook } from '../../domain/models/Guestbook' +import { GuestbookResponse } from '../../domain/models/GuestbookResponse' import { IGuestbooksRepository } from '../../domain/repositories/IGuestbooksRepository' export class GuestbooksRepository extends ApiRepository implements IGuestbooksRepository { private readonly guestbooksResourceName: string = 'guestbooks' private readonly datasetsResourceName: string = 'datasets' + private readonly dataversesResourceName: string = 'dataverses' public async createGuestbook( collectionIdOrAlias: number | string, @@ -33,11 +35,13 @@ export class GuestbooksRepository extends ApiRepository implements IGuestbooksRe } public async getGuestbooksByCollectionId( - collectionIdOrAlias: number | string + collectionIdOrAlias: number | string, + includeStats = false ): Promise { return this.doGet( this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}/list`), - true + true, + includeStats ? { includeStats } : {} ) .then((response) => response.data.data as Guestbook[]) .catch((error) => { @@ -45,6 +49,34 @@ export class GuestbooksRepository extends ApiRepository implements IGuestbooksRe }) } + public async getGuestbookResponsesByDataverseId( + dataverseId: number | string, + guestbookId?: number + ): Promise { + const endpoint = `/${this.dataversesResourceName}/${dataverseId}/guestbookResponses` + const queryParams = guestbookId === undefined ? {} : { guestbookId } + + return this.doGet(endpoint, true, queryParams) + .then((response) => response.data.data as GuestbookResponse[]) + .catch((error) => { + throw error + }) + } + + public async downloadGuestbookResponsesByDataverseId( + dataverseId: number | string, + guestbookId?: number + ): Promise { + const endpoint = `/${this.dataversesResourceName}/${dataverseId}/guestbookResponses` + const queryParams = guestbookId === undefined ? {} : { guestbookId } + + return this.doGet(endpoint, true, queryParams) + .then((response) => response.data as string) + .catch((error) => { + throw error + }) + } + public async setGuestbookEnabled( collectionIdOrAlias: number | string, guestbookId: number, diff --git a/test/integration/guestbooks/GuestbooksRepository.test.ts b/test/integration/guestbooks/GuestbooksRepository.test.ts index 4674d3e0..0d070891 100644 --- a/test/integration/guestbooks/GuestbooksRepository.test.ts +++ b/test/integration/guestbooks/GuestbooksRepository.test.ts @@ -9,15 +9,28 @@ import { DatasetNotNumberedVersion, getDataset } from '../../../src/datasets' -import { deleteUnpublishedDatasetViaApi } from '../../testHelpers/datasets/datasetHelper' +import { + deletePublishedDatasetViaApi, + deleteUnpublishedDatasetViaApi, + publishDatasetViaApi, + waitForNoLocks +} from '../../testHelpers/datasets/datasetHelper' import { createCollectionViaApi, - deleteCollectionViaApi + deleteCollectionViaApi, + publishCollectionViaApi } from '../../testHelpers/collections/collectionHelper' import { CollectionPayload } from '../../../src/collections/infra/repositories/transformers/CollectionPayload' +import { AccessRepository } from '../../../src/access/infra/repositories/AccessRepository' +import { GuestbookResponseDTO } from '../../../src/access/domain/dtos/GuestbookResponseDTO' +import { testTextFile1Name, uploadFileViaApi } from '../../testHelpers/files/filesHelper' +import { FilesRepository } from '../../../src/files/infra/repositories/FilesRepository' +import { FileOrderCriteria } from '../../../src/files/domain/models/FileCriteria' describe('GuestbooksRepository', () => { const sut = new GuestbooksRepository() + const accessRepository = new AccessRepository() + const filesRepository = new FilesRepository() const testCollectionAlias = 'testGuestbooksRepository' let testCollectionId: number let createdGuestbookId: number @@ -70,6 +83,7 @@ describe('GuestbooksRepository', () => { await createCollectionViaApi(testCollectionAlias).then( (collectionPayload: CollectionPayload) => (testCollectionId = collectionPayload.id) ) + await publishCollectionViaApi(testCollectionAlias) }) afterAll(async () => { @@ -118,11 +132,110 @@ describe('GuestbooksRepository', () => { expect(actual.some((guestbook) => guestbook.id === createdByAliasGuestbookId)).toBe(true) }) + test('should list guestbooks for collection with stats', async () => { + const createdGuestbookIdWithStats = await sut.createGuestbook( + testCollectionAlias, + createGuestbookDTO + ) + const actual = await sut.getGuestbooksByCollectionId(testCollectionAlias, true) + const createdGuestbookWithStats = actual.find( + (guestbook) => guestbook.id === createdGuestbookIdWithStats + ) + + expect(createdGuestbookWithStats).toBeDefined() + expect(createdGuestbookWithStats?.usageCount).toEqual(expect.any(Number)) + expect(createdGuestbookWithStats?.responseCount).toEqual(expect.any(Number)) + }) + + test('should increment usageCount when assigned by the dataset admin and responseCount only when a guest submits a response', async () => { + let statsDatasetIds: CreatedDatasetIdentifiers | undefined + let statsDatasetPublished = false + const guestbookResponse: GuestbookResponseDTO = { + guestbookResponse: { + name: 'Guestbook Stats Test', + email: 'guestbook-stats@example.edu' + } + } + const statsGuestbookId = await sut.createGuestbook(testCollectionAlias, { + ...createGuestbookDTO, + name: 'guestbook stats test', + customQuestions: [] + }) + + try { + const initialStats = await getGuestbookStats(statsGuestbookId) + statsDatasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + await uploadFileViaApi(statsDatasetIds.numericId, testTextFile1Name) + const datasetFiles = await filesRepository.getDatasetFiles( + statsDatasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + FileOrderCriteria.NAME_AZ + ) + const fileId = datasetFiles.files[0].id + + await sut.assignDatasetGuestbook(statsDatasetIds.numericId, statsGuestbookId) + + const statsAfterAssignment = await getGuestbookStats(statsGuestbookId) + expect(statsAfterAssignment.usageCount).toBe((initialStats.usageCount ?? 0) + 1) + expect(statsAfterAssignment.responseCount).toBe(initialStats.responseCount ?? 0) + + await publishDatasetViaApi(statsDatasetIds.numericId) + statsDatasetPublished = true + await waitForNoLocks(statsDatasetIds.numericId, 10) + + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + undefined, + () => null + ) + await accessRepository.submitGuestbookForDatafileDownload(fileId, guestbookResponse) + + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + const statsAfterResponse = await getGuestbookStats(statsGuestbookId) + expect(statsAfterResponse.usageCount).toBe(statsAfterAssignment.usageCount) + expect(statsAfterResponse.responseCount).toBe((statsAfterAssignment.responseCount ?? 0) + 1) + } finally { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + if (statsDatasetIds !== undefined) { + if (statsDatasetPublished) { + await deletePublishedDatasetViaApi(statsDatasetIds.persistentId) + } else { + await deleteUnpublishedDatasetViaApi(statsDatasetIds.numericId) + } + } + } + }) + test('should return error when collection does not exist', async () => { await expect(sut.getGuestbooksByCollectionId(999999)).rejects.toThrow(ReadError) }) }) + const getGuestbookStats = async (guestbookId: number) => { + const guestbooks = await sut.getGuestbooksByCollectionId(testCollectionAlias, true) + const guestbook = guestbooks.find((guestbook) => guestbook.id === guestbookId) + + if (guestbook === undefined) { + throw new Error(`Guestbook ${guestbookId} was not found in collection stats.`) + } + + return guestbook + } + describe('getGuestbook', () => { test('should get guestbook by id', async () => { createdGuestbookId = await sut.createGuestbook(testCollectionId, createGuestbookDTO) @@ -136,6 +249,43 @@ describe('GuestbooksRepository', () => { }) }) + describe('downloadGuestbookResponsesByDataverseId', () => { + test('should download all guestbook responses for a dataverse collection', async () => { + const setup = await createGuestbookDownloadSetup('all responses export test') + + try { + const actual = await sut.downloadGuestbookResponsesByDataverseId(testCollectionAlias) + + expect(actual).toContain('Guestbook, Dataset, Dataset PID, Date, Type, File Name') + expect(actual).toContain(setup.guestbookName) + expect(actual).toContain(setup.datasetPersistentId) + expect(actual).toContain(setup.email) + expect(actual).toContain(testTextFile1Name) + } finally { + await cleanupGuestbookDownloadSetup(setup) + } + }) + + test('should download responses only for the specified guestbook', async () => { + const setup = await createGuestbookDownloadSetup('single guestbook export test') + + try { + const actual = await sut.downloadGuestbookResponsesByDataverseId( + testCollectionAlias, + setup.guestbookId + ) + + expect(actual).toContain('Guestbook, Dataset, Dataset PID, Date, Type, File Name') + expect(actual).toContain(setup.guestbookName) + expect(actual).toContain(setup.datasetPersistentId) + expect(actual).toContain(setup.email) + expect(actual).toContain(testTextFile1Name) + } finally { + await cleanupGuestbookDownloadSetup(setup) + } + }) + }) + describe('setGuestbookEnabled', () => { test('should disable guestbook', async () => { createdGuestbookId = await sut.createGuestbook(testCollectionId, createGuestbookDTO) @@ -233,4 +383,70 @@ describe('GuestbooksRepository', () => { }) }) }) + + const createGuestbookDownloadSetup = async (guestbookName: string) => { + const uniqueSuffix = Date.now().toString() + const guestbookId = await sut.createGuestbook(testCollectionAlias, { + ...createGuestbookDTO, + name: `${guestbookName}-${uniqueSuffix}`, + customQuestions: [] + }) + const datasetIds = await createDataset.execute( + TestConstants.TEST_NEW_DATASET_DTO, + testCollectionAlias + ) + await uploadFileViaApi(datasetIds.numericId, testTextFile1Name) + const datasetFiles = await filesRepository.getDatasetFiles( + datasetIds.numericId, + DatasetNotNumberedVersion.LATEST, + false, + FileOrderCriteria.NAME_AZ + ) + const fileId = datasetFiles.files[0].id + const email = `guestbook-download-${uniqueSuffix}@example.edu` + + await sut.assignDatasetGuestbook(datasetIds.numericId, guestbookId) + await publishDatasetViaApi(datasetIds.numericId) + await waitForNoLocks(datasetIds.numericId, 10) + + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.BEARER_TOKEN, + undefined, + undefined, + () => null + ) + await accessRepository.submitGuestbookForDatafileDownload(fileId, { + guestbookResponse: { + name: `Guestbook Download ${uniqueSuffix}`, + email + } + }) + + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + + return { + guestbookId, + guestbookName: `${guestbookName}-${uniqueSuffix}`, + datasetNumericId: datasetIds.numericId, + datasetPersistentId: datasetIds.persistentId, + email + } + } + + const cleanupGuestbookDownloadSetup = async (setup: { + datasetNumericId: number + datasetPersistentId: string + }) => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + await deletePublishedDatasetViaApi(setup.datasetPersistentId) + } }) diff --git a/test/testHelpers/collections/collectionHelper.ts b/test/testHelpers/collections/collectionHelper.ts index b19b668f..6d274deb 100644 --- a/test/testHelpers/collections/collectionHelper.ts +++ b/test/testHelpers/collections/collectionHelper.ts @@ -134,7 +134,7 @@ export async function setStorageDriverViaApi( ): Promise { try { return await axios.put( - `${TestConstants.TEST_API_URL}/admin/dataverse/${collectionAlias}/storageDriver`, + `${TestConstants.TEST_API_URL}/dataverses/${collectionAlias}/storageDriver`, driverLabel, { headers: { 'Content-Type': 'text/plain', 'X-Dataverse-Key': process.env.TEST_API_KEY } diff --git a/test/unit/guestbooks/DownloadGuestbookResponsesByDataverseId.test.ts b/test/unit/guestbooks/DownloadGuestbookResponsesByDataverseId.test.ts new file mode 100644 index 00000000..83ddd51e --- /dev/null +++ b/test/unit/guestbooks/DownloadGuestbookResponsesByDataverseId.test.ts @@ -0,0 +1,30 @@ +import { ReadError } from '../../../src' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { DownloadGuestbookResponsesByDataverseId } from '../../../src/guestbooks/domain/useCases/DownloadGuestbookResponsesByDataverseId' + +describe('DownloadGuestbookResponsesByDataverseId', () => { + const dataverseId = 'collectionAlias' + const csvResponse = + 'Guestbook,Dataset,Dataset PID,Date,Type,File Name,File Id,File PID,User Name,Email,Institution,Position,Custom Questions' + + test('should download guestbook responses for dataverse', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.downloadGuestbookResponsesByDataverseId = jest.fn().mockResolvedValue(csvResponse) + + const sut = new DownloadGuestbookResponsesByDataverseId(repository) + const actual = await sut.execute(dataverseId) + + expect(repository.downloadGuestbookResponsesByDataverseId).toHaveBeenCalledWith(dataverseId) + expect(actual).toEqual(csvResponse) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.downloadGuestbookResponsesByDataverseId = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new DownloadGuestbookResponsesByDataverseId(repository) + + await expect(sut.execute(dataverseId)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/DownloadGuestbookResponsesOfAGuestbook.test.ts b/test/unit/guestbooks/DownloadGuestbookResponsesOfAGuestbook.test.ts new file mode 100644 index 00000000..e1eac18e --- /dev/null +++ b/test/unit/guestbooks/DownloadGuestbookResponsesOfAGuestbook.test.ts @@ -0,0 +1,34 @@ +import { ReadError } from '../../../src' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { DownloadGuestbookResponsesOfAGuestbook } from '../../../src/guestbooks/domain/useCases/DownloadGuestbookResponsesOfAGuestbook' + +describe('DownloadGuestbookResponsesOfAGuestbook', () => { + const dataverseId = 'collectionAlias' + const guestbookId = 12 + const csvResponse = + 'Guestbook,Dataset,Dataset PID,Date,Type,File Name,File Id,File PID,User Name,Email,Institution,Position,Custom Questions' + + test('should download guestbook responses for one guestbook', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.downloadGuestbookResponsesByDataverseId = jest.fn().mockResolvedValue(csvResponse) + + const sut = new DownloadGuestbookResponsesOfAGuestbook(repository) + const actual = await sut.execute(dataverseId, guestbookId) + + expect(repository.downloadGuestbookResponsesByDataverseId).toHaveBeenCalledWith( + dataverseId, + guestbookId + ) + expect(actual).toEqual(csvResponse) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.downloadGuestbookResponsesByDataverseId = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new DownloadGuestbookResponsesOfAGuestbook(repository) + + await expect(sut.execute(dataverseId, guestbookId)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/GetGuestbookResponsesByDataverseId.test.ts b/test/unit/guestbooks/GetGuestbookResponsesByDataverseId.test.ts new file mode 100644 index 00000000..f0a9f53b --- /dev/null +++ b/test/unit/guestbooks/GetGuestbookResponsesByDataverseId.test.ts @@ -0,0 +1,35 @@ +import { ReadError } from '../../../src' +import { GuestbookResponse } from '../../../src/guestbooks/domain/models/GuestbookResponse' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { GetGuestbookResponsesByDataverseId } from '../../../src/guestbooks/domain/useCases/GetGuestbookResponsesByDataverseId' + +describe('GetGuestbookResponsesByDataverseId', () => { + const dataverseId = 'collectionAlias' + const guestbookResponses: GuestbookResponse[] = [ + { + guestbookId: 12, + dataverseId: 34, + name: 'Guest User', + email: 'guest@example.edu' + } + ] + + test('should return guestbook responses for dataverse', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbookResponsesByDataverseId = jest.fn().mockResolvedValue(guestbookResponses) + + const sut = new GetGuestbookResponsesByDataverseId(repository) + const actual = await sut.execute(dataverseId) + + expect(repository.getGuestbookResponsesByDataverseId).toHaveBeenCalledWith(dataverseId) + expect(actual).toEqual(guestbookResponses) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbookResponsesByDataverseId = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetGuestbookResponsesByDataverseId(repository) + + await expect(sut.execute(dataverseId)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/GetGuestbookResponsesOfAGuestbook.test.ts b/test/unit/guestbooks/GetGuestbookResponsesOfAGuestbook.test.ts new file mode 100644 index 00000000..e7fc558e --- /dev/null +++ b/test/unit/guestbooks/GetGuestbookResponsesOfAGuestbook.test.ts @@ -0,0 +1,39 @@ +import { ReadError } from '../../../src' +import { GuestbookResponse } from '../../../src/guestbooks/domain/models/GuestbookResponse' +import { IGuestbooksRepository } from '../../../src/guestbooks/domain/repositories/IGuestbooksRepository' +import { GetGuestbookResponsesOfAGuestbook } from '../../../src/guestbooks/domain/useCases/GetGuestbookResponsesOfAGuestbook' + +describe('GetGuestbookResponsesOfAGuestbook', () => { + const dataverseId = 'collectionAlias' + const guestbookId = 12 + const guestbookResponses: GuestbookResponse[] = [ + { + guestbookId, + dataverseId: 34, + name: 'Guest User', + email: 'guest@example.edu' + } + ] + + test('should return guestbook responses for one guestbook', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbookResponsesByDataverseId = jest.fn().mockResolvedValue(guestbookResponses) + + const sut = new GetGuestbookResponsesOfAGuestbook(repository) + const actual = await sut.execute(dataverseId, guestbookId) + + expect(repository.getGuestbookResponsesByDataverseId).toHaveBeenCalledWith( + dataverseId, + guestbookId + ) + expect(actual).toEqual(guestbookResponses) + }) + + test('should throw ReadError when repository fails', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbookResponsesByDataverseId = jest.fn().mockRejectedValue(new ReadError()) + const sut = new GetGuestbookResponsesOfAGuestbook(repository) + + await expect(sut.execute(dataverseId, guestbookId)).rejects.toThrow(ReadError) + }) +}) diff --git a/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts b/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts index 527e7b7f..abf4660d 100644 --- a/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts +++ b/test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts @@ -15,7 +15,9 @@ describe('GetGuestbooksByCollectionId', () => { positionRequired: false, customQuestions: [], createTime: '2024-01-01T00:00:00Z', - dataverseId: 10 + dataverseId: 10, + usageCount: 3, + responseCount: 2 } ] const collectionId = 'collectionAlias' @@ -31,6 +33,17 @@ describe('GetGuestbooksByCollectionId', () => { expect(actual).toEqual(guestbooks) }) + test('should request guestbooks with stats when includeStats is true', async () => { + const repository: IGuestbooksRepository = {} as IGuestbooksRepository + repository.getGuestbooksByCollectionId = jest.fn().mockResolvedValue(guestbooks) + + const sut = new GetGuestbooksByCollectionId(repository) + const actual = await sut.execute(collectionId, true) + + expect(repository.getGuestbooksByCollectionId).toHaveBeenCalledWith(collectionId, true) + expect(actual).toEqual(guestbooks) + }) + test('should throw ReadError when repository fails', async () => { const repository: IGuestbooksRepository = {} as IGuestbooksRepository repository.getGuestbooksByCollectionId = jest.fn().mockRejectedValue(new ReadError()) diff --git a/test/unit/guestbooks/GuestbooksRepository.test.ts b/test/unit/guestbooks/GuestbooksRepository.test.ts new file mode 100644 index 00000000..f55d10d2 --- /dev/null +++ b/test/unit/guestbooks/GuestbooksRepository.test.ts @@ -0,0 +1,177 @@ +import axios from 'axios' +import { + ApiConfig, + DataverseApiAuthMechanism +} from '../../../src/core/infra/repositories/ApiConfig' +import { GuestbooksRepository } from '../../../src/guestbooks/infra/repositories/GuestbooksRepository' +import { ReadError } from '../../../src/core/domain/repositories/ReadError' +import { TestConstants } from '../../testHelpers/TestConstants' + +describe('GuestbooksRepository', () => { + const sut = new GuestbooksRepository() + const collectionIdOrAlias = 'collectionAlias' + const guestbooksResponse = { + data: { + status: 'OK', + data: [ + { + id: 12, + name: 'test', + enabled: true, + emailRequired: true, + nameRequired: true, + institutionRequired: false, + positionRequired: false, + customQuestions: [], + createTime: '2024-01-01T00:00:00Z', + dataverseId: 10, + usageCount: 3, + responseCount: 2 + } + ] + } + } + const guestbookResponsesResponse = { + data: { + status: 'OK', + data: [ + { + guestbookId: 12, + dataverseId: 10, + name: 'Guest User', + email: 'guest@example.edu' + } + ] + } + } + const guestbookResponsesCsv = + 'Guestbook,Dataset,Dataset PID,Date,Type,File Name,File Id,File PID,User Name,Email,Institution,Position,Custom Questions' + + beforeEach(() => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + TestConstants.TEST_DUMMY_API_KEY + ) + + jest.clearAllMocks() + }) + + describe('getGuestbooksByCollectionId', () => { + test('should list guestbooks without stats by default', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(guestbooksResponse) + + const actual = await sut.getGuestbooksByCollectionId(collectionIdOrAlias) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/guestbooks/${collectionIdOrAlias}/list`, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toStrictEqual(guestbooksResponse.data.data) + }) + + test('should list guestbooks with stats when includeStats is true', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(guestbooksResponse) + + const actual = await sut.getGuestbooksByCollectionId(collectionIdOrAlias, true) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/guestbooks/${collectionIdOrAlias}/list`, + { + params: { + includeStats: true + }, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + ) + expect(actual[0].usageCount).toBe(3) + expect(actual[0].responseCount).toBe(2) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + await expect(sut.getGuestbooksByCollectionId(collectionIdOrAlias, true)).rejects.toThrow( + ReadError + ) + }) + }) + + describe('getGuestbookResponsesByDataverseId', () => { + test('should list guestbook responses for dataverse', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(guestbookResponsesResponse) + + const actual = await sut.getGuestbookResponsesByDataverseId(collectionIdOrAlias) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/dataverses/${collectionIdOrAlias}/guestbookResponses`, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toStrictEqual(guestbookResponsesResponse.data.data) + }) + + test('should list guestbook responses filtered by guestbook id', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(guestbookResponsesResponse) + + const actual = await sut.getGuestbookResponsesByDataverseId(collectionIdOrAlias, 12) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/dataverses/${collectionIdOrAlias}/guestbookResponses`, + { + params: { + guestbookId: 12 + }, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + ) + expect(actual).toStrictEqual(guestbookResponsesResponse.data.data) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + await expect(sut.getGuestbookResponsesByDataverseId(collectionIdOrAlias)).rejects.toThrow( + ReadError + ) + }) + }) + + describe('downloadGuestbookResponsesByDataverseId', () => { + test('should download guestbook responses for dataverse', async () => { + jest.spyOn(axios, 'get').mockResolvedValue({ data: guestbookResponsesCsv }) + + const actual = await sut.downloadGuestbookResponsesByDataverseId(collectionIdOrAlias) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/dataverses/${collectionIdOrAlias}/guestbookResponses`, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toStrictEqual(guestbookResponsesCsv) + }) + + test('should download guestbook responses filtered by guestbook id', async () => { + jest.spyOn(axios, 'get').mockResolvedValue({ data: guestbookResponsesCsv }) + + const actual = await sut.downloadGuestbookResponsesByDataverseId(collectionIdOrAlias, 12) + + expect(axios.get).toHaveBeenCalledWith( + `${TestConstants.TEST_API_URL}/dataverses/${collectionIdOrAlias}/guestbookResponses`, + { + params: { + guestbookId: 12 + }, + headers: TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY.headers + } + ) + expect(actual).toStrictEqual(guestbookResponsesCsv) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + await expect(sut.downloadGuestbookResponsesByDataverseId(collectionIdOrAlias)).rejects.toThrow( + ReadError + ) + }) + }) +})