Skip to content
Merged
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
1 change: 0 additions & 1 deletion src/client/javascripts/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import createSearchPlugin from '@defra/interactive-map/plugins/search'
// @ts-expect-error - no types
import maplibreProvider from '@defra/interactive-map/providers/maplibre'
import { centroid } from '@turf/centroid'
// @ts-expect-error - no types
import OsGridRef, { LatLon } from 'geodesy/osgridref.js'

import { processGeospatial } from '~/src/client/javascripts/geospatial-map.js'
Expand Down
5 changes: 1 addition & 4 deletions src/server/plugins/engine/components/ComponentCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,7 @@ export class ComponentCollection {

// Update error with parent title
if (translator && parent) {
error.local.title ??= translator.tComponent(
parent as ComponentDef,
'title'
)
error.local.title ??= getTranslatedLabel(parent.def, translator)
} else {
error.local.title ??= parent?.label
}
Expand Down
84 changes: 84 additions & 0 deletions src/server/plugins/engine/components/DatePartsField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,90 @@ describe('DatePartsField', () => {
})
})

describe('Validation with translator', () => {
describe.each([
{
description: 'error description',
def: {
title: 'Example date parts field',
shortDescription: 'Example date parts',
errorDescription: 'Example date error',
name: 'myComponent',
type: ComponentType.DatePartsField,
options: {}
} satisfies DatePartsFieldComponent,
expectedLabel: 'Example date error'
},
{
description: 'short description',
def: {
title: 'Example date parts field',
shortDescription: 'Example date parts',
name: 'myComponent',
type: ComponentType.DatePartsField,
options: {}
} satisfies DatePartsFieldComponent,
expectedLabel: 'Example date parts'
},
{
description: 'title',
def: {
title: 'Example date parts field',
name: 'myComponent',
type: ComponentType.DatePartsField,
options: {}
} satisfies DatePartsFieldComponent,
expectedLabel: 'Example date parts field'
}
])('Error label uses $description', ({ def, expectedLabel }) => {
let collection: ComponentCollection

beforeEach(() => {
collection = new ComponentCollection([def], { model })
})

it('uses the label for missing date parts', () => {
const result = collection.validate(
getFormData({
day: '',
month: '',
year: ''
}),
translator
)

expect(result.errors).toEqual([
expect.objectContaining({
text: `${expectedLabel} must include a day`
}),
expect.objectContaining({
text: `${expectedLabel} must include a month`
}),
expect.objectContaining({
text: `${expectedLabel} must include a year`
})
])
})

it('uses the label for an invalid date', () => {
const result = collection.validate(
getFormData({
day: '32',
month: '1',
year: '2024'
}),
translator
)

expect(result.errors).toEqual([
expect.objectContaining({
text: `${expectedLabel} must be a real date`
})
])
})
})
})

