Skip to content

Commit

Permalink
feat(Forms): add validation for dnr and fnr in `Field.NationalIdentit…
Browse files Browse the repository at this point in the history
…yNumber` (#3771)

Motivation from
https://dnb-it.slack.com/archives/CMXABCHEY/p1720521252444219

---------

Co-authored-by: Tobias Høegh <[email protected]>
  • Loading branch information
langz and tujoworker authored Sep 2, 2024
1 parent 652448c commit 8a2da43
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,28 @@ export const ValidationRequired = () => {
)
}

export const ValidationFnr = () => {
return (
<ComponentBox>
<Field.NationalIdentityNumber
value="29020112345"
validateInitially
/>
</ComponentBox>
)
}

export const ValidationDnr = () => {
return (
<ComponentBox>
<Field.NationalIdentityNumber
value="69020112345"
validateInitially
/>
</ComponentBox>
)
}

export const ValidationFunction = () => {
return (
<ComponentBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,24 @@ import * as Examples from './Examples'

<Examples.ValidationRequired />

### Validation - Norwegian national identity numbers

It validates [Norwegian national identity numbers(fnr)](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/fodselsnummer/) using the [fnrvalidator](https://github.com/navikt/fnrvalidator).

Below is an example of the error message displayed when there's an invalid Norwegian national identity number(fnr):

<Examples.ValidationFnr />

### Validation - D numbers

It validates [D numbers](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/d-nummer/) using the [fnrvalidator](https://github.com/navikt/fnrvalidator).

Below is an example of the error message displayed when there's an invalid D number:

<Examples.ValidationDnr />

### Validation function

You can provide your own validation function or may use the one [from NAV](https://github.com/navikt/fnrvalidator).
You can provide your own validation function.

<Examples.ValidationFunction />
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ showTabs: true

`Field.NationalIdentityNumber` is a wrapper component for the [input of strings](/uilib/extensions/forms/base-fields/String), with user experience tailored for national identity number values.

This field is meant for norwegian national identity numbers, and therefor takes a 11-digit string as a value. A norwegian national identity number can have a leading zero, hence why its a string and not a number.
This field is meant for [norwegian national identity numbers(fnr)](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/fodselsnummer/) and [D numbers](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/d-nummer/), and therefor takes a 11-digit string as a value. A norwegian national identity number can have a leading zero, hence why its a string and not a number.
More info can be found at [Skatteetaten](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/fodselsnummer/#:~:text=A%20national%20identity%20number%20consists,national%20identity%20number%20are%20220676)

It validates input for [norwegian national identity numbers(fnr)](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/fodselsnummer/) and [D numbers](https://www.skatteetaten.no/en/person/national-registry/identitetsnummer/d-nummer/) using the [fnrvalidator](https://github.com/navikt/fnrvalidator).

```jsx
import { Field } from '@dnb/eufemia/extensions/forms'
render(<Field.NationalIdentityNumber />)
Expand Down
1 change: 1 addition & 0 deletions packages/dnb-eufemia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"typings": "./index.d.ts",
"dependencies": {
"@babel/runtime": "7.22.5",
"@navikt/fnrvalidator": "1.3.0",
"@ungap/structured-clone": "1.2.0",
"ajv": "8.14.0",
"ajv-errors": "3.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import StringField, { Props as StringFieldProps } from '../String'
import { dnr, fnr } from '@navikt/fnrvalidator'

import useErrorMessage from '../../hooks/useErrorMessage'
import useTranslation from '../../hooks/useTranslation'
Expand Down Expand Up @@ -40,17 +41,58 @@ function NationalIdentityNumber(props: Props) {
],
[omitMask]
)
const validationPattern = '^[0-9]{11}$'

const fnrValidator = useCallback(
(value: string) => {
if (
new RegExp(validationPattern).test(value) &&
fnr(value).status === 'invalid'
) {
return Error(translations.errorFnr)
}
return undefined
},
[translations.errorFnr]
)

const dnrValidator = useCallback(
(value: string) => {
const validationPattern = '^[4-7]([0-9]{10}$)' // 1st num is increased by 4. i.e, if 01.01.1985, D number would be 410185.
if (
new RegExp(validationPattern).test(value) &&
dnr(value).status === 'invalid'
) {
return Error(translations.errorDnr)
}
return undefined
},
[translations.errorDnr]
)

const dnrAndFnrValidator = useCallback(
(value: string) => {
return dnrValidator(value) || fnrValidator(value)
},
[dnrValidator, fnrValidator]
)

const StringFieldProps: Props = {
...props,
pattern:
props.pattern ??
(validate && !props.validator ? '^[0-9]{11}$' : undefined),
validate && props.pattern
? props.pattern
: validate && !props.validator
? validationPattern
: undefined,
label: props.label ?? translations.label,
errorMessages,
mask,
width: props.width ?? 'medium',
inputMode: 'numeric',
validator: validate
? props.validator || dnrAndFnrValidator
: undefined,
}

return <StringField {...StringFieldProps} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React from 'react'
import { fireEvent, render, waitFor } from '@testing-library/react'
import { fireEvent, render, waitFor, screen } from '@testing-library/react'
import { Props } from '..'
import { Field, Form } from '../../..'
import nbNO from '../../../constants/locales/nb-NO'

const nb = nbNO['nb-NO']

async function expectNever(callable: () => unknown): Promise<void> {
await expect(() => waitFor(callable)).rejects.toEqual(expect.anything())
}

describe('Field.NationalIdentityNumber', () => {
it('should render with props', () => {
Expand Down Expand Up @@ -39,27 +46,25 @@ describe('Field.NationalIdentityNumber', () => {
'.dnb-forms-submit-button'
)

expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

fireEvent.click(buttonElement)

expect(document.querySelector('.dnb-form-status')).toBeInTheDocument()
expect(screen.queryByRole('alert')).toBeInTheDocument()
})

it('should execute validateInitially if required', () => {
it('should execute validateInitially if required', async () => {
const { rerender } = render(
<Field.NationalIdentityNumber required validateInitially />
)

expect(document.querySelector('.dnb-form-status')).toBeInTheDocument()
expect(screen.queryByRole('alert')).toBeInTheDocument()

rerender(<Field.NationalIdentityNumber validateInitially />)

expect(
document.querySelector('.dnb-form-status')
).not.toBeInTheDocument()
await waitFor(() => {
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
})

it('should validate given function', async () => {
Expand Down Expand Up @@ -115,4 +120,181 @@ describe('Field.NationalIdentityNumber', () => {

expect(input).toHaveAttribute('inputmode', 'numeric')
})

it('should not validate pattern when validate false', async () => {
const invalidPattern = '1234'
render(
<Field.NationalIdentityNumber
value={invalidPattern}
validateInitially
validate={false}
/>
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it('should not validate custom pattern when validate false', async () => {
const invalidPattern = '1234'
render(
<Field.NationalIdentityNumber
pattern="[A-Z]"
value={invalidPattern}
validateInitially
validate={false}
/>
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it('should not validate dnum when validate false', async () => {
const invalidDnum = '69020112345'
render(
<Field.NationalIdentityNumber
value={invalidDnum}
validateInitially
validate={false}
/>
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it('should not validate fnr when validate false', async () => {
const invalidFnr = '29020112345'
render(
<Field.NationalIdentityNumber
value={invalidFnr}
validateInitially
validate={false}
/>
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it('should not validate custom validator when validate false', async () => {
const text = 'Custom Error message'
const validator = jest.fn((value) => {
return value.length < 4 ? new Error(text) : undefined
})

render(
<Field.NationalIdentityNumber
value="123"
required
validator={validator}
validateInitially
validate={false}
/>
)

await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

describe('should validate Norwegian D number', () => {
const validDNum = [
'53097248016',
'51041678171',
'58081633086',
'53050129159',
'65015439860',
'51057844748',
'71075441007',
]

const invalidDNum = [
'69020112345',
'53097248032',
'53097248023',
'72127248022',
'53137248022',
]

it.each(validDNum)('Valid D number: %s', async (dNum) => {
render(
<Field.NationalIdentityNumber value={dNum} validateInitially />
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
})

it.each(invalidDNum)('Invalid D number: %s', async (dNum) => {
render(
<Field.NationalIdentityNumber value={dNum} validateInitially />
)

await waitFor(() => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).toHaveTextContent(
nb.NationalIdentityNumber.errorDnr
)
})
})
})

describe('should validate Norwegian national identity number(fnr)', () => {
const validFnrNum = [
'08121312590',
'12018503288',
'03025742965',
'14046512368',
'21033601864',
'27114530463',
'07014816857',
'11069497545',
'22032012969',
'10042223293',
]

const invalidFnrNum = [
'29020112345',
'13097248032',
'13097248023',
'32127248022',
'13137248022',
]

it.each(validFnrNum)(
'Valid national identity number(fnr): %s',
async (fnrNum) => {
render(
<Field.NationalIdentityNumber validateInitially value={fnrNum} />
)
await expectNever(() => {
// Can't just waitFor and expect not to be in the document, it would approve the first render before the error might appear async.
expect(screen.queryByRole('alert')).toBeInTheDocument()
})
}
)

it.each(invalidFnrNum)(
'Invalid national identity number(fnr): %s',
async (fnrNum) => {
render(
<Field.NationalIdentityNumber validateInitially value={fnrNum} />
)
await waitFor(() => {
expect(screen.queryByRole('alert')).toBeInTheDocument()
expect(screen.queryByRole('alert')).toHaveTextContent(
nb.NationalIdentityNumber.errorFnr
)
})
}
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react'
import { Field } from '../../..'

export default {
title: 'Eufemia/Extensions/Forms/NationalIdentityNumber',
}

export function NationalIdentityNumber() {
return (
<>
<Field.NationalIdentityNumber />
<Field.NationalIdentityNumber value="123" />
<Field.NationalIdentityNumber value="12345678901" />
</>
)
}
Loading

0 comments on commit 8a2da43

Please sign in to comment.