Skip to content

Commit

Permalink
VEGA-2485 Add update type <actor>_CONFIRM_IDENTITY #minor (#218)
Browse files Browse the repository at this point in the history
* Add IdentityCheck property to donor and certificate provider

* Add identity check complete change parser

* Add ID check complete Apply()

* Add ID_CHECK_COMPLETE to openapi spec

* Fix unit test

* Rename Date to CheckedAt

* Incorporate @hawx's PR comments

* Compare ID_CHECK_COMPLETED update fields with existing LPA

* Add API test for update with ID_CHECK_COMPLETE

* Make IdentityCheck a pointer so it can be excluded from JSON output

* IdentityCheck is not always serialised now

* Rename ID_CHECK_COMPLETE to <actor>_CONFIRM_IDENTITY

* Change fieldname from 'date' to 'checkedAt' for CONFIRM_IDENTITY updates

* Make the identity check explicit as a property

* Incorporate improvements suggested by PR review

* Add additional tests for CONFIRM_IDENTITY failure cases

* Reference field is optional for CONFIRM_IDENTITY updates

* Remove MustMatchExisting when validating CONFIRM_IDENTITY change fields

This is now enforced for all changes.

* Assert on presence of specific error messages
  • Loading branch information
townxelliot authored Jun 14, 2024
1 parent b8d41ad commit 973c839
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 9 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ test-api:
# attorney sign
cat ./docs/attorney-sign.json | ./api-test/tester -expectedStatus=201 REQUEST POST $(URL)/lpas/$(LPA_UID)/updates "`xargs -0`"

# donor id check complete
cat ./docs/donor-confirm-identity.json | ./api-test/tester -expectedStatus=201 REQUEST POST $(URL)/lpas/$(LPA_UID)/updates "`xargs -0`"

# certificate provider id check complete
cat ./docs/certificate-provider-confirm-identity.json | ./api-test/tester -expectedStatus=201 REQUEST POST $(URL)/lpas/$(LPA_UID)/updates "`xargs -0`"

# trust corporation sign
cat ./docs/trust-corporation-sign.json | ./api-test/tester -expectedStatus=201 REQUEST POST $(URL)/lpas/$(LPA_UID)/updates "`xargs -0`"

