Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -2932,17 +2932,21 @@ _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:

```typescript
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_.
Expand Down
2 changes: 2 additions & 0 deletions src/guestbooks/domain/models/Guestbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ export interface Guestbook {
customQuestions: GuestbookCustomQuestion[]
createTime: string
dataverseId: number
usageCount?: number
responseCount?: number
}
5 changes: 4 additions & 1 deletion src/guestbooks/domain/repositories/IGuestbooksRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export interface IGuestbooksRepository {
guestbook: CreateGuestbookDTO
): Promise<number>
getGuestbook(guestbookId: number): Promise<Guestbook>
getGuestbooksByCollectionId(collectionIdOrAlias: number | string): Promise<Guestbook[]>
getGuestbooksByCollectionId(
collectionIdOrAlias: number | string,
includeStats?: boolean
): Promise<Guestbook[]>
setGuestbookEnabled(
collectionIdOrAlias: number | string,
guestbookId: number,
Expand Down
12 changes: 10 additions & 2 deletions src/guestbooks/domain/useCases/GetGuestbooksByCollectionId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ export class GetGuestbooksByCollectionId implements UseCase<Guestbook[]> {
* 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<Guestbook[]>}
*/
async execute(collectionIdOrAlias: number | string): Promise<Guestbook[]> {
return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias)
async execute(collectionIdOrAlias: number | string, includeStats = false): Promise<Guestbook[]> {
if (!includeStats) {
return await this.guestbooksRepository.getGuestbooksByCollectionId(collectionIdOrAlias)
}

Comment thread
ChengShi-1 marked this conversation as resolved.
Outdated
return await this.guestbooksRepository.getGuestbooksByCollectionId(
collectionIdOrAlias,
includeStats
)
}
}
6 changes: 4 additions & 2 deletions src/guestbooks/infra/repositories/GuestbooksRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ export class GuestbooksRepository extends ApiRepository implements IGuestbooksRe
}

public async getGuestbooksByCollectionId(
collectionIdOrAlias: number | string
collectionIdOrAlias: number | string,
includeStats = false
): Promise<Guestbook[]> {
return this.doGet(
this.buildApiEndpoint(this.guestbooksResourceName, `${collectionIdOrAlias}/list`),
true
true,
includeStats ? { includeStats } : {}
)
.then((response) => response.data.data as Guestbook[])
.catch((error) => {
Expand Down
117 changes: 115 additions & 2 deletions test/integration/guestbooks/GuestbooksRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,6 +83,7 @@ describe('GuestbooksRepository', () => {
await createCollectionViaApi(testCollectionAlias).then(
(collectionPayload: CollectionPayload) => (testCollectionId = collectionPayload.id)
)
await publishCollectionViaApi(testCollectionAlias)
})

afterAll(async () => {
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion test/unit/guestbooks/GetGuestbooksByCollectionId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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())
Expand Down
84 changes: 84 additions & 0 deletions test/unit/guestbooks/GuestbooksRepository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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
}
]
}
}

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
)
})
})
})
Loading