Add estimate_temp range validation and docs note

This commit is contained in:
Kari Lintulaakso
2026-03-11 10:49:04 +02:00
parent 299d58da69
commit 10b38321fd
5 changed files with 74 additions and 2 deletions
+23 -1
View File
@@ -2,7 +2,12 @@ import { beforeEach, beforeAll, afterAll, describe, it, expect } from '@jest/glo
import { Locality, LocalityDetailsType, SpeciesDetailsType } from '../../../../frontend/src/shared/types' import { Locality, LocalityDetailsType, SpeciesDetailsType } from '../../../../frontend/src/shared/types'
import { LogRow } from '../../services/write/writeOperations/types' import { LogRow } from '../../services/write/writeOperations/types'
import { ValidationObject } from '../../../../frontend/src/shared/validators/validator' import { ValidationObject } from '../../../../frontend/src/shared/validators/validator'
import { invalidPollenCreateLocality, invalidPollenTotalCreateLocality, newLocalityBasis } from './data' import {
invalidEstimateTempCreateLocality,
invalidPollenCreateLocality,
invalidPollenTotalCreateLocality,
newLocalityBasis,
} from './data'
import { login, logout, resetDatabase, send, testLogRows, resetDatabaseTimeout, noPermError } from '../utils' import { login, logout, resetDatabase, send, testLogRows, resetDatabaseTimeout, noPermError } from '../utils'
import { pool } from '../../utils/db' import { pool } from '../../utils/db'
@@ -122,6 +127,23 @@ describe('Creating new locality works', () => {
) )
}) })
it('Creation fails when estimate temperature is out of range', async () => {
const { body, status } = await send<ValidationObject[]>('locality', 'PUT', {
locality: invalidEstimateTempCreateLocality,
})
expect(status).toEqual(403)
expect(body).toEqual(
expect.arrayContaining([
{
name: 'Estimated temperature',
error: 'Estimated temperature must be between -999.9 and 999.9',
},
])
)
})
it('Species update also logged correctly', async () => { it('Species update also logged correctly', async () => {
const speciesId = createdLocality!.now_ls.find(ls => ls.com_species.species_name === 'Newspecies')?.species_id const speciesId = createdLocality!.now_ls.find(ls => ls.com_species.species_name === 'Newspecies')?.species_id
const speciesResult = await send<SpeciesDetailsType>(`species/${speciesId}`, 'GET') const speciesResult = await send<SpeciesDetailsType>(`species/${speciesId}`, 'GET')
+11
View File
@@ -256,6 +256,17 @@ export const invalidPollenTotalCreateLocality = {
pers_pollen_other: 20, pers_pollen_other: 20,
} }
export const invalidEstimateTempCreateLocality = {
...newLocalityBasis,
estimate_temp: 1000,
}
export const invalidEstimateTempUpdateLocality = {
...editedLocality,
estimate_temp: -1000,
}
export const invalidPollenUpdateLocality = { export const invalidPollenUpdateLocality = {
...editedLocality, ...editedLocality,
pers_pollen_ap: 101, pers_pollen_ap: 101,
+23 -1
View File
@@ -2,7 +2,12 @@ import { beforeEach, beforeAll, afterAll, describe, it, expect } from '@jest/glo
import { LocalityDetailsType, SpeciesDetailsType } from '../../../../frontend/src/shared/types' import { LocalityDetailsType, SpeciesDetailsType } from '../../../../frontend/src/shared/types'
import { LogRow } from '../../services/write/writeOperations/types' import { LogRow } from '../../services/write/writeOperations/types'
import { ValidationObject } from '../../../../frontend/src/shared/validators/validator' import { ValidationObject } from '../../../../frontend/src/shared/validators/validator'
import { editedLocality, invalidPollenTotalUpdateLocality, invalidPollenUpdateLocality } from './data' import {
editedLocality,
invalidEstimateTempUpdateLocality,
invalidPollenTotalUpdateLocality,
invalidPollenUpdateLocality,
} from './data'
import { login, resetDatabase, send, testLogRows, resetDatabaseTimeout } from '../utils' import { login, resetDatabase, send, testLogRows, resetDatabaseTimeout } from '../utils'
import { pool } from '../../utils/db' import { pool } from '../../utils/db'
@@ -118,6 +123,23 @@ describe('Locality update works', () => {
) )
}) })
it('Update fails when estimate temperature is out of range', async () => {
const { body, status } = await send<ValidationObject[]>('locality', 'PUT', {
locality: invalidEstimateTempUpdateLocality,
})
expect(status).toEqual(403)
expect(body).toEqual(
expect.arrayContaining([
{
name: 'Estimated temperature',
error: 'Estimated temperature must be between -999.9 and 999.9',
},
])
)
})
it('Editing locality without changing anything should succeed', async () => { it('Editing locality without changing anything should succeed', async () => {
const locality = editedLocality const locality = editedLocality
// fixing correct species_id, was 85729 // fixing correct species_id, was 85729
+9
View File
@@ -58,6 +58,15 @@ Instead of calling the `setFieldsWithErrors` function directly with the new erro
This ensures that when multiple components try to update the state at the same time, they don't overwrite each other. The react documentation goes into more detail: https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state This ensures that when multiple components try to update the state at the same time, they don't overwrite each other. The react documentation goes into more detail: https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state
**Decimal fields and runtime type caveat**
Some DB fields use MariaDB `DECIMAL` and are represented as `Decimal` in Prisma-level TypeScript types (for example `estimate_temp` in `frontend/src/shared/types/data.ts`). In runtime data sent to the frontend, these values are JSON-serialized via Decimal.js `toJSON()`, so they arrive as strings (for example `"5.1"`).
- Keep this mismatch in mind when adding UI logic for decimal fields.
- Prefer explicit parsing/conversion at usage points if numeric operations are required.
- `estimate_temp` additionally has validator bounds to avoid DB errors from out-of-range values (`-999.9` to `999.9`).
**Detailed documentation for editingComponents** **Detailed documentation for editingComponents**
- [RadioSelector](./components/editingComponents/RadioSelector.md) - [RadioSelector](./components/editingComponents/RadioSelector.md)
@@ -6,6 +6,7 @@ const pollenFields = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other']
export const pollenTotalValidationError = export const pollenTotalValidationError =
'Combined Arboreal (AP%), Non-arboreal (NAP%), and Other pollen (OP%) must be less than or equal to 100' 'Combined Arboreal (AP%), Non-arboreal (NAP%), and Other pollen (OP%) must be less than or equal to 100'
export const estimateTempRangeValidationError = 'Estimated temperature must be between -999.9 and 999.9'
const validatePollenPercentage = (name: string, num: number) => { const validatePollenPercentage = (name: string, num: number) => {
if (!Number.isInteger(num)) return `${name} must be an integer value` if (!Number.isInteger(num)) return `${name} must be an integer value`
@@ -185,6 +186,13 @@ export const validateLocality = (
return return
}, },
}, },
estimate_temp: {
name: 'Estimated temperature',
asNumber: (num: number) => {
if (num <= -1000 || num >= 1000) return estimateTempRangeValidationError
return
},
},
} }
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName) return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)