Expand Down
20 changes: 20 additions & 0 deletions docs/certificate-provider-confirm-identity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"type": "CERTIFICATE_PROVIDER_CONFIRM_IDENTITY",
"changes": [
{
"key": "/certificateProvider/identityCheck/checkedAt",
"new": "2024-01-13T22:00:00Z",
"old": null
},
{
"key": "/certificateProvider/identityCheck/type",
"new": "one-login",
"old": null
},
{
"key": "/certificateProvider/identityCheck/reference",
"new": "1234567890",
"old": null
}
]
}
20 changes: 20 additions & 0 deletions docs/donor-confirm-identity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"type": "DONOR_CONFIRM_IDENTITY",
"changes": [
{
"key": "/donor/identityCheck/checkedAt",
"new": "2024-01-13T22:00:00Z",
"old": null
},
{
"key": "/donor/identityCheck/type",
"new": "one-login",
"old": null
},
{
"key": "/donor/identityCheck/reference",
"new": "1234567890",
"old": null
}
]
}
2 changes: 2 additions & 0 deletions docs/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ components:
- PERFECT
- REGISTER
- CERTIFICATE_PROVIDER_OPT_OUT
- DONOR_CONFIRM_IDENTITY
- CERTIFICATE_PROVIDER_CONFIRM_IDENTITY
changes:
type: array
items:
Expand Down
37 changes: 28 additions & 9 deletions internal/shared/person.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@ type Person struct {

type Donor struct {
Person
DateOfBirth Date `json:"dateOfBirth"`
Email string `json:"email"`
OtherNamesKnownBy string `json:"otherNamesKnownBy,omitempty"`
ContactLanguagePreference Lang `json:"contactLanguagePreference"`
DateOfBirth Date `json:"dateOfBirth"`
Email string `json:"email"`
OtherNamesKnownBy string `json:"otherNamesKnownBy,omitempty"`
ContactLanguagePreference Lang `json:"contactLanguagePreference"`
IdentityCheck *IdentityCheck `json:"identityCheck,omitempty"`
}

type CertificateProvider struct {
Person
Email string `json:"email"`
Phone string `json:"phone"`
Channel Channel `json:"channel"`
SignedAt *time.Time `json:"signedAt,omitempty"`
ContactLanguagePreference Lang `json:"contactLanguagePreference,omitempty"`
Email string `json:"email"`
Phone string `json:"phone"`
Channel Channel `json:"channel"`
SignedAt *time.Time `json:"signedAt,omitempty"`
ContactLanguagePreference Lang `json:"contactLanguagePreference,omitempty"`
IdentityCheck *IdentityCheck `json:"identityCheck,omitempty"`
}

type Channel string
Expand Down Expand Up @@ -101,6 +103,23 @@ type PersonToNotify struct {
Person
}

type IdentityCheck struct {
CheckedAt time.Time `json:"checkedAt"`
Reference string `json:"reference"`
Type IdentityCheckType `json:"type"`
}

type IdentityCheckType string

const (
IdentityCheckTypeOneLogin = IdentityCheckType("one-login")
IdentityCheckTypeOpgPaperId = IdentityCheckType("opg-paper-id")
)

func (e IdentityCheckType) IsValid() bool {
return e == IdentityCheckTypeOneLogin || e == IdentityCheckTypeOpgPaperId
}

type HowMakeDecisions string

const (
Expand Down
63 changes: 63 additions & 0 deletions lambda/update/confirm_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"github.com/ministryofjustice/opg-data-lpa-store/internal/shared"
"github.com/ministryofjustice/opg-data-lpa-store/internal/validate"
"github.com/ministryofjustice/opg-data-lpa-store/lambda/update/parse"
)

type IdCheckComplete struct {
Actor idccActor
IdentityCheck *shared.IdentityCheck
}

type idccActor string

var (
donor = idccActor("Donor")
certificateProvider = idccActor("CertificateProvider")
)

func (idcc IdCheckComplete) Apply(lpa *shared.Lpa) []shared.FieldError {
if idcc.Actor == donor {
lpa.Donor.IdentityCheck = idcc.IdentityCheck
} else {
lpa.CertificateProvider.IdentityCheck = idcc.IdentityCheck
}
return nil
}

func validateConfirmIdentity(prefix string, actor idccActor, ic *shared.IdentityCheck, changes []shared.Change) (IdCheckComplete, []shared.FieldError) {
var idcc IdCheckComplete

errors := parse.Changes(changes).
Prefix(prefix, func(p *parse.Parser) []shared.FieldError {
idcc.Actor = actor

if ic == nil {
ic = &shared.IdentityCheck{}
}
idcc.IdentityCheck = ic

return p.
Field("/type", &ic.Type, parse.Validate(func() []shared.FieldError {
return validate.IsValid("", ic.Type)
})).
Field("/checkedAt", &ic.CheckedAt, parse.Validate(func() []shared.FieldError {
return validate.Time("", ic.CheckedAt)
})).
Field("/reference", &ic.Reference, parse.Optional()).
Consumed()
}).
Consumed()

return idcc, errors
}

func validateDonorConfirmIdentity(changes []shared.Change, lpa *shared.Lpa) (IdCheckComplete, []shared.FieldError) {
return validateConfirmIdentity("/donor/identityCheck", donor, lpa.Donor.IdentityCheck, changes)
}

func validateCertificateProviderConfirmIdentity(changes []shared.Change, lpa *shared.Lpa) (IdCheckComplete, []shared.FieldError) {
return validateConfirmIdentity("/certificateProvider/identityCheck", certificateProvider, lpa.CertificateProvider.IdentityCheck, changes)
}
209 changes: 209 additions & 0 deletions lambda/update/confirm_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package main

import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
"time"

"github.com/ministryofjustice/opg-data-lpa-store/internal/shared"
)

func TestConfirmIdentityDonor(t *testing.T) {
today := time.Now()

changes := []shared.Change{
{
Key: "/donor/identityCheck/checkedAt",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + today.Format(time.RFC3339Nano) + `"`),
},
{
Key: "/donor/identityCheck/reference",
Old: json.RawMessage("null"),
New: json.RawMessage(`"xyz"`),
},
{
Key: "/donor/identityCheck/type",
Old: json.RawMessage("null"),
New: json.RawMessage(`"one-login"`),
},
}

idCheckComplete, errors := validateDonorConfirmIdentity(changes, &shared.Lpa{})

assert.Len(t, errors, 0)
assert.Equal(t, "xyz", idCheckComplete.IdentityCheck.Reference)
assert.Equal(t, shared.IdentityCheckTypeOneLogin, idCheckComplete.IdentityCheck.Type)
assert.Equal(t, today.Format(time.RFC3339Nano), idCheckComplete.IdentityCheck.CheckedAt.Format(time.RFC3339Nano))
assert.Equal(t, donor, idCheckComplete.Actor)
}

func TestConfirmIdentityDonorBadFieldsFails(t *testing.T) {
changes := []shared.Change{
// irrelevant field with no prefix
{
Key: "/irrelevant",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + time.Now().Format(time.RFC3339Nano) + `"`),
},
// irrelevant field with prefix
{
Key: "/donor/identityCheck/irrelevant",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + time.Now().Format(time.RFC3339Nano) + `"`),
},
// empty optional field - does not cause an error message
{
Key: "/donor/identityCheck/reference",
Old: json.RawMessage("null"),
New: json.RawMessage(`""`),
},
// invalid value for field
{
Key: "/donor/identityCheck/type",
Old: json.RawMessage("null"),
New: json.RawMessage(`"rinky-dink-login-system"`),
},
}

idCheckComplete, errors := validateDonorConfirmIdentity(changes, &shared.Lpa{})