describe('sub-field title key constants', () => {
let dateParts: DatePartsField

Expand Down
1 change: 0 additions & 1 deletion src/server/plugins/engine/components/helpers/geospatial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
import Bourne from '@hapi/bourne'
import { booleanWithin } from '@turf/boolean-within'
import { point } from '@turf/helpers'
// @ts-expect-error - no types
import OsGridRef from 'geodesy/osgridref.js'
import JoiBase, { type CustomValidator } from 'joi'

Expand Down
17 changes: 13 additions & 4 deletions src/server/plugins/nunjucks/enviroment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { environment } from '~/src/server/plugins/nunjucks/environment.js'

describe('Nunjucks environment', () => {
describe('t global function', () => {
/** @type {typeof tGlobal} */
let t

beforeEach(() => {
t = environment.getGlobal('t')
})

it('has t as a global function', () => {
const globals = /** @type {any} */ (environment).globals
expect(typeof globals.t).toBe('function')
expect(typeof t).toBe('function')
})

it('t global returns the English string for a known key', () => {
const globals = /** @type {any} */ (environment).globals
expect(globals.t('errors.title')).toBe('There is a problem')
expect(t('errors.title')).toBe('There is a problem')
})
})

Expand Down Expand Up @@ -214,6 +219,10 @@ describe('Nunjucks environment', () => {
})
})

/**
* @import { tGlobal } from '~/src/server/plugins/nunjucks/environment.js'
*/

/*
* @import { ComponentViewModel } from '~/src/server/plugins/engine/components/types.js'
*/
19 changes: 16 additions & 3 deletions src/server/plugins/postcode-lookup/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,19 @@ export const stepSchema = Joi.string()
.valid(...Object.keys(steps))
.required()

/**
* Joi object schema factories typed with their payload, as JavaScript files
* cannot pass type arguments to `Joi.object<T>()`
*/
/** @type {() => ObjectSchema<PostcodeLookupDetailsPayload>} */
const detailsObjectSchema = Joi.object.bind(Joi)

/** @type {() => ObjectSchema<PostcodeLookupSelectPayload>} */
const selectObjectSchema = Joi.object.bind(Joi)

/** @type {() => ObjectSchema<PostcodeLookupManualPayload>} */
const manualObjectSchema = Joi.object.bind(Joi)

const sharedPayloadSchemaKeys = {
crumb: crumbSchema,
step: stepSchema
Expand All @@ -309,7 +322,7 @@ const sharedPayloadSchemaKeys = {
* @returns {ObjectSchema<PostcodeLookupDetailsPayload>}
*/
export function createDetailsPayloadSchema(language = 'en-GB') {
return Joi.object()
return detailsObjectSchema()
.keys({
...sharedPayloadSchemaKeys,
[postcodeQueryFieldName]: Joi.string()
Expand Down Expand Up @@ -338,7 +351,7 @@ export function createDetailsPayloadSchema(language = 'en-GB') {
* @returns {ObjectSchema<PostcodeLookupSelectPayload>}
*/
export function createSelectPayloadSchema(language = 'en-GB') {
return Joi.object()
return selectObjectSchema()
.keys({
...sharedPayloadSchemaKeys,
[uprnFieldName]: Joi.string()
Expand All @@ -356,7 +369,7 @@ export function createSelectPayloadSchema(language = 'en-GB') {
* @returns {ObjectSchema<PostcodeLookupManualPayload>}
*/
export function createManualPayloadSchema(language = 'en-GB') {
return Joi.object()
return manualObjectSchema()
.keys({
...sharedPayloadSchemaKeys,
[line1FieldName]: Joi.string()
Expand Down
31 changes: 23 additions & 8 deletions src/server/plugins/postcode-lookup/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ function postRoute(options) {
}
}

/**
* Validate a payload against a schema. Joi types the value as `any` when
* validation fails, so it is typed here as the partially converted payload.
* @template TSchema
* @param {ObjectSchema<TSchema>} schema
* @param {unknown} payload
* @param {ValidationOptions} [options]
* @returns {{ value: TSchema, error: undefined } | { value: TSchema | undefined, error: ValidationError }}
*/
function validatePayload(schema, payload, options) {
return schema.validate(payload, options)
}

/**
* Post handler for the details step
* @param {PostcodeLookupPostRequest} request
Expand All @@ -214,9 +227,10 @@ async function detailsPostHandler(request, h, options) {
)
const language = translator.language

const { value: details, error } = createDetailsPayloadSchema(
language
).validate(request.payload)
const { value: details, error } = validatePayload(
createDetailsPayloadSchema(language),
request.payload
)

let model

Expand Down Expand Up @@ -252,7 +266,8 @@ async function selectPostHandler(request, h, options) {
request.query.language
)
const language = translator.language
const { value: select, error } = createSelectPayloadSchema(language).validate(
const { value: select, error } = validatePayload(
createSelectPayloadSchema(language),
request.payload
)

Expand Down Expand Up @@ -295,11 +310,10 @@ async function manualPostHandler(request, h) {
)
const language = translator.language

const { value: manual, error } = createManualPayloadSchema(language).validate(
const { value: manual, error } = validatePayload(
createManualPayloadSchema(language),
request.payload,
{
abortEarly: false
}
{ abortEarly: false }
)

if (error) {
Expand All @@ -317,6 +331,7 @@ async function manualPostHandler(request, h) {

/**
* @import { ResponseToolkit, ServerRoute } from '@hapi/hapi'
* @import { ObjectSchema, ValidationError, ValidationOptions } from 'joi'
* @import { FormMetadata } from '@defra/forms-model'
* @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js'
* @import { FormRequestPayload, FormResponseToolkit } from '~/src/server/routes/types.js'
Expand Down
18 changes: 18 additions & 0 deletions src/typings/geodesy/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
declare module 'geodesy/osgridref.js' {
export class LatLon {
constructor(lat: number, lon: number, height?: number)
get latitude(): number
get longitude(): number
get height(): number
toOsGrid(): OsGridRef
}

export default class OsGridRef {
constructor(easting: number, northing: number)
easting: number
northing: number
toLatLon(): LatLon
toString(digits?: number): string
static parse(gridref: string): OsGridRef
}
}
Loading