Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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

- Collections: Exposed optional `guestbookRoot` through `getCollection`, `createCollection`, and `updateCollection` to read and persist guestbook inheritance settings (Dataverse 6.12+).

### Changed

### Fixed
Expand Down
14 changes: 13 additions & 1 deletion docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ The different use cases currently available in the package are classified below,

Returns a [Collection](../src/collections/domain/models/Collection.ts) instance, given the search parameters to identify it.

The optional `guestbookRoot` property reports guestbook inheritance: `false` includes guestbooks from parent collections; `true` stops inheritance. It is undefined when the server does not expose this setting (before Dataverse 6.12).

##### Example call:

```typescript
Expand Down Expand Up @@ -522,6 +524,8 @@ Notes:

Creates a new Collection, given a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) object and an optional parent collection identifier, which defaults to `:root`.

On Dataverse 6.12 or later, optionally set `guestbookRoot` to `false` to include parent guestbooks or `true` to stop inheritance. Omit it to use the server default.

##### Example call:

```typescript
Expand Down Expand Up @@ -602,7 +606,15 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe

#### Update a Collection

Updates an existing collection, given a collection identifier and a [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) including the updated collection data.
Updates an existing collection, given a collection identifier and a partial [CollectionDTO](../src/collections/domain/dtos/CollectionDTO.ts) including the updated collection data.

On Dataverse 6.12 or later, persist the "include guestbooks from parent collections" setting using `guestbookRoot`. The value is the inverse of the checkbox: `false` includes parent guestbooks and `true` stops inheritance. Omitting the property leaves the setting unchanged.

```typescript
await updateCollection.execute(collectionIdOrAlias, { guestbookRoot: false })
const collection = await getCollection.execute(collectionIdOrAlias)
const includeParentGuestbooks = collection.guestbookRoot === false
```

##### Example call:

Expand Down
2 changes: 2 additions & 0 deletions src/collections/domain/dtos/CollectionDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface CollectionDTO {
name: string
contacts: string[]
type: CollectionType
/** True stops guestbook inheritance; false includes guestbooks from parent collections. */
guestbookRoot?: boolean
affiliation?: string
description?: string
metadataBlockNames?: string[]
Expand Down
2 changes: 2 additions & 0 deletions src/collections/domain/models/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface Collection {
alias: string
name: string
isReleased: boolean
/** True stops guestbook inheritance; false includes guestbooks from parent collections. */
guestbookRoot?: boolean
affiliation?: string
description?: string
isPartOf: DvObjectOwnerNode
Expand Down
8 changes: 8 additions & 0 deletions src/collections/infra/repositories/CollectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface NewCollectionRequestPayload {
dataverseContacts: NewCollectionContactRequestPayload[]
dataverseType: string
description?: string
guestbookRoot?: boolean
affiliation?: string
metadataBlocks: NewCollectionMetadataBlocksRequestPayload
}
Expand Down Expand Up @@ -401,6 +402,9 @@ export class CollectionsRepository extends ApiRepository implements ICollections
dataverseType: collectionDTO.type,
...(collectionDTO.description && { description: collectionDTO.description }),
...(collectionDTO.affiliation && { affiliation: collectionDTO.affiliation }),
...(collectionDTO.guestbookRoot !== undefined && {
guestbookRoot: collectionDTO.guestbookRoot
}),
metadataBlocks: metadataBlocksRequestBody
}
}
Expand Down Expand Up @@ -478,6 +482,10 @@ export class CollectionsRepository extends ApiRepository implements ICollections
requestBody.affiliation = collectionDTO.affiliation
}

if (collectionDTO.guestbookRoot !== undefined) {
requestBody.guestbookRoot = collectionDTO.guestbookRoot
}

if (metadataBlocksRequestBody !== undefined) {
requestBody.metadataBlocks = metadataBlocksRequestBody
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface CollectionPayload {
id: number
alias: string
name: string
guestbookRoot?: boolean
affiliation?: string
isReleased: boolean
description?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const transformPayloadToCollection = (collectionPayload: CollectionPayload): Col
isFacetRoot: collectionPayload.isFacetRoot,
description: collectionPayload.description,
childCount: collectionPayload.childCount,
...(collectionPayload.guestbookRoot !== undefined && {
guestbookRoot: collectionPayload.guestbookRoot
}),
...(collectionPayload.theme && {
theme: collectionPayload.theme
}),
Expand Down
19 changes: 19 additions & 0 deletions test/functional/collections/UpdateCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createCollection,
getCollection,
updateCollection,
deleteCollection,
CollectionDTO
} from '../../../src'
import { TestConstants } from '../../testHelpers/TestConstants'
Expand All @@ -19,6 +20,24 @@ describe('execute', () => {
)
})

test('persists guestbook inheritance through public use cases', async () => {
const dto = { ...createCollectionDTO('guestbook-root-functional'), guestbookRoot: true }
const id = await createCollection.execute(dto)
try {
expect((await getCollection.execute(id)).guestbookRoot).toBe(true)
await updateCollection.execute(dto.alias, { guestbookRoot: false })
expect((await getCollection.execute(dto.alias)).guestbookRoot).toBe(false)
await updateCollection.execute(id, { name: 'Updated guestbook collection' })
expect((await getCollection.execute(id)).guestbookRoot).toBe(false)
await updateCollection.execute(id, { guestbookRoot: true })
const collection = await getCollection.execute(id)
expect(collection.guestbookRoot).toBe(true)
expect(collection.name).toBe('Updated guestbook collection')
} finally {
await deleteCollection.execute(id)
}
})