assert.Len(t, errors, 4)
assert.Contains(t, errors, shared.FieldError{Source: "/changes", Detail: "missing /donor/identityCheck/checkedAt"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/0", Detail: "unexpected change provided"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/1", Detail: "unexpected change provided"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/3/new", Detail: "invalid value"})
assert.Equal(t, &shared.IdentityCheck{Type: "rinky-dink-login-system"}, idCheckComplete.IdentityCheck)
assert.Equal(t, donor, idCheckComplete.Actor)
}

func TestConfirmIdentityDonorANDCertificateProviderFails(t *testing.T) {
changes := []shared.Change{
{
Key: "/certificateProvider/identityCheck/checkedAt",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + time.Now().Format(time.RFC3339Nano) + `"`),
},
{
Key: "/donor/identityCheck/reference",
Old: json.RawMessage("null"),
New: json.RawMessage(`"xyz"`),
},
{
Key: "/donor/identityCheck/type",
Old: json.RawMessage("null"),
New: json.RawMessage(`"one-login"`),
},
}

idCheckComplete, errors := validateDonorConfirmIdentity(changes, &shared.Lpa{})
expectedIdCheckComplete := &shared.IdentityCheck{
Type: shared.IdentityCheckTypeOneLogin,
Reference: "xyz",
}

assert.Len(t, errors, 2)
assert.Contains(t, errors, shared.FieldError{Source: "/changes", Detail: "missing /donor/identityCheck/checkedAt"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/0", Detail: "unexpected change provided"})

assert.Equal(t, expectedIdCheckComplete, idCheckComplete.IdentityCheck)
assert.Equal(t, donor, idCheckComplete.Actor)
}

func TestConfirmIdentityDonorMismatchWithExistingLpaFails(t *testing.T) {
changes := []shared.Change{
{
Key: "/donor/identityCheck/checkedAt",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + time.Now().Format(time.RFC3339Nano) + `"`),
},
{
Key: "/donor/identityCheck/reference",
Old: json.RawMessage("null"),
New: json.RawMessage(`"xyz"`),
},
{
Key: "/donor/identityCheck/type",
Old: json.RawMessage("null"),
New: json.RawMessage(`"one-login"`),
},
}

existingLpa := &shared.Lpa{
LpaInit: shared.LpaInit{
Donor: shared.Donor{
IdentityCheck: &shared.IdentityCheck{
CheckedAt: time.Now().AddDate(-1, 0, 0),
Reference: "notxyz",
Type: "not-one-login",
},
},
},
}

idCheckComplete, errors := validateDonorConfirmIdentity(changes, existingLpa)

assert.Len(t, errors, 3)
assert.Contains(t, errors, shared.FieldError{Source: "/changes/0/old", Detail: "does not match existing value"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/1/old", Detail: "does not match existing value"})
assert.Contains(t, errors, shared.FieldError{Source: "/changes/2/old", Detail: "does not match existing value"})
assert.Equal(t, existingLpa.LpaInit.Donor.IdentityCheck, idCheckComplete.IdentityCheck)
assert.Equal(t, donor, idCheckComplete.Actor)
}

func TestConfirmIdentityCertificateProvider(t *testing.T) {
today := time.Now()

changes := []shared.Change{
{
Key: "/certificateProvider/identityCheck/checkedAt",
Old: json.RawMessage("null"),
New: json.RawMessage(`"` + today.Format(time.RFC3339Nano) + `"`),
},
{
Key: "/certificateProvider/identityCheck/reference",
Old: json.RawMessage("null"),
New: json.RawMessage(`"abn"`),
},
{
Key: "/certificateProvider/identityCheck/type",
Old: json.RawMessage("null"),
New: json.RawMessage(`"opg-paper-id"`),
},
}

idCheckComplete, errors := validateCertificateProviderConfirmIdentity(changes, &shared.Lpa{})

assert.Len(t, errors, 0)
assert.Equal(t, "abn", idCheckComplete.IdentityCheck.Reference)
assert.Equal(t, shared.IdentityCheckTypeOpgPaperId, idCheckComplete.IdentityCheck.Type)
assert.Equal(t, today.Format(time.RFC3339Nano), idCheckComplete.IdentityCheck.CheckedAt.Format(time.RFC3339Nano))
assert.Equal(t, certificateProvider, idCheckComplete.Actor)
}

func TestConfirmIdentityApplyDonor(t *testing.T) {
check := IdCheckComplete{
Actor: donor,
IdentityCheck: &shared.IdentityCheck{},
}

lpa := shared.Lpa{}

check.Apply(&lpa)

assert.Equal(t, check.IdentityCheck, lpa.Donor.IdentityCheck)
}

func TestConfirmIdentityApplyCertificateProvider(t *testing.T) {
check := IdCheckComplete{
Actor: certificateProvider,
IdentityCheck: &shared.IdentityCheck{},
}

lpa := shared.Lpa{}

check.Apply(&lpa)

assert.Equal(t, check.IdentityCheck, lpa.CertificateProvider.IdentityCheck)
}
Loading

0 comments on commit 973c839

Please sign in to comment.