Skip to content

Commit

Permalink
VR-228: query response bugfix (#181)
Browse files Browse the repository at this point in the history
* VR-228: curly bracket removal and controller update (types).

* VR-228: rebase with main.

* VR-228: a few refinements in regards to partials along with total emissions.

* VR-228: second company mock.'

* VR-228: switch back company number to DC.

* VR-228: do not deconstruct @Body since CI/CD complains.

* VR-228: remove id attribute since it's no longer needed.

* VR-228: avoiding allowing excess properties for partial queries.

* VR-228: avoid arbitrary data in the request body

* VR-228: tidy up and a comment .

* VR-228: validation check and grouping of partial query inputs.

* VR-228: quantity -> quantities (rename).

* VR-228: partial validation tests.

* VR-228: typo.:
  • Loading branch information
n3op2 authored Sep 30, 2024
1 parent 69c9bd8 commit d0f79d0
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 38 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veritable-ui",
"version": "0.9.4",
"version": "0.9.5",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "module",
Expand Down
111 changes: 107 additions & 4 deletions src/controllers/queries/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,110 @@ describe('QueriesController', () => {
expect(result).to.equal('scope3_error_scope3')
})
})

describe('query responses', () => {
describe('partial query responses', () => {
describe('if invalid partial input', () => {
it('throws if connectionsIds array is not in the req.body', async () => {
const { args } = withQueriesMocks()
const controller = new QueriesController(...args)
try {
await controller.scope3CarbonConsumptionResponseSubmit(req, 'query-partial-id-test', {
companyId: 'some-company-id',
action: 'success',
partialQuery: ['on'],
productIds: ['product-1', 'product-2'],
quantities: ['10', '20'],
})
// the below expect should never happen since we expect test to throw
expect(false).to.be.equal(true)
} catch (err) {
expect(err.toString()).to.be.equal('Error: missing a property in the request body')
}
})

it('throws if productIds array is not in the req.body', async () => {
const { args } = withQueriesMocks()
const controller = new QueriesController(...args)
try {
await controller.scope3CarbonConsumptionResponseSubmit(req, 'query-partial-id-test', {
companyId: 'some-company-id',
action: 'success',
partialQuery: ['on'],
connectionIds: ['conn-id-1', 'conn-id-2'],
quantities: ['10', '20'],
})
// the below expect should never happen since we expect test to throw
expect(false).to.be.equal(true)
} catch (err) {
expect(err.toString()).to.be.equal('Error: missing a property in the request body')
}
})

it('throws if quantities and productIds arrays are not req.body', async () => {
const { args } = withQueriesMocks()
const controller = new QueriesController(...args)
try {
await controller.scope3CarbonConsumptionResponseSubmit(req, 'query-partial-id-test', {
companyId: 'some-company-id',
action: 'success',
partialQuery: ['on'],
connectionIds: ['conn-id-1', 'conn-id-2'],
})
// the below expect should never happen since we expect test to throw
expect(false).to.be.equal(true)
} catch (err) {
expect(err.toString()).to.be.equal('Error: missing a property in the request body')
}
})

it('throws if arrays are not the same size', async () => {
const { args } = withQueriesMocks()
const controller = new QueriesController(...args)
try {
await controller.scope3CarbonConsumptionResponseSubmit(req, 'query-partial-id-test', {
companyId: 'some-company-id',
action: 'success',
partialQuery: ['on'],
productIds: ['product-id-1'],
quantities: ['1', '2', '3'],
connectionIds: ['conn-id-1', 'conn-id-2'],
})
// the below expect should never happen since we expect test to throw
expect(false).to.be.equal(true)
} catch (err) {
expect(err.toString()).to.be.equal('Error: partial query validation failed, invalid data')
}
})
})

it('sets a partial query status to resolved and renders response template', async () => {
const { args, dbMock } = withQueriesMocks()
const controller = new QueriesController(...args)
const getSpy = dbMock.get
const updateSpy = dbMock.update
const result = await controller
.scope3CarbonConsumptionResponseSubmit(req, 'query-partial-id-test', {
companyId: 'some-company-id',
action: 'success',
partialQuery: ['on'],
connectionIds: ['conn-id-1', 'conn-id-2'],
productIds: ['product-1', 'product-2'],
quantities: ['10', '20'],
})
.then(toHTMLString)

expect(getSpy.firstCall.calledWith('connection', { id: 'some-company-id', status: 'verified_both' })).to.equal(
true
)
expect(getSpy.secondCall.calledWith('query', { id: 'query-partial-id-test' })).to.equal(true)
expect(
updateSpy.firstCall.calledWith('query', { id: 'query-partial-id-test' }, { status: 'resolved' })
).to.equal(true)
expect(result).to.be.equal('queriesResponse_template')
})
})

it('should call db as expected', async () => {
const { args, dbMock } = withQueriesMocks()
const controller = new QueriesController(...args)
Expand All @@ -164,7 +267,7 @@ describe('QueriesController', () => {
.scope3CarbonConsumptionResponseSubmit(req, '5390af91-c551-4d74-b394-d8ae0805059e', {
companyId: '2345789',
action: 'success',
totalScope3CarbonEmissions: '25',
emissions: '25',
})
.then(toHTMLString)

Expand All @@ -181,7 +284,7 @@ describe('QueriesController', () => {
.scope3CarbonConsumptionResponseSubmit(req, '5390af91-c551-4d74-b394-d8ae0805059e', {
companyId: '2345789',
action: 'success',
totalScope3CarbonEmissions: '25',
emissions: '25',
})
.then(toHTMLString)

Expand All @@ -197,7 +300,7 @@ describe('QueriesController', () => {
.scope3CarbonConsumptionResponseSubmit(req, '5390af91-c551-4d74-b394-d8ae0805059e', {
companyId: '2345789',
action: 'success',
totalScope3CarbonEmissions: '25',
emissions: '25',
})
.then(toHTMLString)

Expand All @@ -217,7 +320,7 @@ describe('QueriesController', () => {
.scope3CarbonConsumptionResponseSubmit(req, '5390af91-c551-4d74-b394-d8ae0805059e', {
companyId: '2345789',
action: 'success',
totalScope3CarbonEmissions: '25',
emissions: '25',
})
.then(toHTMLString)

Expand Down
82 changes: 59 additions & 23 deletions src/controllers/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { injectable } from 'tsyringe'

import pino from 'pino'
import { InvalidInputError, NotFoundError } from '../../errors.js'
import { PartialQueryPayload, type PartialQuery } from '../../models/arrays.js'
import Database from '../../models/db/index.js'
import { ConnectionRow, QueryRow, Where } from '../../models/db/types.js'
import { type UUID } from '../../models/strings.js'
Expand Down Expand Up @@ -246,7 +247,7 @@ export class QueriesController extends HTMLController {
* @returns a table of connections for partial query
*/
@SuccessResponse(200)
@Get('/{queryId}/partial/{companyId}')
@Get('/{queryId}/partial')
public async scope3CO2Partial(
@Request() req: express.Request,
@Path() queryId: UUID,
Expand Down Expand Up @@ -286,7 +287,7 @@ export class QueriesController extends HTMLController {
* @returns - a tabe row for partial query
*/
@SuccessResponse(200)
@Get('/partial-select/{connectionId}/')
@Get('/partial-select/{connectionId}')
public async partialSelect(
@Request() req: express.Request,
@Path() connectionId: UUID,
Expand All @@ -311,6 +312,8 @@ export class QueriesController extends HTMLController {

/**
* Submits the query response page
* @param connections - since table contains only 3 cells this data will need to be
* devided into chunks of size 3
*/
@SuccessResponse(200)
@Post('/scope-3-carbon-consumption/{queryId}/response')
Expand All @@ -321,33 +324,63 @@ export class QueriesController extends HTMLController {
body: {
companyId: UUID
action: 'success'
totalScope3CarbonEmissions: string
emissions?: string
partialQuery?: 'on'[]
partialSelect?: 'on'[]
connectionIds?: string[]
productIds?: string[]
quantities?: string[]
}
): Promise<HTML> {
req.log.info('query page requested %j', { queryId, body })
const { action, companyId, emissions, partialQuery, partialSelect, ...partial } = body
req.log.info('query page requested %j', { body })

const [connection] = await this.db.get('connection', { id: body.companyId, status: 'verified_both' }, [
const [connection]: ConnectionRow[] = await this.db.get('connection', { id: companyId, status: 'verified_both' }, [
['updated_at', 'desc'],
])
if (!connection) {
req.log.warn('invalid input error %j', body)
throw new InvalidInputError(`Invalid connection ${body.companyId}`)
req.log.warn('invalid input error %j', { companyId, action })
throw new InvalidInputError(`Invalid connection ${companyId}`)
}
if (!connection.agent_connection_id || connection.status !== 'verified_both') {
req.log.warn('invalid input error %j', body)
req.log.warn('invalid input error %j', { companyId, action, emissions })
throw new InvalidInputError(`Cannot query unverified connection`)
}
const [queryRow] = await this.db.get('query', { id: queryId })
const [queryRow]: QueryRow[] = await this.db.get('query', { id: queryId })
if (!queryRow.response_id) {
req.log.warn('missing DRPC response_id to respond to %j', queryRow)
throw new InvalidInputError(`Missing queryId to respond to.`)
throw new InvalidInputError(`Missing response_id to respond to.`)
}

const query = {
emissions: body.totalScope3CarbonEmissions,
const partialConnections: PartialQuery[] = []
if (partial && partialQuery) {
if (!partial.connectionIds || !partial.productIds || !partial.quantities) {
throw new InvalidInputError('missing a property in the request body')
}
req.log.info('processing partial query %j', partial)
const size: number = this.validatePartialQuery(partial)
req.log.debug('partial query has been validated %j', { partial, size })

for (let i = 0; i < size; i++) {
partialConnections.push({
connectionId: partial.connectionIds[i],
productId: partial.productIds[i],
quantity: parseInt(partial.quantities[i]),
})
req.log.info('partial connection has been formatted %j', partialConnections[i])
}

req.log.debug('formatted partial connections %j', partialConnections)
}

const query: { emissions: string; queryIdForResponse: UUID } = {
emissions:
(emissions as string) ||
partialConnections.reduce((out: number, next: PartialQuery) => (out += next.quantity), 0).toString(),
queryIdForResponse: queryRow.response_id,
}
req.log.debug('query for DRPC response %j', query)

req.log.debug('query for DRPC response with aggregated emissions %j', query)
//send a drpc message with response
let rpcResponse: DrpcResponse
try {
Expand All @@ -361,12 +394,12 @@ export class QueriesController extends HTMLController {
)
req.log.info('submitting DRPC request %j', maybeResponse)
if (!maybeResponse) {
return await this.handleError(req.log, queryRow, connection)
return this.handleError(req.log, queryRow, connection)
}
rpcResponse = maybeResponse
} catch (err) {
req.log.warn('DRPC has failed %j', err)
return await this.handleError(req.log, queryRow, connection, undefined, err)
return this.handleError(req.log, queryRow, connection, undefined, err)
}
const { result, error, id: rpcId } = rpcResponse

Expand All @@ -379,17 +412,11 @@ export class QueriesController extends HTMLController {
result,
error,
})
await this.db.update(
'query',
{ id: queryId },
{
status: 'resolved',
}
)
await this.db.update('query', { id: queryId }, { status: 'resolved' })

if (!result || error) {
req.log.warn('error happened while persisting query_rpc %j', error)
return await this.handleError(req.log, queryRow, connection, rpcId)
return this.handleError(req.log, queryRow, connection, rpcId)
}

return this.html(
Expand Down Expand Up @@ -434,6 +461,15 @@ export class QueriesController extends HTMLController {
)
}

private validatePartialQuery({ connectionIds: a, productIds: b, quantities: c }: PartialQueryPayload): number {
if (!a || !b || !c) throw new InvalidInputError('empty arrays of data provided')
if (a.length !== b.length || a.length !== c.length || b.length !== c.length) {
throw new InvalidInputError('partial query validation failed, invalid data')
}

return a.length
}

private async handleError(
logger: pino.Logger,
query: QueryRow,
Expand Down
1 change: 1 addition & 0 deletions src/models/__tests__/companyHouseEntity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('companyHouseEntity', () => {
const environment = new Env()
const companyHouseObject = new CompanyHouseEntity(environment)
const response = await companyHouseObject.localCompanyHouseProfile()

expect(response).deep.equal(successResponse)
})
})
Expand Down
13 changes: 13 additions & 0 deletions src/models/__tests__/fixtures/companyHouseFixtures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CompanyProfile } from '../../companyHouseEntity.js'

export const validCompanyNumber = '07964699'
export const secondaryCompanyNumber = '11111111'
export const invalidCompanyNumber = '079646992'
export const noCompanyNumber = ''

Expand All @@ -15,3 +16,15 @@ export const successResponse: CompanyProfile = {
company_name: 'DIGITAL CATAPULT',
company_number: '07964699',
}

export const successResponse2: CompanyProfile = {
registered_office_address: {
address_line_1: 'Flat 3 Nelmes Court, Nelmes Way, Nelmes Way',
postal_code: 'RM11 2QL',
locality: 'Hornchurch',
},
company_status: 'active',
registered_office_is_in_dispute: false,
company_name: 'CARE ONUS LTD',
company_number: '11111111',
}
10 changes: 10 additions & 0 deletions src/models/__tests__/helpers/mockCompanyHouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Env } from '../../../env.js'
import {
invalidCompanyNumber,
noCompanyNumber,
secondaryCompanyNumber,
successResponse,
successResponse2,
validCompanyNumber,
} from '../fixtures/companyHouseFixtures.js'

Expand All @@ -27,6 +29,14 @@ export function withCompanyHouseMock() {
.reply(200, successResponse)
.persist()

client
.intercept({
path: `/company/${secondaryCompanyNumber}`,
method: 'GET',
})
.reply(200, successResponse2)
.persist()

client
.intercept({
path: `/company/${noCompanyNumber}`,
Expand Down
6 changes: 6 additions & 0 deletions src/models/arrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type PartialQuery = { connectionId: string; productId: string; quantity: number }
export type PartialQueryPayload = {
connectionIds?: string[]
productIds?: string[]
quantities?: string[]
}
Loading

0 comments on commit d0f79d0

Please sign in to comment.