test('should successfully update a new collection', async () => {
const testNewCollectionAlias = 'updateCollection-functional-test'
const testNewCollection = createCollectionDTO(testNewCollectionAlias)
Expand Down
53 changes: 53 additions & 0 deletions test/integration/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
createBuiltInUser,
deleteAuthenticatedUserViaAdminApi
} from '../../testHelpers/users/builtinUserApiHelper'
import { CollectionsRepository } from '../../../src/collections/infra/repositories/CollectionsRepository'
import { TestConstants } from '../../testHelpers/TestConstants'
import {
Expand Down Expand Up @@ -88,6 +92,55 @@ describe('CollectionsRepository', () => {
await deleteCollectionViaApi(testCollectionAlias)
})

describe('guestbookRoot', () => {
const authenticate = (key?: string) =>
ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.API_KEY, key)

beforeEach(() => authenticate(process.env.TEST_API_KEY))
afterEach(() => authenticate(process.env.TEST_API_KEY))

test('allows anonymous reads but rejects guestbookRoot updates without permission', async () => {
const username = `guestbookReader${Date.now()}`
const otherUserKey = await createBuiltInUser(username)
const id = await sut.createCollection({
...createCollectionDTO('guestbook-root-permissions'),
guestbookRoot: false
})
try {
await sut.publishCollection(id)
authenticate()
expect((await sut.getCollection(id)).guestbookRoot).toBe(false)
authenticate(otherUserKey)
await expect(sut.updateCollection(id, { guestbookRoot: true })).rejects.toMatchObject({
message: expect.stringContaining(`[401] User @${username} is not permitted`)
})
authenticate(process.env.TEST_API_KEY)
expect((await sut.getCollection(id)).guestbookRoot).toBe(false)
} finally {
authenticate(process.env.TEST_API_KEY)
await sut.deleteCollection(id)
await deleteAuthenticatedUserViaAdminApi(username)
}
})

test.each([true, false])(
'persists guestbookRoot=%s on creation and update',
async (guestbookRoot) => {
const dto = { ...createCollectionDTO(`guestbook-root-${guestbookRoot}`), guestbookRoot }
const id = await sut.createCollection(dto)
try {
expect((await sut.getCollection(id)).guestbookRoot).toBe(guestbookRoot)
await sut.updateCollection(dto.alias, { guestbookRoot: !guestbookRoot })
expect((await sut.getCollection(dto.alias)).guestbookRoot).toBe(!guestbookRoot)
await sut.updateCollection(id, { name: 'Renamed collection' })
expect((await sut.getCollection(id)).guestbookRoot).toBe(!guestbookRoot)
} finally {
await sut.deleteCollection(id)
}
}
)
})

describe('getCollection', () => {
describe('by default `root` Id', () => {
test('should return the root collection of the Dataverse installation if no parameter is passed AS `root`', async () => {
Expand Down
4 changes: 4 additions & 0 deletions test/testHelpers/users/builtinUserApiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ export const createBuiltInUser = async (userName: string): Promise<string> => {
throw new Error(`Error while creating API token`)
}
}

export const deleteAuthenticatedUserViaAdminApi = async (userIdentifier: string): Promise<void> => {
await axios.delete(`${TestConstants.TEST_API_URL}/admin/authenticatedUsers/${userIdentifier}`)
}
46 changes: 46 additions & 0 deletions test/unit/collections/CollectionsRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,52 @@ describe('CollectionsRepository', () => {
jest.clearAllMocks()
})

describe('guestbookRoot', () => {
test.each([true, false, undefined])('reads guestbookRoot=%s', async (guestbookRoot) => {
jest.spyOn(axios, 'get').mockResolvedValue({
data: { data: { ...createCollectionPayload(), guestbookRoot } }
})

const collection = await sut.getCollection('test-collection')

expect(collection.guestbookRoot).toBe(guestbookRoot)
expect(Object.prototype.hasOwnProperty.call(collection, 'guestbookRoot')).toBe(
guestbookRoot !== undefined
)
})

test.each([true, false, undefined])('creates with guestbookRoot=%s', async (guestbookRoot) => {
jest.spyOn(axios, 'post').mockResolvedValue({ data: { data: { id: 1 } } })

await sut.createCollection({ ...createCollectionDTO(), guestbookRoot })

const requestBody = JSON.parse(jest.mocked(axios.post).mock.calls[0][1] as string)
expect(requestBody).toEqual({ ...createNewCollectionRequestPayload(), guestbookRoot })
expect(Object.prototype.hasOwnProperty.call(requestBody, 'guestbookRoot')).toBe(
guestbookRoot !== undefined
)

expect(axios.post).toHaveBeenCalledWith(
`${TestConstants.TEST_API_URL}/dataverses/:root`,
expect.any(String),
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
)
})

test.each([123, 'test-collection'])('updates only guestbookRoot by %s', async (identifier) => {
jest.spyOn(axios, 'put').mockResolvedValue({ data: { data: {} } })
for (const guestbookRoot of [true, false]) {
await sut.updateCollection(identifier, { guestbookRoot })

expect(axios.put).toHaveBeenLastCalledWith(
`${TestConstants.TEST_API_URL}/dataverses/${identifier}`,
JSON.stringify({ guestbookRoot }),
TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY
)
}
})
})

describe('getCollection', () => {
const expectedRequestConfigApiKey = {
params: {
Expand Down
Loading