diff --git a/src/lib/core/dto/did-dto.ts b/src/lib/core/dto/did-dto.ts index 2987666c6..2b77bd6b9 100644 --- a/src/lib/core/dto/did-dto.ts +++ b/src/lib/core/dto/did-dto.ts @@ -72,4 +72,27 @@ export interface DIDRulesDTO extends Omit, BaseDTO { */ export interface CreateDIDSampleDTO extends BaseDTO { created: boolean +} + +/** + * Data Transfer Object for AddDIDEndpoint + */ +export interface AddDIDDTO extends BaseDTO { + created: boolean +} + +/** + * Data Transfer Object for AttachDIDEndpoint + */ +export interface AttachDIDDTO extends BaseDTO { + created: boolean +} + +/** + * Data Transfer Object for SetDIDStatus (open/closed) endpoint + */ +export interface SetDIDStatusDTO extends BaseDTO { + scope: string + name: string + open: boolean } \ No newline at end of file diff --git a/src/lib/core/port/secondary/did-gateway-output-port.ts b/src/lib/core/port/secondary/did-gateway-output-port.ts index 79c811a24..c3d87bdc1 100644 --- a/src/lib/core/port/secondary/did-gateway-output-port.ts +++ b/src/lib/core/port/secondary/did-gateway-output-port.ts @@ -1,11 +1,49 @@ -import { ListDIDDTO, DIDExtendedDTO, DIDMetaDTO, ListDIDRulesDTO, DIDKeyValuePairsDTO, CreateDIDSampleDTO } from "../../dto/did-dto"; -import { DIDType } from "../../entity/rucio"; +import { ListDIDDTO, DIDExtendedDTO, DIDMetaDTO, ListDIDRulesDTO, DIDKeyValuePairsDTO, CreateDIDSampleDTO, AddDIDDTO, AttachDIDDTO as AttachDIDsDTO, SetDIDStatusDTO } from "../../dto/did-dto"; +import { DID, DIDType } from "../../entity/rucio"; /** * Output port for the DID Gateway, responsible for defining the methods that the Gateway will use to interact with the Rucio Server. */ export default interface DIDGatewayOutputPort { + /** + * Sends a request to create a sample DID to the Rucio Server. + * @param rucioAuthToken A valid Rucio auth token. + * @param inputScope The scope of the input DID. + * @param inputName The name of the input DID. + * @param outputScope The scope of the output DID. + * @param outputName The name of the output DID. + * @param nbFiles The number of files. + * @returns A Promise that resolves to a {@link CreateDIDSampleDTO} object. + */ + createDIDSample( + rucioAuthToken: string, + inputScope: string, + inputName: string, + outputScope: string, + outputName: string, + nbFiles: number + ): Promise + + + /** + * Creates a new DID on the Rucio Server. + * @param rucioAuthToken A valid Rucio auth token. + * @param scope The scope of the DID + * @param name The name of the DID + * @param didType The DIDType of the DID + */ + addDID(rucioAuthToken: string, scope: string, name: string, didType: DIDType): Promise + + /** + * Attaches a list of DIDs to a parent DID. + * @param rucioAuthToken A valid Rucio auth token. + * @param scope The scope of the parent DID + * @param name The name of the parent DID + * @param dids A list of DIDs to attach to the parent DID + */ + attachDIDs(rucioAuthToken: string, scope: string, name: string, dids: DID[]): Promise + /** * Retrieves a DID from the Rucio Server. * @param rucioAuthToken A valid Rucio auth token. @@ -79,21 +117,11 @@ export default interface DIDGatewayOutputPort { listDIDContents(rucioAuthToken: string, scope: string, name: string): Promise /** - * Sends a request to create a sample DID to the Rucio Server. + * Sets a DID status to open or closed. * @param rucioAuthToken A valid Rucio auth token. - * @param inputScope The scope of the input DID. - * @param inputName The name of the input DID. - * @param outputScope The scope of the output DID. - * @param outputName The name of the output DID. - * @param nbFiles The number of files. - * @returns A Promise that resolves to a {@link CreateDIDSampleDTO} object. + * @param scope The scope of the DID whose status is to be changed. + * @param name The name of the DID whose status is to be changed. + * @param open A boolean value indicating whether the DID should be open or closed. */ - createDIDSample( - rucioAuthToken: string, - inputScope: string, - inputName: string, - outputScope: string, - outputName: string, - nbFiles: number - ): Promise + setDIDStatus(rucioAuthToken: string, scope: string, name: string, open: boolean): Promise } \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/did-gateway/did-gateway.ts b/src/lib/infrastructure/gateway/did-gateway/did-gateway.ts index 7c7e7bf1c..0945aba63 100644 --- a/src/lib/infrastructure/gateway/did-gateway/did-gateway.ts +++ b/src/lib/infrastructure/gateway/did-gateway/did-gateway.ts @@ -1,5 +1,5 @@ -import { DIDExtendedDTO, DIDMetaDTO, ListDIDDTO, ListDIDRulesDTO, DIDKeyValuePairsDTO, CreateDIDSampleDTO } from '@/lib/core/dto/did-dto' -import { DIDAvailability, DIDType } from '@/lib/core/entity/rucio' +import { DIDExtendedDTO, DIDMetaDTO, ListDIDDTO, ListDIDRulesDTO, DIDKeyValuePairsDTO, CreateDIDSampleDTO, AddDIDDTO, AttachDIDDTO, SetDIDStatusDTO } from '@/lib/core/dto/did-dto' +import { DID, DIDAvailability, DIDType } from '@/lib/core/entity/rucio' import DIDGatewayOutputPort from '@/lib/core/port/secondary/did-gateway-output-port' import { injectable } from 'inversify' import GetDIDEndpoint from './endpoints/get-did-endpoint' @@ -10,9 +10,74 @@ import GetDIDKeyValuePairsEndpoint from './endpoints/get-did-keyvaluepairs-endpo import ListDIDParentsEndpoint from './endpoints/list-did-parents-endpoint' import ListDIDContentsEndpoint from './endpoints/list-did-contents-endpoint' import CreateDIDSampleEndpoint from './endpoints/create-did-sample-endpoint' +import AddDIDEndpoint from './endpoints/add-did-endpoint' +import AttachDIDsEndpoint from './endpoints/attach-dids-endpoint' +import SetDIDStatusEndpoint from './endpoints/set-did-status-endpoints' @injectable() export default class RucioDIDGateway implements DIDGatewayOutputPort { + async addDID(rucioAuthToken: string, scope: string, name: string, didType: DIDType): Promise { + try { + const endpoint = new AddDIDEndpoint(rucioAuthToken, scope, name, didType) + const dto = await endpoint.fetch() + return dto + } catch(error) { + const errorDTO: AddDIDDTO = { + status: 'error', + created: false, + errorName: 'An exception occurred while creating the DID.', + errorType: 'gateway_endpoint_error', + errorCode: 500, + errorMessage: error?.toString(), + } + return Promise.resolve(errorDTO) + } + } + + + async attachDIDs(rucioAuthToken: string, scope: string, name: string, dids: DID[]): Promise { + try { + const endpoint = new AttachDIDsEndpoint(rucioAuthToken, scope, name, dids) + const dto = endpoint.fetch() + return dto + }catch (error) { + const errorDTO: AttachDIDDTO = { + status: 'error', + created: false, + errorName: 'An exception occurred while attaching the DIDs.', + errorType: 'gateway_endpoint_error', + errorCode: 500, + errorMessage: error?.toString(), + } + return Promise.resolve(errorDTO) + } + } + + async createDIDSample( + rucioAuthToken: string, + inputScope: string, + inputName: string, + outputScope: string, + outputName: string, + nbFiles: number + ): Promise { + try { + const endpoint = new CreateDIDSampleEndpoint(rucioAuthToken, inputScope, inputName, outputScope, outputName, nbFiles); + const dto = await endpoint.fetch() + return dto; + } catch(error) { + const errorDTO: CreateDIDSampleDTO = { + status: 'error', + created: false, + errorName: 'An exception occurred while creating the sample DID.', + errorType: 'gateway_endpoint_error', + errorCode: 500, + errorMessage: error?.toString(), + } + return Promise.resolve(errorDTO) + } + } + async getDID( rucioAuthToken: string, @@ -174,23 +239,18 @@ export default class RucioDIDGateway implements DIDGatewayOutputPort { } } - async createDIDSample( - rucioAuthToken: string, - inputScope: string, - inputName: string, - outputScope: string, - outputName: string, - nbFiles: number - ): Promise { + async setDIDStatus(rucioAuthToken: string, scope: string, name: string, open: boolean): Promise { try { - const endpoint = new CreateDIDSampleEndpoint(rucioAuthToken, inputScope, inputName, outputScope, outputName, nbFiles); + const endpoint = new SetDIDStatusEndpoint(rucioAuthToken, scope, name, open) const dto = await endpoint.fetch() - return dto; + return dto } catch(error) { - const errorDTO: CreateDIDSampleDTO = { + const errorDTO: SetDIDStatusDTO = { status: 'error', - created: false, - errorName: 'An exception occurred while creating the sample DID.', + scope: scope, + name: name, + open: open, + errorName: 'Unknown Error', errorType: 'gateway_endpoint_error', errorCode: 500, errorMessage: error?.toString(), diff --git a/src/lib/infrastructure/gateway/did-gateway/endpoints/add-did-endpoint.ts b/src/lib/infrastructure/gateway/did-gateway/endpoints/add-did-endpoint.ts new file mode 100644 index 000000000..53fe151c5 --- /dev/null +++ b/src/lib/infrastructure/gateway/did-gateway/endpoints/add-did-endpoint.ts @@ -0,0 +1,67 @@ +import { AddDIDDTO } from "@/lib/core/dto/did-dto"; +import { DIDType } from "@/lib/core/entity/rucio"; +import { BaseEndpoint, extractErrorMessage } from "@/lib/sdk/gateway-endpoints"; +import { HTTPRequest } from "@/lib/sdk/http"; +import { Response } from "node-fetch"; + + +export default class AddDIDEndpoint extends BaseEndpoint { + constructor( + private readonly rucioAuthToken: string, + private readonly scope: string, + private readonly name: string, + private readonly didType: DIDType, + + ){ + super(true) + } + + async initialize(): Promise { + await super.initialize() + const rucioHost = await this.envConfigGateway.rucioHost() + const endpoint = `${rucioHost}/dids/${this.scope}/${this.name}` + const request: HTTPRequest = { + method: 'POST', + url: endpoint, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/json', + }, + body: { + scope: this.scope, + name: this.name, + type: this.didType.toUpperCase(), + }, + } + this.request = request + this.initialized = true + } + + async reportErrors(statusCode: number, response: Response): Promise { + const errorDTO: AddDIDDTO = { + status: 'error', + created: false, + errorMessage: 'Unknown Exception while making the request to Rucio Server or parsing the response from Rucio Server', + errorCode: statusCode, + errorName: 'Unknown Error', + errorType: 'gateway-endpoint-error', + } + if(statusCode === 409) { + errorDTO.errorMessage = `DID ${this.scope}:${this.name} Already Exists` + errorDTO.errorName = 'DID Already Attached' + return errorDTO + } + const error = await extractErrorMessage(response) + errorDTO.errorMessage = error + errorDTO.errorName = 'Rucio Server Error' + return errorDTO + } + + createDTO(data: string): AddDIDDTO { + const dto: AddDIDDTO = { + status: 'success', + created: data.toLowerCase() === "created" ? true : false, + } + return dto + } +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/did-gateway/endpoints/attach-dids-endpoint.ts b/src/lib/infrastructure/gateway/did-gateway/endpoints/attach-dids-endpoint.ts new file mode 100644 index 000000000..778685e23 --- /dev/null +++ b/src/lib/infrastructure/gateway/did-gateway/endpoints/attach-dids-endpoint.ts @@ -0,0 +1,70 @@ +import { AttachDIDDTO } from "@/lib/core/dto/did-dto"; +import { DID } from "@/lib/core/entity/rucio"; +import { BaseEndpoint, extractErrorMessage } from "@/lib/sdk/gateway-endpoints"; +import { HTTPRequest } from "@/lib/sdk/http"; +import { Response } from "node-fetch"; + + +export default class AttachDIDsEndpoint extends BaseEndpoint { + constructor( + private readonly rucioAuthToken: string, + private readonly scope: string, + private readonly name: string, + private readonly dids: DID[], + ){ + super(true) + } + + async initialize(): Promise { + await super.initialize() + const rucioHost = await this.envConfigGateway.rucioHost() + const endpoint = `${rucioHost}/dids/${this.scope}/${this.name}/dids` + const request: HTTPRequest = { + method: 'POST', + url: endpoint, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/json', + }, + body: { + scope: this.scope, + name: this.name, + dids: this.dids.map(did => { + return { + scope: did.scope, + name: did.name, + } + }) + }, + } + this.request = request + this.initialized = true + } + + async reportErrors(statusCode: number, response: Response): Promise { + const errorDTO: AttachDIDDTO = { + status: 'error', + created: false, + errorMessage: 'Unknown Exception from Rucio Server', + errorCode: statusCode, + errorName: 'Unknown Error', + errorType: 'gateway-endpoint-error', + } + if(statusCode === 409) { + errorDTO.errorMessage = `Already Attached` + errorDTO.errorName = 'DID Already Attached' + return errorDTO + } + const error = await extractErrorMessage(response) + errorDTO.errorMessage = error + return errorDTO + } + + createDTO(data: string): AttachDIDDTO { + const dto: AttachDIDDTO = { + status: 'success', + created: data.toLowerCase() === "created" ? true : false, + } + return dto + } +} \ No newline at end of file diff --git a/src/lib/infrastructure/gateway/did-gateway/endpoints/set-did-status-endpoints.ts b/src/lib/infrastructure/gateway/did-gateway/endpoints/set-did-status-endpoints.ts new file mode 100644 index 000000000..b1d91ef5d --- /dev/null +++ b/src/lib/infrastructure/gateway/did-gateway/endpoints/set-did-status-endpoints.ts @@ -0,0 +1,73 @@ +import { SetDIDStatusDTO } from "@/lib/core/dto/did-dto" +import { BaseEndpoint } from "@/lib/sdk/gateway-endpoints" +import { HTTPRequest } from "@/lib/sdk/http" +import { Response } from "node-fetch" + +export default class SetDIDStatusEndpoint extends BaseEndpoint { + constructor( + private rucioAuthToken: string, + private scope: string, + private name: string, + private open: boolean, + ) { + //parse body as text + super(true) + } + /** + * @override + */ + async initialize(): Promise { + await super.initialize() + this.url = `${this.rucioHost}/dids/${this.scope}/${this.name}/status` + const request: HTTPRequest = { + method: 'PUT', + url: this.url, + headers: { + 'X-Rucio-Auth-Token': this.rucioAuthToken, + 'Content-Type': 'application/json', + }, + body: { + 'open': open + }, + params: undefined + } + this.request = request + this.initialized = true + } + + /** + * @implements + * Status 409 means + */ + async reportErrors(statusCode: number, response: Response): Promise { + const dto: SetDIDStatusDTO = { + status: 'error', + errorCode: statusCode, + errorType: 'gateway_endpoint_error', + scope: this.scope, + name: this.name, + open: this.open, + } + if(statusCode === 409) { + dto.errorMessage = `The status of DID ${this.scope}:${this.name} cannot be changed. It is possible that the status is already set to ${this.open ? 'open' : 'closed'}` + dto.errorName = 'Cannot Change DID Status' + return dto + } + const error = await response.json() + dto.errorMessage = error.errorMessage + dto.errorName = 'Rucio Server Error' + return dto + } + + /** + * @implements + */ + createDTO(data: any): SetDIDStatusDTO { + return { + status: 'success', + scope: this.scope, + name: this.name, + open: this.open, + } + } +} \ No newline at end of file diff --git a/test/fixtures/rucio-server.ts b/test/fixtures/rucio-server.ts index 9cf6694b7..c40beb209 100644 --- a/test/fixtures/rucio-server.ts +++ b/test/fixtures/rucio-server.ts @@ -2,6 +2,7 @@ import { HTTPRequest } from '@/lib/sdk/http' import { Headers } from 'node-fetch' import { Readable } from 'stream' import { Response } from 'node-fetch' +import { BaseViewModel } from '@/lib/sdk/view-models' /** * Represents a mock HTTP request endpoint. */ @@ -20,6 +21,13 @@ export interface MockEndpoint extends HTTPRequest { * The response to send when this endpoint is matched. */ response: MockGatewayResponse + + /** + * Validate the request parameters, body, and headers. + * @param req The request to validate. + * @returns undefined if the request is valid, otherwise a BaseViewModel with the error. + */ + requestValidator?: (req: Request) => Promise } /** @@ -91,6 +99,9 @@ export default class MockRucioServerFactory { body: JSON.stringify('Not found') } as MockGatewayResponse) } + if(endpoint.requestValidator) { + endpoint.requestValidator(req) + } return Promise.resolve(endpoint.response) }) } diff --git a/test/gateway/did/did-gateway-add-attach-status.test.ts b/test/gateway/did/did-gateway-add-attach-status.test.ts new file mode 100644 index 000000000..23e0edb76 --- /dev/null +++ b/test/gateway/did/did-gateway-add-attach-status.test.ts @@ -0,0 +1,109 @@ +import { AttachDIDDTO, CreateDIDSampleDTO, SetDIDStatusDTO } from "@/lib/core/dto/did-dto" +import { DIDType } from "@/lib/core/entity/rucio" +import DIDGatewayOutputPort from "@/lib/core/port/secondary/did-gateway-output-port" +import appContainer from "@/lib/infrastructure/ioc/container-config" +import GATEWAYS from "@/lib/infrastructure/ioc/ioc-symbols-gateway" +import MockRucioServerFactory, { MockEndpoint } from "test/fixtures/rucio-server" + +describe('DID Gateway Tests: Add DID, Attach DIDs, Set DIDStatus', () => { + beforeEach(() => { + fetchMock.doMock() + const addDIDEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/conatiner10`, + endsWith: "/test/container10", + method: 'POST', + requestValidator: async(req: Request) => { + const body = await req.json() + if(body.type !== 'CONTAINER') { + throw new Error('Invalid DID Type') + } + }, + response: { + status: 201, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + body: 'Created' + } + } + const attachDIDEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dataset10/dids`, + endsWith: "/test/dataset10/dids", + method: 'POST', + response: { + status: 201, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + body: 'Created' + } + } + + const setStatusEndpoint: MockEndpoint = { + url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dataset10/status`, + endsWith: "/test/dataset10/status", + method: 'PUT', + response: { + status: 200, + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + body: null + } + } + + MockRucioServerFactory.createMockRucioServer(true, [addDIDEndpoint, attachDIDEndpoint, setStatusEndpoint]); + }) + afterEach(() => { + fetchMock.dontMock() + }) + it('should successfully hit the right Rucio Server endpoint when adding a new DID', async () => { + const rucioDIDGateway: DIDGatewayOutputPort = appContainer.get( + GATEWAYS.DID, + ) + const dto: CreateDIDSampleDTO = await rucioDIDGateway.addDID( + MockRucioServerFactory.VALID_RUCIO_TOKEN, + 'test', + 'container10', + DIDType.CONTAINER + ) + expect(dto.status).toEqual('success') + expect(dto.created).toEqual(true) + }) + it('should successfully hit the right Rucio Server endpoint when attaching DIDs to a new DID', async () => { + const rucioDIDGateway: DIDGatewayOutputPort = appContainer.get( + GATEWAYS.DID, + ) + const dto: AttachDIDDTO = await rucioDIDGateway.attachDIDs( + MockRucioServerFactory.VALID_RUCIO_TOKEN, + 'test', + 'dataset10', + [ + { + scope: 'test', + name: 'file1', + did_type: DIDType.FILE, + }, + { + scope: 'test', + name: 'file2', + did_type: DIDType.FILE, + }, + ] + ) + expect(dto.status).toEqual('success') + expect(dto.created).toEqual(true) + }) + it('should successfully hit the right Rucio Server endpoint when setting the status of a DID', async () => { + const rucioDIDGateway: DIDGatewayOutputPort = appContainer.get( + GATEWAYS.DID, + ) + const dto: SetDIDStatusDTO = await rucioDIDGateway.setDIDStatus( + MockRucioServerFactory.VALID_RUCIO_TOKEN, + 'test', + 'dataset10', + false + ) + expect(dto.status).toEqual('success') + }) +}) \ No newline at end of file diff --git a/tools/meow-maker b/tools/meow-maker index e022725f7..5076c706c 160000 --- a/tools/meow-maker +++ b/tools/meow-maker @@ -1 +1 @@ -Subproject commit e022725f77da27d4da9bd042eddf64a887057a6d +Subproject commit 5076c706cfdb4f31e2891a7d37853e27ee9b26b8