Skip to content

Commit

Permalink
feature-list-dids: stream errors as view models and add tests for the…
Browse files Browse the repository at this point in the history
… same #252
  • Loading branch information
maany committed Aug 3, 2023
1 parent 31dc0db commit ed44266
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 19 deletions.
12 changes: 6 additions & 6 deletions src/lib/sdk/gateway-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ export abstract class BaseEndpoint<TDTO extends BaseDTO> {
/**
* Reports any common errors that occurred during the Gateway request.
* Handles HTTP status codes 400, 401, and 500.
* @param statusCode The HTTP status code returned by the API.
* @param response The response object returned by the API.
* @returns A promise that resolves to the API response as a data transfer object (DTO) containing the error,
* or `undefined` if the error could not be identified clearly.
*/
async function handleCommonGatewayEndpointErrors<TDTO extends BaseDTO>(statusCode: number, response: Response): Promise<TDTO | undefined> {
const dto: TDTO = {
Expand All @@ -236,12 +240,6 @@ async function handleCommonGatewayEndpointErrors<TDTO extends BaseDTO>(statusCod
errorMessage: `An error occurred while fetching ${response.url}`,
} as TDTO;

try {
const errorDetails: string = await response.json()
dto.errorMessage = errorDetails
} catch(error : any) {
// do nothing
}
switch(statusCode) {
case 400:
dto.errorName = BaseHttpErrorTypes.NOT_FOUND.errorName;
Expand All @@ -255,6 +253,8 @@ async function handleCommonGatewayEndpointErrors<TDTO extends BaseDTO>(statusCod
dto.errorName = BaseHttpErrorTypes.UNKNOWN_ERROR.errorName;
dto.errorMessage = `An unknown server side error occurred while fetching ${response.url}`;
break;
default:
return undefined;
}
return dto;
}
23 changes: 12 additions & 11 deletions src/lib/sdk/postprocessing-pipeline-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,27 +151,28 @@ export abstract class BaseStreamingPostProcessingPipelineElement<TRequestModel,
*/
abstract transformResponseModel(responseModel: TResponseModel, dto: TDTO): TResponseModel | TErrorModel

async _transform(chunk: {status: 'success' | 'error', requestModel: TRequestModel, responseModel: TResponseModel | TErrorModel}, encoding: BufferEncoding, callback: (error?: Error | null, data?: any | TErrorModel) => void): Promise<void> {
let { requestModel, responseModel } = chunk
async _transform(chunk: {status: 'success' | 'error', requestModel: TRequestModel, responseModel: TResponseModel | TErrorModel }, encoding: BufferEncoding, callback: (error?: Error | null, data?: any | TErrorModel) => void): Promise<void> {
let { status, requestModel, responseModel } = chunk

//bypass is responseModel is already an error model
if (responseModel.status === 'error') {
if (status === 'error') {
const errorModel = responseModel as TErrorModel
callback(null, {
status: 'error',
requestModel: requestModel,
responseModel: errorModel,
})
return
}
try {
responseModel = responseModel as TResponseModel
const dto = await this.makeGatewayRequest(requestModel, responseModel)
const response: TResponseModel | TErrorModel = this.processGatewayResponse(responseModel, dto)
callback(null, {
status: response.status,
requestModel: requestModel,
responseModel: response,
})
responseModel = responseModel as TResponseModel
const dto = await this.makeGatewayRequest(requestModel, responseModel)
const response: TResponseModel | TErrorModel = this.processGatewayResponse(responseModel, dto)
callback(null, {
status: response.status,
requestModel: requestModel,
responseModel: response,
})
} catch (error: Error | any) {
const errorModel: TErrorModel = {
status: 'error',
Expand Down
2 changes: 1 addition & 1 deletion src/lib/sdk/usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ export abstract class BaseMultiCallStreamableUseCase<
callback(null, {
status: 'error',
requestModel: this.requestModel,
errorModel: errorModel,
responseModel: errorModel,
})
}
}
Expand Down
182 changes: 182 additions & 0 deletions test/api/did/list-dids-stream-error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { BaseController } from '@/lib/sdk/controller'
import { ListDIDsRequest } from '@/lib/core/usecase-models/list-dids-usecase-models'
import appContainer from '@/lib/infrastructure/ioc/container-config'
import CONTROLLERS from '@/lib/infrastructure/ioc/ioc-symbols-controllers'
import { ListDIDsControllerParameters } from '@/lib/infrastructure/controller/list-dids-controller'
import { NextApiResponse } from 'next'
import { Readable } from 'stream'
import { MockHttpStreamableResponseFactory } from 'test/fixtures/http-fixtures'
import MockRucioServerFactory, { MockEndpoint } from 'test/fixtures/rucio-server'

describe('DID API Tests', () => {
beforeEach(() => {
fetchMock.doMock()
const listDIDsEndpoint: MockEndpoint = {
url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dids/search`,
method: 'GET',
includes: 'test/dids/search',
response: {
status: 200,
headers: {
'Content-Type': 'application/x-json-stream',
},
body: Readable.from(
[
'"dataset1"',
'"dataset2"',
'"dataset3"',
" "
].join('\n'),
),
},
}

const dataset1StatusEndpoint: MockEndpoint = {
url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dataset1/status`,
method: 'GET',
response: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
scope: 'test',
name: 'dataset1',
type: 'DATASET',
account: 'root',
open: true,
monotonic: false,
expired_at: null,
length: 0,
bytes: 0,
}),
},
}

const dataset2StatusEndpoint: MockEndpoint = {
url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dataset2/status`,
method: 'GET',
response: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
scope: 'test',
name: 'dataset2',
type: 'DATASET',
account: 'root',
open: true,
monotonic: false,
expired_at: null,
bytes: 123,
length: 456,
}),
},
}

const dataset3StatusEndpoint: MockEndpoint = {
url: `${MockRucioServerFactory.RUCIO_HOST}/dids/test/dataset3/status`,
method: 'GET',
response: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
scope: 'test',
name: 'dataset3',
type: 'DATASET',
account: 'root',
open: true,
monotonic: false,
expired_at: null,
bytes: 456,
length: 789,
}),
},
}

MockRucioServerFactory.createMockRucioServer(true, [
listDIDsEndpoint,
dataset1StatusEndpoint,
dataset2StatusEndpoint,
dataset3StatusEndpoint,
])
})
afterEach(() => {
fetchMock.dontMock()
})

it('Should successfully stream DIDs', async () => {
const res = MockHttpStreamableResponseFactory.getMockResponse()
const listDIDsController = appContainer.get<BaseController<ListDIDsControllerParameters, ListDIDsRequest>>(
CONTROLLERS.LIST_DIDS,
)
const controllerParams: ListDIDsControllerParameters = {
response: res as unknown as NextApiResponse,
rucioAuthToken: MockRucioServerFactory.VALID_RUCIO_TOKEN,
query: "test:dataset1",
type: "dataset"
}
await listDIDsController.execute(
controllerParams,
)

const receivedData: any[] = []
const onData = (data: any) => {
receivedData.push(JSON.parse(data))
}

const done = new Promise<void>((resolve, reject) => {
res.on('data', onData)
res.on('end', () => {
res.off('data', onData)
resolve()
})
res.on('error', err => {
res.off('data', onData)
reject(err)
})
})

await done


expect(receivedData).toEqual([
{
"status": "success",
"name": "dataset1",
"scope": "test",
"did_type": "Dataset",
"bytes": 0,
"length": 0,
},
{
"status": "success",
"name": "dataset2",
"scope": "test",
"did_type": "Dataset",
"bytes": 123,
"length": 456,
},
{
"status": "success",
"name": "dataset3",
"scope": "test",
"did_type": "Dataset",
"bytes": 456,
"length": 789,
},
{
"status": "error",
"name": "Gateway Error: Undefined DID in stream",
"message": "Gateway recieved an invalid (undefined) DID for the query",
"scope": "",
"did_type": "Unknown",
"bytes": 0,
"length": 0,
}
])
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BaseStreamableDTO } from "@/lib/sdk/dto";
import { BaseStreamingPostProcessingPipelineElement } from "@/lib/sdk/postprocessing-pipeline-elements";
import { BaseMultiCallStreamableUseCase } from "@/lib/sdk/usecase";
import { BaseErrorResponseModel } from "@/lib/sdk/usecase-models";
import { BaseViewModel } from "@/lib/sdk/view-models";
Expand Down

0 comments on commit ed44266

Please sign in to comment.