From d6aeeced0b11452efac18e7d05619d3bbfd5841f Mon Sep 17 00:00:00 2001 From: GPortas Date: Wed, 7 Aug 2024 13:45:56 +0200 Subject: [PATCH 1/2] Added: GetCollectionUserPermissions use case --- docs/useCases.md | 29 ++++++++++ .../models/CollectionUserPermissions.ts | 9 +++ .../repositories/ICollectionsRepository.ts | 4 ++ .../useCases/GetCollectionUserPermissions.ts | 22 ++++++++ src/collections/index.ts | 5 +- .../repositories/CollectionsRepository.ts | 17 ++++++ .../CollectionUserPermissionsPayload.ts | 9 +++ .../collectionUserPermissionsTransformers.ts | 17 ++++++ test/environment/.env | 4 +- .../GetCollectionUserPermissions.test.ts | 52 ++++++++++++++++++ .../collections/CollectionsRepository.test.ts | 25 +++++++++ .../collectionUserPermissionsHelper.ts | 26 +++++++++ .../collections/CollectionsRepository.test.ts | 55 +++++++++++++++++++ .../GetCollectionUserPermissions.test.ts | 34 ++++++++++++ 14 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 src/collections/domain/models/CollectionUserPermissions.ts create mode 100644 src/collections/domain/useCases/GetCollectionUserPermissions.ts create mode 100644 src/collections/infra/repositories/transformers/CollectionUserPermissionsPayload.ts create mode 100644 src/collections/infra/repositories/transformers/collectionUserPermissionsTransformers.ts create mode 100644 test/functional/collections/GetCollectionUserPermissions.test.ts create mode 100644 test/testHelpers/collections/collectionUserPermissionsHelper.ts create mode 100644 test/unit/collections/GetCollectionUserPermissions.test.ts diff --git a/docs/useCases.md b/docs/useCases.md index 0d3df7df..56fc9777 100644 --- a/docs/useCases.md +++ b/docs/useCases.md @@ -12,6 +12,7 @@ The different use cases currently available in the package are classified below, - [Collections read use cases](#collections-read-use-cases) - [Get a Collection](#get-a-collection) - [Get Collection Facets](#get-collection-facets) + - [Get User Permissions on a Collection](#get-user-permissions-on-a-collection) - [Collections write use cases](#collections-write-use-cases) - [Create a Collection](#create-a-collection) - [Datasets](#Datasets) @@ -128,6 +129,34 @@ The `collectionIdOrAlias` is a generic collection identifier, which can be eithe If no collection identifier is specified, the default collection identifier; `root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. +#### Get User Permissions on a Collection + +Returns an instance of [CollectionUserPermissions](../src/collections/domain/models/CollectionUserPermissions.ts) that includes the permissions that the calling user has on a particular Collection. + +##### Example call: + +```typescript +import { getCollectionUserPermissions } from '@iqss/dataverse-client-javascript' + +/* ... */ + +const collectionIdOrAlias = 12345 + +getCollectionUserPermissions + .execute(collectionIdOrAlias) + .then((permissions: CollectionUserPermissions) => { + /* ... */ + }) + +/* ... */ +``` + +_See [use case](../src/collections/domain/useCases/GetCollectionUserPermissions.ts) implementation_. + +The `collectionIdOrAlias` is a generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId). + +If no collection identifier is specified, the default collection identifier; `root` will be used. If you want to search for a different collection, you must add the collection identifier as a parameter in the use case call. + ### Collections Write Use Cases #### Create a Collection diff --git a/src/collections/domain/models/CollectionUserPermissions.ts b/src/collections/domain/models/CollectionUserPermissions.ts new file mode 100644 index 00000000..1a29933f --- /dev/null +++ b/src/collections/domain/models/CollectionUserPermissions.ts @@ -0,0 +1,9 @@ +export interface CollectionUserPermissions { + canAddCollection: boolean + canAddDataset: boolean + canViewUnpublishedCollection: boolean + canEditCollection: boolean + canManageCollectionPermissions: boolean + canPublishCollection: boolean + canDeleteCollection: boolean +} diff --git a/src/collections/domain/repositories/ICollectionsRepository.ts b/src/collections/domain/repositories/ICollectionsRepository.ts index c449388d..776d94b4 100644 --- a/src/collections/domain/repositories/ICollectionsRepository.ts +++ b/src/collections/domain/repositories/ICollectionsRepository.ts @@ -1,5 +1,6 @@ import { CollectionDTO } from '../dtos/CollectionDTO' import { Collection } from '../models/Collection' +import { CollectionUserPermissions } from '../models/CollectionUserPermissions' export interface ICollectionsRepository { getCollection(collectionIdOrAlias: number | string): Promise @@ -8,4 +9,7 @@ export interface ICollectionsRepository { parentCollectionId: number | string ): Promise getCollectionFacets(collectionIdOrAlias: number | string): Promise + getCollectionUserPermissions( + collectionIdOrAlias: number | string + ): Promise } diff --git a/src/collections/domain/useCases/GetCollectionUserPermissions.ts b/src/collections/domain/useCases/GetCollectionUserPermissions.ts new file mode 100644 index 00000000..bf8cc7bd --- /dev/null +++ b/src/collections/domain/useCases/GetCollectionUserPermissions.ts @@ -0,0 +1,22 @@ +import { UseCase } from '../../../core/domain/useCases/UseCase' +import { CollectionUserPermissions } from '../models/CollectionUserPermissions' +import { ICollectionsRepository } from '../repositories/ICollectionsRepository' + +export class GetCollectionUserPermissions implements UseCase { + private collectionsRepository: ICollectionsRepository + + constructor(collectionsRepository: ICollectionsRepository) { + this.collectionsRepository = collectionsRepository + } + + /** + * Returns an instance of CollectionUserPermissions that includes the permissions that the calling user has on a particular Collection. + * + * @param {number | string} [collectionIdOrAlias = 'root'] - A generic collection identifier, which can be either a string (for queries by CollectionAlias), or a number (for queries by CollectionId) + * If this parameter is not set, the default value is: 'root' + * @returns {Promise} + */ + async execute(collectionIdOrAlias: number | string): Promise { + return await this.collectionsRepository.getCollectionUserPermissions(collectionIdOrAlias) + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 53ee454b..1608b03f 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -1,6 +1,7 @@ import { CreateCollection } from './domain/useCases/CreateCollection' import { GetCollection } from './domain/useCases/GetCollection' import { GetCollectionFacets } from './domain/useCases/GetCollectionFacets' +import { GetCollectionUserPermissions } from './domain/useCases/GetCollectionUserPermissions' import { CollectionsRepository } from './infra/repositories/CollectionsRepository' @@ -9,7 +10,9 @@ const collectionsRepository = new CollectionsRepository() const getCollection = new GetCollection(collectionsRepository) const createCollection = new CreateCollection(collectionsRepository) const getCollectionFacets = new GetCollectionFacets(collectionsRepository) +const getCollectionUserPermissions = new GetCollectionUserPermissions(collectionsRepository) -export { getCollection, createCollection, getCollectionFacets } +export { getCollection, createCollection, getCollectionFacets, getCollectionUserPermissions } export { Collection, CollectionInputLevel } from './domain/models/Collection' +export { CollectionUserPermissions } from './domain/models/CollectionUserPermissions' export { CollectionDTO, CollectionInputLevelDTO } from './domain/dtos/CollectionDTO' diff --git a/src/collections/infra/repositories/CollectionsRepository.ts b/src/collections/infra/repositories/CollectionsRepository.ts index e9e3bfea..e7960161 100644 --- a/src/collections/infra/repositories/CollectionsRepository.ts +++ b/src/collections/infra/repositories/CollectionsRepository.ts @@ -3,6 +3,8 @@ import { ICollectionsRepository } from '../../domain/repositories/ICollectionsRe import { transformCollectionResponseToCollection } from './transformers/collectionTransformers' import { Collection, ROOT_COLLECTION_ALIAS } from '../../domain/models/Collection' import { CollectionDTO } from '../../domain/dtos/CollectionDTO' +import { CollectionUserPermissions } from '../../domain/models/CollectionUserPermissions' +import { transformCollectionUserPermissionsResponseToCollectionUserPermissions } from './transformers/collectionUserPermissionsTransformers' export interface NewCollectionRequestPayload { alias: string @@ -86,4 +88,19 @@ export class CollectionsRepository extends ApiRepository implements ICollections throw error }) } + + public async getCollectionUserPermissions( + collectionIdOrAlias: number | string + ): Promise { + return this.doGet( + `/${this.collectionsResourceName}/${collectionIdOrAlias}/userPermissions`, + true + ) + .then((response) => + transformCollectionUserPermissionsResponseToCollectionUserPermissions(response) + ) + .catch((error) => { + throw error + }) + } } diff --git a/src/collections/infra/repositories/transformers/CollectionUserPermissionsPayload.ts b/src/collections/infra/repositories/transformers/CollectionUserPermissionsPayload.ts new file mode 100644 index 00000000..e174794a --- /dev/null +++ b/src/collections/infra/repositories/transformers/CollectionUserPermissionsPayload.ts @@ -0,0 +1,9 @@ +export interface CollectionUserPermissionsPayload { + canAddDataverse: boolean + canAddDataset: boolean + canViewUnpublishedDataverse: boolean + canEditDataverse: boolean + canManageDataversePermissions: boolean + canPublishDataverse: boolean + canDeleteDataverse: boolean +} diff --git a/src/collections/infra/repositories/transformers/collectionUserPermissionsTransformers.ts b/src/collections/infra/repositories/transformers/collectionUserPermissionsTransformers.ts new file mode 100644 index 00000000..3f1bf296 --- /dev/null +++ b/src/collections/infra/repositories/transformers/collectionUserPermissionsTransformers.ts @@ -0,0 +1,17 @@ +import { AxiosResponse } from 'axios' +import { CollectionUserPermissions } from '../../../domain/models/CollectionUserPermissions' + +export const transformCollectionUserPermissionsResponseToCollectionUserPermissions = ( + response: AxiosResponse +): CollectionUserPermissions => { + const collectionUserPermissionsPayload = response.data.data + return { + canAddCollection: collectionUserPermissionsPayload.canAddDataverse, + canAddDataset: collectionUserPermissionsPayload.canAddDataset, + canViewUnpublishedCollection: collectionUserPermissionsPayload.canViewUnpublishedDataverse, + canEditCollection: collectionUserPermissionsPayload.canEditDataverse, + canManageCollectionPermissions: collectionUserPermissionsPayload.canManageDataversePermissions, + canPublishCollection: collectionUserPermissionsPayload.canPublishDataverse, + canDeleteCollection: collectionUserPermissionsPayload.canDeleteDataverse + } +} diff --git a/test/environment/.env b/test/environment/.env index 80e9a14e..106b1090 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 -DATAVERSE_IMAGE_REGISTRY=docker.io -DATAVERSE_IMAGE_TAG=unstable +DATAVERSE_IMAGE_REGISTRY=ghcr.io +DATAVERSE_IMAGE_TAG=10749-dataverse-user-permissions-api DATAVERSE_BOOTSTRAP_TIMEOUT=5m diff --git a/test/functional/collections/GetCollectionUserPermissions.test.ts b/test/functional/collections/GetCollectionUserPermissions.test.ts new file mode 100644 index 00000000..fd813979 --- /dev/null +++ b/test/functional/collections/GetCollectionUserPermissions.test.ts @@ -0,0 +1,52 @@ +import { + ApiConfig, + CollectionUserPermissions, + ReadError, + getCollectionUserPermissions +} from '../../../src' +import { TestConstants } from '../../testHelpers/TestConstants' +import { DataverseApiAuthMechanism } from '../../../src/core/infra/repositories/ApiConfig' +import { ROOT_COLLECTION_ALIAS } from '../../../src/collections/domain/models/Collection' + +describe('execute', () => { + beforeEach(async () => { + ApiConfig.init( + TestConstants.TEST_API_URL, + DataverseApiAuthMechanism.API_KEY, + process.env.TEST_API_KEY + ) + }) + + test('should return user permissions when a valid collection alias is provided', async () => { + let actual: CollectionUserPermissions + try { + actual = await getCollectionUserPermissions.execute(ROOT_COLLECTION_ALIAS) + } catch (error) { + throw new Error('Permissions should be retrieved') + } finally { + expect(actual.canAddDataset).toBe(true) + expect(actual.canAddCollection).toBe(true) + expect(actual.canDeleteCollection).toBe(true) + expect(actual.canEditCollection).toBe(true) + expect(actual.canManageCollectionPermissions).toBe(true) + expect(actual.canPublishCollection).toBe(true) + expect(actual.canViewUnpublishedCollection).toBe(true) + } + }) + + test('should throw an error when collection does not exist', async () => { + expect.assertions(2) + let readError: ReadError + try { + await getCollectionUserPermissions.execute(TestConstants.TEST_DUMMY_COLLECTION_ID) + throw new Error('Use case should throw an error') + } catch (error) { + readError = error + } finally { + expect(readError).toBeInstanceOf(ReadError) + expect(readError.message).toEqual( + `There was an error when reading the resource. Reason was: [404] Can't find dataverse with identifier='${TestConstants.TEST_DUMMY_COLLECTION_ID}'` + ) + } + }) +}) diff --git a/test/integration/collections/CollectionsRepository.test.ts b/test/integration/collections/CollectionsRepository.test.ts index 80dd77d3..e13c4838 100644 --- a/test/integration/collections/CollectionsRepository.test.ts +++ b/test/integration/collections/CollectionsRepository.test.ts @@ -165,4 +165,29 @@ describe('CollectionsRepository', () => { ).rejects.toThrow(expectedError) }) }) + + describe('getCollectionUserPermissions', () => { + test('should return user permissions', async () => { + const actual = await sut.getCollectionUserPermissions('root') + expect(actual.canAddDataset).toBe(true) + expect(actual.canAddCollection).toBe(true) + expect(actual.canDeleteCollection).toBe(true) + expect(actual.canEditCollection).toBe(true) + expect(actual.canManageCollectionPermissions).toBe(true) + expect(actual.canPublishCollection).toBe(true) + expect(actual.canViewUnpublishedCollection).toBe(true) + }) + + test('should return error when collection does not exist', async () => { + const nonExistentCollectionAlias = 'nonExistentCollection' + + const expectedError = new ReadError( + `[404] Can't find dataverse with identifier='${nonExistentCollectionAlias}'` + ) + + await expect(sut.getCollectionUserPermissions(nonExistentCollectionAlias)).rejects.toThrow( + expectedError + ) + }) + }) }) diff --git a/test/testHelpers/collections/collectionUserPermissionsHelper.ts b/test/testHelpers/collections/collectionUserPermissionsHelper.ts new file mode 100644 index 00000000..eeabe8c4 --- /dev/null +++ b/test/testHelpers/collections/collectionUserPermissionsHelper.ts @@ -0,0 +1,26 @@ +import { CollectionUserPermissions } from '../../../src/collections/domain/models/CollectionUserPermissions' +import { CollectionUserPermissionsPayload } from '../../../src/collections/infra/repositories/transformers/CollectionUserPermissionsPayload' + +export const createCollectionUserPermissionsModel = (): CollectionUserPermissions => { + return { + canAddCollection: true, + canAddDataset: true, + canViewUnpublishedCollection: true, + canEditCollection: true, + canManageCollectionPermissions: true, + canPublishCollection: true, + canDeleteCollection: true + } +} + +export const createCollectionUserPermissionsPayload = (): CollectionUserPermissionsPayload => { + return { + canAddDataverse: true, + canAddDataset: true, + canViewUnpublishedDataverse: true, + canEditDataverse: true, + canManageDataversePermissions: true, + canPublishDataverse: true, + canDeleteDataverse: true + } +} diff --git a/test/unit/collections/CollectionsRepository.test.ts b/test/unit/collections/CollectionsRepository.test.ts index 46b27ec4..ecae691f 100644 --- a/test/unit/collections/CollectionsRepository.test.ts +++ b/test/unit/collections/CollectionsRepository.test.ts @@ -13,6 +13,10 @@ import { import { TestConstants } from '../../testHelpers/TestConstants' import { ReadError, WriteError } from '../../../src' import { ROOT_COLLECTION_ALIAS } from '../../../src/collections/domain/models/Collection' +import { + createCollectionUserPermissionsModel, + createCollectionUserPermissionsPayload +} from '../../testHelpers/collections/collectionUserPermissionsHelper' describe('CollectionsRepository', () => { const sut: CollectionsRepository = new CollectionsRepository() @@ -228,4 +232,55 @@ describe('CollectionsRepository', () => { }) }) }) + + describe('getCollectionUserPermissions', () => { + const testCollectionUserPermissions = createCollectionUserPermissionsModel() + const testCollectionUserPermissionsResponse = { + data: { + status: 'OK', + data: createCollectionUserPermissionsPayload() + } + } + + describe('by numeric id', () => { + const expectedApiEndpoint = `${TestConstants.TEST_API_URL}/dataverses/${testCollectionModel.id}/userPermissions` + + test('should return dataset user permissions when providing id and response is successful', async () => { + jest.spyOn(axios, 'get').mockResolvedValue(testCollectionUserPermissionsResponse) + + // API Key auth + let actual = await sut.getCollectionUserPermissions(testCollectionModel.id) + + expect(axios.get).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(actual).toStrictEqual(testCollectionUserPermissions) + + // Session cookie auth + ApiConfig.init(TestConstants.TEST_API_URL, DataverseApiAuthMechanism.SESSION_COOKIE) + + actual = await sut.getCollectionUserPermissions(testCollectionModel.id) + + expect(axios.get).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_SESSION_COOKIE + ) + expect(actual).toStrictEqual(testCollectionUserPermissions) + }) + + test('should return error result on error response', async () => { + jest.spyOn(axios, 'get').mockRejectedValue(TestConstants.TEST_ERROR_RESPONSE) + + let error = undefined as unknown as ReadError + await sut.getCollectionUserPermissions(testCollectionModel.id).catch((e) => (error = e)) + + expect(axios.get).toHaveBeenCalledWith( + expectedApiEndpoint, + TestConstants.TEST_EXPECTED_AUTHENTICATED_REQUEST_CONFIG_API_KEY + ) + expect(error).toBeInstanceOf(Error) + }) + }) + }) }) diff --git a/test/unit/collections/GetCollectionUserPermissions.test.ts b/test/unit/collections/GetCollectionUserPermissions.test.ts new file mode 100644 index 00000000..fe63d638 --- /dev/null +++ b/test/unit/collections/GetCollectionUserPermissions.test.ts @@ -0,0 +1,34 @@ +import { GetCollectionUserPermissions } from '../../../src/collections/domain/useCases/GetCollectionUserPermissions' +import { ReadError } from '../../../src/core/domain/repositories/ReadError' +import { ICollectionsRepository } from '../../../src/collections/domain/repositories/ICollectionsRepository' +import { createCollectionUserPermissionsModel } from '../../testHelpers/collections/collectionUserPermissionsHelper' + +describe('execute', () => { + const testCollectionAlias = 'test' + + test('should return collection user permissions on repository success', async () => { + const testCollectionUserPermissions = createCollectionUserPermissionsModel() + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getCollectionUserPermissions = jest + .fn() + .mockResolvedValue(testCollectionUserPermissions) + const sut = new GetCollectionUserPermissions(collectionsRepositoryStub) + + const actual = await sut.execute(testCollectionAlias) + + expect(actual).toEqual(testCollectionUserPermissions) + expect(collectionsRepositoryStub.getCollectionUserPermissions).toHaveBeenCalledWith( + testCollectionAlias + ) + }) + + test('should return error result on repository error', async () => { + const collectionsRepositoryStub: ICollectionsRepository = {} as ICollectionsRepository + collectionsRepositoryStub.getCollectionUserPermissions = jest + .fn() + .mockRejectedValue(new ReadError()) + const sut = new GetCollectionUserPermissions(collectionsRepositoryStub) + + await expect(sut.execute(testCollectionAlias)).rejects.toThrow(ReadError) + }) +}) From 3f8ee0a7fd9f9e6e2e61ed8156232b262098a1f6 Mon Sep 17 00:00:00 2001 From: Ellen Kraffmiller Date: Thu, 15 Aug 2024 13:41:52 -0400 Subject: [PATCH 2/2] change dataverse image to 'unstable' --- test/environment/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/environment/.env b/test/environment/.env index 106b1090..80e9a14e 100644 --- a/test/environment/.env +++ b/test/environment/.env @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse SOLR_VERSION=9.3.0 -DATAVERSE_IMAGE_REGISTRY=ghcr.io -DATAVERSE_IMAGE_TAG=10749-dataverse-user-permissions-api +DATAVERSE_IMAGE_REGISTRY=docker.io +DATAVERSE_IMAGE_TAG=unstable DATAVERSE_BOOTSTRAP_TIMEOUT=5m