From c1afbfae95282cf59bb4cf5a131249b1b553d815 Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell Date: Mon, 18 Dec 2023 14:53:07 +0000 Subject: [PATCH] MLPAB-1637 Add more data to the LPA (#72) --- docs/openapi/openapi.yaml | 132 +++++++++++- internal/shared/lpa.go | 37 +++- internal/shared/person.go | 119 +++++++++-- lambda/create/validate.go | 209 +++++++++++++----- lambda/create/validate_test.go | 374 +++++++++++++++++++++++++++++---- mock-apigw/main.go | 22 +- 6 files changed, 775 insertions(+), 118 deletions(-) diff --git a/docs/openapi/openapi.yaml b/docs/openapi/openapi.yaml index 6d72d196..3665a7d1 100644 --- a/docs/openapi/openapi.yaml +++ b/docs/openapi/openapi.yaml @@ -232,9 +232,17 @@ components: InitialLpa: type: object required: + - lpaType - donor - attorneys + - certificateProvider + - signedAt properties: + lpaType: + type: string + enum: + - property-and-affairs + - personal-welfare donor: $ref: "#/components/schemas/Donor" attorneys: @@ -242,6 +250,55 @@ components: items: $ref: "#/components/schemas/Attorney" minLength: 1 + trustCorporations: + type: array + items: + $ref: "#/components/schemas/TrustCorporation" + certificateProvider: + $ref: "#/components/schemas/CertificateProvider" + peopleToNotify: + type: array + items: + $ref: "#/components/schemas/PersonToNotify" + howAttorneysMakeDecisions: + type: string + enum: + - jointly + - jointly-and-severally + - jointly-for-some-severally-for-others + howAttorneysMakeDecisionsDetails: + type: string + howReplacementAttorneysMakeDecisions: + type: string + enum: + - jointly + - jointly-and-severally + - jointly-for-some-severally-for-others + howReplacementAttorneysMakeDecisionsDetails: + type: string + howReplacementAttorneysStepIn: + type: string + enum: + - all-can-no-longer-act + - one-can-no-longer-act + - another-way + howReplacementAttorneysStepInDetails: + type: string + whenTheLpaCanBeUsed: + type: string + enum: + - when-capacity-lost + - when-has-capacity + lifeSustainingTreatmentOption: + type: string + enum: + - option-a + - option-b + restrictions: + type: string + signedAt: + type: string + format: date-time additionalProperties: false Address: type: object @@ -284,22 +341,15 @@ components: type: object required: - firstNames - - surname - - dateOfBirth + - lastName - address properties: firstNames: type: string x-faker: name.firstName - surname: + lastName: type: string x-faker: name.lastName - dateOfBirth: - type: string - format: date - email: - type: string - x-faker: internet.email address: $ref: "#/components/schemas/Address" additionalProperties: false @@ -307,7 +357,16 @@ components: allOf: - $ref: "#/components/schemas/Person" type: object + required: + - dateOfBirth + - email properties: + dateOfBirth: + type: string + format: date + email: + type: string + x-faker: internet.email otherNamesKnownBy: type: string nullable: true @@ -317,14 +376,67 @@ components: allOf: - $ref: "#/components/schemas/Person" type: object + required: + - dateOfBirth + - email + - status properties: + dateOfBirth: + type: string + format: date + email: + type: string + x-faker: internet.email status: type: string enum: - active - replacement - removed - additionalProperties: false + TrustCorporation: + type: object + required: + - name + - companyNumber + - email + - address + - status + properties: + name: + type: string + companyNumber: + type: string + email: + type: string + x-faker: internet.email + address: + $ref: "#/components/schemas/Address" + status: + type: string + enum: + - active + - replacement + - removed + CertificateProvider: + allOf: + - $ref: "#/components/schemas/Person" + type: object + required: + - email + - channel + properties: + email: + type: string + x-faker: internet.email + channel: + type: string + enum: + - paper + - online + PersonToNotify: + allOf: + - $ref: "#/components/schemas/Person" + type: object Update: type: object required: diff --git a/internal/shared/lpa.go b/internal/shared/lpa.go index 4ba0b87e..56f166bc 100644 --- a/internal/shared/lpa.go +++ b/internal/shared/lpa.go @@ -3,16 +3,41 @@ package shared import "time" type LpaInit struct { - Donor Donor `json:"donor" dynamodbav:""` - Attorneys []Attorney `json:"attorneys" dynamodbav:""` + LpaType LpaType `json:"lpaType"` + Donor Donor `json:"donor"` + Attorneys []Attorney `json:"attorneys"` + TrustCorporations []TrustCorporation `json:"trustCorporations"` + CertificateProvider CertificateProvider `json:"certificateProvider"` + PeopleToNotify []PersonToNotify `json:"peopleToNotify"` + HowAttorneysMakeDecisions HowMakeDecisions `json:"howAttorneysMakeDecisions"` + HowAttorneysMakeDecisionsDetails string `json:"howAttorneysMakeDecisionsDetails"` + HowReplacementAttorneysMakeDecisions HowMakeDecisions `json:"howReplacementAttorneysMakeDecisions"` + HowReplacementAttorneysMakeDecisionsDetails string `json:"howReplacementAttorneysMakeDecisionsDetails"` + HowReplacementAttorneysStepIn HowStepIn `json:"howReplacementAttorneysStepIn"` + HowReplacementAttorneysStepInDetails string `json:"howReplacementAttorneysStepInDetails"` + WhenTheLpaCanBeUsed CanUse `json:"whenTheLpaCanBeUsed"` + LifeSustainingTreatmentOption LifeSustainingTreatment `json:"lifeSustainingTreatmentOption"` + Restrictions string `json:"restrictions"` + SignedAt time.Time `json:"signedAt"` } type Lpa struct { LpaInit - Uid string `json:"uid" dynamodbav:""` - Status LpaStatus `json:"status" dynamodbav:""` - RegistrationDate time.Time `json:"registrationDate" dynamodbav:""` - UpdatedAt time.Time `json:"updatedAt" dynamodbav:""` + Uid string `json:"uid"` + Status LpaStatus `json:"status"` + RegistrationDate time.Time `json:"registrationDate"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type LpaType string + +const ( + LpaTypePersonalWelfare = LpaType("personal-welfare") + LpaTypePropertyAndAffairs = LpaType("property-and-affairs") +) + +func (e LpaType) IsValid() bool { + return e == LpaTypePersonalWelfare || e == LpaTypePropertyAndAffairs } type LpaStatus string diff --git a/internal/shared/person.go b/internal/shared/person.go index 2e78bcfa..febd607c 100644 --- a/internal/shared/person.go +++ b/internal/shared/person.go @@ -1,25 +1,42 @@ package shared type Address struct { - Line1 string `json:"line1" dynamodbav:""` - Line2 string `json:"line2" dynamodbav:""` - Line3 string `json:"line3" dynamodbav:""` - Town string `json:"town" dynamodbav:""` - Postcode string `json:"postcode" dynamodbav:""` - Country string `json:"country" dynamodbav:""` + Line1 string `json:"line1"` + Line2 string `json:"line2"` + Line3 string `json:"line3"` + Town string `json:"town"` + Postcode string `json:"postcode"` + Country string `json:"country"` } type Person struct { - FirstNames string `json:"firstNames" dynamodbav:""` - Surname string `json:"surname" dynamodbav:""` - DateOfBirth Date `json:"dateOfBirth" dynamodbav:""` - Email string `json:"email" dynamodbav:""` - Address Address `json:"address" dynamodbav:""` + FirstNames string `json:"firstNames"` + LastName string `json:"lastName"` + Address Address `json:"address"` } type Donor struct { Person - OtherNamesKnownBy string `json:"otherNamesKnownBy" dynamodbav:""` + DateOfBirth Date `json:"dateOfBirth"` + Email string `json:"email"` + OtherNamesKnownBy string `json:"otherNamesKnownBy"` +} + +type CertificateProvider struct { + Person + Email string `json:"email"` + Channel Channel `json:"channel"` +} + +type Channel string + +const ( + ChannelOnline = Channel("online") + ChannelPaper = Channel("paper") +) + +func (e Channel) IsValid() bool { + return e == ChannelOnline || e == ChannelPaper } type AttorneyStatus string @@ -36,5 +53,81 @@ func (a AttorneyStatus) IsValid() bool { type Attorney struct { Person - Status AttorneyStatus `json:"status" dynamodbav:""` + DateOfBirth Date `json:"dateOfBirth"` + Email string `json:"email"` + Status AttorneyStatus `json:"status"` +} + +type TrustCorporation struct { + Name string `json:"name"` + CompanyNumber string `json:"companyNumber"` + Email string `json:"email"` + Address Address `json:"address"` + Status AttorneyStatus `json:"status"` +} + +type PersonToNotify struct { + Person +} + +type HowMakeDecisions string + +const ( + HowMakeDecisionsUnset = HowMakeDecisions("") + HowMakeDecisionsJointly = HowMakeDecisions("jointly") + HowMakeDecisionsJointlyAndSeverally = HowMakeDecisions("jointly-and-severally") + HowMakeDecisionsJointlyForSomeSeverallyForOthers = HowMakeDecisions("jointly-for-some-severally-for-others") +) + +func (e HowMakeDecisions) IsValid() bool { + return e == HowMakeDecisionsJointly || e == HowMakeDecisionsJointlyAndSeverally || e == HowMakeDecisionsJointlyForSomeSeverallyForOthers +} + +func (e HowMakeDecisions) Unset() bool { + return e == HowMakeDecisionsUnset +} + +type HowStepIn string + +const ( + HowStepInUnset = HowStepIn("") + HowStepInAllCanNoLongerAct = HowStepIn("all-can-no-longer-act") + HowStepInOneCanNoLongerAct = HowStepIn("one-can-no-longer-act") + HowStepInAnotherWay = HowStepIn("another-way") +) + +func (e HowStepIn) IsValid() bool { + return e == HowStepInUnset || e == HowStepInAllCanNoLongerAct || e == HowStepInOneCanNoLongerAct || e == HowStepInAnotherWay +} + +type CanUse string + +const ( + CanUseUnset = CanUse("") + CanUseWhenCapacityLost = CanUse("when-capacity-lost") + CanUseWhenHasCapacity = CanUse("when-has-capacity") +) + +func (e CanUse) IsValid() bool { + return e == CanUseWhenCapacityLost || e == CanUseWhenHasCapacity +} + +func (e CanUse) Unset() bool { + return e == CanUseUnset +} + +type LifeSustainingTreatment string + +const ( + LifeSustainingTreatmentUnset = LifeSustainingTreatment("") + LifeSustainingTreatmentOptionA = LifeSustainingTreatment("option-a") + LifeSustainingTreatmentOptionB = LifeSustainingTreatment("option-b") +) + +func (e LifeSustainingTreatment) IsValid() bool { + return e == LifeSustainingTreatmentOptionA || e == LifeSustainingTreatmentOptionB +} + +func (e LifeSustainingTreatment) Unset() bool { + return e == LifeSustainingTreatmentUnset } diff --git a/lambda/create/validate.go b/lambda/create/validate.go index d5b878cd..29ab8215 100644 --- a/lambda/create/validate.go +++ b/lambda/create/validate.go @@ -3,88 +3,203 @@ package main import ( "fmt" "regexp" + "time" "github.com/ministryofjustice/opg-data-lpa-store/internal/shared" ) -func validateAddress(address shared.Address, prefix string, errors []shared.FieldError) []shared.FieldError { - requiredFields := map[string]string{ - fmt.Sprintf("%s/line1", prefix): address.Line1, - fmt.Sprintf("%s/town", prefix): address.Town, - fmt.Sprintf("%s/country", prefix): address.Country, +var countryCodeRe = regexp.MustCompile("^[A-Z]{2}$") + +func Validate(lpa shared.LpaInit) []shared.FieldError { + activeAttorneyCount, replacementAttorneyCount := countAttorneys(lpa.Attorneys, lpa.TrustCorporations) + + return flatten( + validateIsValid("/lpaType", lpa.LpaType), + required("/donor/firstNames", lpa.Donor.FirstNames), + required("/donor/lastName", lpa.Donor.LastName), + validateDate("/donor/dateOfBirth", lpa.Donor.DateOfBirth), + validateAddress("/donor/address", lpa.Donor.Address), + required("/certificateProvider/firstNames", lpa.CertificateProvider.FirstNames), + required("/certificateProvider/lastName", lpa.CertificateProvider.LastName), + validateAddress("/certificateProvider/address", lpa.CertificateProvider.Address), + validateIsValid("/certificateProvider/channel", lpa.CertificateProvider.Channel), + validateIfElse(lpa.CertificateProvider.Channel == shared.ChannelOnline, + required("/certificateProvider/email", lpa.CertificateProvider.Email), + empty("/certificateProvider/email", lpa.CertificateProvider.Email)), + validateAttorneys("/attorneys", lpa.Attorneys), + validateTrustCorporations("/trustCorporations", lpa.TrustCorporations), + validateIfElse(activeAttorneyCount > 1, + validateIsValid("/howAttorneysMakeDecisions", lpa.HowAttorneysMakeDecisions), + validateUnset("/howAttorneysMakeDecisions", lpa.HowAttorneysMakeDecisions)), + validateIfElse(lpa.HowAttorneysMakeDecisions == shared.HowMakeDecisionsJointlyForSomeSeverallyForOthers, + required("/howAttorneysMakeDecisionsDetails", lpa.HowAttorneysMakeDecisionsDetails), + empty("/howAttorneysMakeDecisionsDetails", lpa.HowAttorneysMakeDecisionsDetails)), + validateIf(replacementAttorneyCount > 0 && lpa.HowAttorneysMakeDecisions == shared.HowMakeDecisionsJointlyAndSeverally, + validateIsValid("/howReplacementAttorneysStepIn", lpa.HowReplacementAttorneysStepIn)), + validateIfElse(lpa.HowReplacementAttorneysStepIn == shared.HowStepInAnotherWay, + required("/howReplacementAttorneysStepInDetails", lpa.HowReplacementAttorneysStepInDetails), + empty("/howReplacementAttorneysStepInDetails", lpa.HowReplacementAttorneysStepInDetails)), + validateIfElse(replacementAttorneyCount > 1 && (lpa.HowReplacementAttorneysStepIn == shared.HowStepInAllCanNoLongerAct || lpa.HowAttorneysMakeDecisions != shared.HowMakeDecisionsJointlyAndSeverally), + validateIsValid("/howReplacementAttorneysMakeDecisions", lpa.HowReplacementAttorneysMakeDecisions), + validateUnset("/howReplacementAttorneysMakeDecisions", lpa.HowReplacementAttorneysMakeDecisions)), + validateIfElse(lpa.HowReplacementAttorneysMakeDecisions == shared.HowMakeDecisionsJointlyForSomeSeverallyForOthers, + required("/howReplacementAttorneysMakeDecisionsDetails", lpa.HowReplacementAttorneysMakeDecisionsDetails), + empty("/howReplacementAttorneysMakeDecisionsDetails", lpa.HowReplacementAttorneysMakeDecisionsDetails)), + validateIf(lpa.LpaType == shared.LpaTypePersonalWelfare, flatten( + validateIsValid("/lifeSustainingTreatmentOption", lpa.LifeSustainingTreatmentOption), + validateUnset("/whenTheLpaCanBeUsed", lpa.WhenTheLpaCanBeUsed))), + validateIf(lpa.LpaType == shared.LpaTypePropertyAndAffairs, flatten( + validateIsValid("/whenTheLpaCanBeUsed", lpa.WhenTheLpaCanBeUsed), + validateUnset("/lifeSustainingTreatmentOption", lpa.LifeSustainingTreatmentOption))), + validateTime("/signedAt", lpa.SignedAt), + ) +} + +func countAttorneys(as []shared.Attorney, ts []shared.TrustCorporation) (actives, replacements int) { + for _, a := range as { + switch a.Status { + case shared.AttorneyStatusActive: + actives++ + case shared.AttorneyStatusReplacement: + replacements++ + } } - for source, value := range requiredFields { - if value == "" { - errors = append(errors, shared.FieldError{Source: source, Detail: "field is required"}) + for _, t := range ts { + switch t.Status { + case shared.AttorneyStatusActive: + actives++ + case shared.AttorneyStatusReplacement: + replacements++ } } - if ok, _ := regexp.MatchString("^[A-Z]{2}$", address.Country); !ok { - errors = append(errors, shared.FieldError{Source: fmt.Sprintf("%s/country", prefix), Detail: "must be a valid ISO-3166-1 country code"}) + return actives, replacements +} + +func flatten(fieldErrors ...[]shared.FieldError) []shared.FieldError { + var errors []shared.FieldError + + for _, e := range fieldErrors { + if e != nil { + errors = append(errors, e...) + } } return errors } -func validateAttorney(attorney shared.Attorney, prefix string, errors []shared.FieldError) []shared.FieldError { - requiredFields := map[string]string{ - fmt.Sprintf("%s/firstNames", prefix): attorney.FirstNames, - fmt.Sprintf("%s/surname", prefix): attorney.Surname, - fmt.Sprintf("%s/status", prefix): string(attorney.Status), +func validateIfElse(ok bool, eIf []shared.FieldError, eElse []shared.FieldError) []shared.FieldError { + if ok { + return eIf } - for source, value := range requiredFields { - if value == "" { - errors = append(errors, shared.FieldError{Source: source, Detail: "field is required"}) - } + return eElse +} + +func validateIf(ok bool, e []shared.FieldError) []shared.FieldError { + return validateIfElse(ok, e, nil) +} + +func required(source string, value string) []shared.FieldError { + return validateIf(value == "", []shared.FieldError{{Source: source, Detail: "field is required"}}) +} + +func empty(source string, value string) []shared.FieldError { + return validateIf(value != "", []shared.FieldError{{Source: source, Detail: "field must not be provided"}}) +} + +func validateDate(source string, date shared.Date) []shared.FieldError { + if date.IsMalformed { + return []shared.FieldError{{Source: source, Detail: "invalid format"}} } - if attorney.DateOfBirth.IsMalformed { - errors = append(errors, shared.FieldError{Source: fmt.Sprintf("%s/dateOfBirth", prefix), Detail: "invalid format"}) - } else if attorney.DateOfBirth.IsZero() { - errors = append(errors, shared.FieldError{Source: fmt.Sprintf("%s/dateOfBirth", prefix), Detail: "field is required"}) + if date.IsZero() { + return []shared.FieldError{{Source: source, Detail: "field is required"}} } - if !attorney.Status.IsValid() { - errors = append(errors, shared.FieldError{Source: fmt.Sprintf("%s/status", prefix), Detail: "invalid value"}) + return nil +} + +func validateTime(source string, t time.Time) []shared.FieldError { + return validateIf(t.IsZero(), []shared.FieldError{{Source: source, Detail: "field is required"}}) +} + +func validateAddress(prefix string, address shared.Address) []shared.FieldError { + return flatten( + required(fmt.Sprintf("%s/line1", prefix), address.Line1), + required(fmt.Sprintf("%s/town", prefix), address.Town), + required(fmt.Sprintf("%s/country", prefix), address.Country), + validateIf(!countryCodeRe.MatchString(address.Country), []shared.FieldError{{Source: fmt.Sprintf("%s/country", prefix), Detail: "must be a valid ISO-3166-1 country code"}}), + ) +} + +type isValid interface { + ~string + IsValid() bool +} + +func validateIsValid[V isValid](source string, v V) []shared.FieldError { + if e := required(source, string(v)); e != nil { + return e } - errors = validateAddress(attorney.Address, fmt.Sprintf("%s/address", prefix), errors) + if !v.IsValid() { + return []shared.FieldError{{Source: source, Detail: "invalid value"}} + } - return errors + return nil } -func Validate(lpa shared.LpaInit) []shared.FieldError { - errors := []shared.FieldError{} +func validateUnset(source string, v interface{ Unset() bool }) []shared.FieldError { + return validateIf(!v.Unset(), []shared.FieldError{{Source: source, Detail: "field must not be provided"}}) +} - requiredFields := map[string]string{ - "/donor/firstNames": lpa.Donor.FirstNames, - "/donor/surname": lpa.Donor.Surname, +func validateAttorneys(prefix string, attorneys []shared.Attorney) []shared.FieldError { + var errors []shared.FieldError + + if len(attorneys) == 0 { + return []shared.FieldError{{Source: prefix, Detail: "at least one attorney is required"}} } - for source, value := range requiredFields { - if value == "" { - errors = append(errors, shared.FieldError{Source: source, Detail: "field is required"}) + for i, attorney := range attorneys { + if e := validateAttorney(fmt.Sprintf("%s/%d", prefix, i), attorney); e != nil { + errors = append(errors, e...) } } - if lpa.Donor.DateOfBirth.IsMalformed { - errors = append(errors, shared.FieldError{Source: "/donor/dateOfBirth", Detail: "invalid format"}) - } else if lpa.Donor.DateOfBirth.IsZero() { - errors = append(errors, shared.FieldError{Source: "/donor/dateOfBirth", Detail: "field is required"}) - } + return errors +} - errors = validateAddress(lpa.Donor.Address, "/donor/address", errors) +func validateAttorney(prefix string, attorney shared.Attorney) []shared.FieldError { + return flatten( + required(fmt.Sprintf("%s/firstNames", prefix), attorney.FirstNames), + required(fmt.Sprintf("%s/lastName", prefix), attorney.LastName), + required(fmt.Sprintf("%s/status", prefix), string(attorney.Status)), + validateDate(fmt.Sprintf("%s/dateOfBirth", prefix), attorney.DateOfBirth), + validateAddress(fmt.Sprintf("%s/address", prefix), attorney.Address), + validateIsValid(fmt.Sprintf("%s/status", prefix), attorney.Status), + ) +} - if len(lpa.Attorneys) == 0 { - errors = append(errors, shared.FieldError{Source: "/attorneys", Detail: "at least one attorney is required"}) - } +func validateTrustCorporations(prefix string, trustCorporations []shared.TrustCorporation) []shared.FieldError { + var errors []shared.FieldError - for index, attorney := range lpa.Attorneys { - prefix := fmt.Sprintf("/attorneys/%d", index) - errors = validateAttorney(attorney, prefix, errors) + for i, trustCorporation := range trustCorporations { + if e := validateTrustCorporation(fmt.Sprintf("%s/%d", prefix, i), trustCorporation); e != nil { + errors = append(errors, e...) + } } return errors } + +func validateTrustCorporation(prefix string, trustCorporation shared.TrustCorporation) []shared.FieldError { + return flatten( + required(fmt.Sprintf("%s/name", prefix), trustCorporation.Name), + required(fmt.Sprintf("%s/companyNumber", prefix), trustCorporation.CompanyNumber), + required(fmt.Sprintf("%s/email", prefix), trustCorporation.Email), + validateAddress(fmt.Sprintf("%s/address", prefix), trustCorporation.Address), + validateIsValid(fmt.Sprintf("%s/status", prefix), trustCorporation.Status), + ) +} diff --git a/lambda/create/validate_test.go b/lambda/create/validate_test.go index 6cb8f400..2482fdc6 100644 --- a/lambda/create/validate_test.go +++ b/lambda/create/validate_test.go @@ -23,9 +23,69 @@ func newDate(date string, isMalformed bool) shared.Date { } } +func TestCountAttorneys(t *testing.T) { + actives, replacements := countAttorneys([]shared.Attorney{}, []shared.TrustCorporation{}) + assert.Equal(t, 0, actives) + assert.Equal(t, 0, replacements) + + actives, replacements = countAttorneys([]shared.Attorney{ + {Status: shared.AttorneyStatusReplacement}, + {Status: shared.AttorneyStatusActive}, + {Status: shared.AttorneyStatusReplacement}, + }, []shared.TrustCorporation{ + {Status: shared.AttorneyStatusReplacement}, + {Status: shared.AttorneyStatusActive}, + }) + assert.Equal(t, 2, actives) + assert.Equal(t, 3, replacements) +} + +func TestFlatten(t *testing.T) { + errA := shared.FieldError{Source: "a", Detail: "a"} + errB := shared.FieldError{Source: "b", Detail: "b"} + errC := shared.FieldError{Source: "c", Detail: "c"} + + assert.Nil(t, flatten()) + assert.Nil(t, flatten([]shared.FieldError{}, []shared.FieldError{})) + assert.Equal(t, []shared.FieldError{errA, errB, errC}, flatten([]shared.FieldError{errA, errB}, []shared.FieldError{errC})) + assert.Equal(t, []shared.FieldError{errA, errB, errC}, flatten([]shared.FieldError{errA}, []shared.FieldError{errB, errC})) + assert.Equal(t, []shared.FieldError{errA, errB, errC}, flatten([]shared.FieldError{errA}, []shared.FieldError{errB}, []shared.FieldError{errC})) +} + +func TestValidateIf(t *testing.T) { + errs := []shared.FieldError{{Source: "a", Detail: "a"}} + + assert.Equal(t, errs, validateIf(true, errs)) + assert.Nil(t, validateIf(false, errs)) +} + +func TestValidateIfElse(t *testing.T) { + errsA := []shared.FieldError{{Source: "a", Detail: "a"}} + errsB := []shared.FieldError{{Source: "b", Detail: "b"}} + + assert.Equal(t, errsA, validateIfElse(true, errsA, errsB)) + assert.Equal(t, errsB, validateIfElse(false, errsA, errsB)) +} + +func TestRequired(t *testing.T) { + assert.Nil(t, required("a", "a")) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "field is required"}}, required("a", "")) +} + +func TestEmpty(t *testing.T) { + assert.Nil(t, empty("a", "")) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "field must not be provided"}}, empty("a", "a")) +} + +func TestValidateDate(t *testing.T) { + assert.Nil(t, validateDate("a", shared.Date{Time: time.Now()})) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "invalid format"}}, validateDate("a", shared.Date{IsMalformed: true})) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "field is required"}}, validateDate("a", shared.Date{})) +} + func TestValidateAddressEmpty(t *testing.T) { address := shared.Address{} - errors := validateAddress(address, "/test", []shared.FieldError{}) + errors := validateAddress("/test", address) assert.Contains(t, errors, shared.FieldError{Source: "/test/line1", Detail: "field is required"}) assert.Contains(t, errors, shared.FieldError{Source: "/test/town", Detail: "field is required"}) @@ -33,7 +93,7 @@ func TestValidateAddressEmpty(t *testing.T) { } func TestValidateAddressValid(t *testing.T) { - errors := validateAddress(validAddress, "/test", []shared.FieldError{}) + errors := validateAddress("/test", validAddress) assert.Empty(t, errors) } @@ -44,32 +104,54 @@ func TestValidateAddressInvalidCountry(t *testing.T) { Town: "Homeland", Country: "United Kingdom", } - errors := validateAddress(invalidAddress, "/test", []shared.FieldError{}) + errors := validateAddress("/test", invalidAddress) assert.Contains(t, errors, shared.FieldError{Source: "/test/country", Detail: "must be a valid ISO-3166-1 country code"}) } +type testIsValid string + +func (t testIsValid) IsValid() bool { return string(t) == "ok" } + +func TestValidateIsValid(t *testing.T) { + assert.Nil(t, validateIsValid("a", testIsValid("ok"))) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "field is required"}}, validateIsValid("a", testIsValid(""))) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "invalid value"}}, validateIsValid("a", testIsValid("x"))) +} + +type testUnset bool + +func (t testUnset) Unset() bool { return bool(t) } + +func TestValidateUnset(t *testing.T) { + assert.Nil(t, validateUnset("a", testUnset(true))) + assert.Equal(t, []shared.FieldError{{Source: "a", Detail: "field must not be provided"}}, validateUnset("a", testUnset(false))) +} + func TestValidateAttorneyEmpty(t *testing.T) { attorney := shared.Attorney{} - errors := validateAttorney(attorney, "/test", []shared.FieldError{}) + errors := validateAttorney("/test", attorney) assert.Contains(t, errors, shared.FieldError{Source: "/test/firstNames", Detail: "field is required"}) - assert.Contains(t, errors, shared.FieldError{Source: "/test/surname", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/lastName", Detail: "field is required"}) assert.Contains(t, errors, shared.FieldError{Source: "/test/status", Detail: "field is required"}) assert.Contains(t, errors, shared.FieldError{Source: "/test/dateOfBirth", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/line1", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/town", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/country", Detail: "field is required"}) } func TestValidateAttorneyValid(t *testing.T) { attorney := shared.Attorney{ Person: shared.Person{ - FirstNames: "Lesia", - Surname: "Lathim", - Address: validAddress, - DateOfBirth: newDate("1928-01-18", false), + FirstNames: "Lesia", + LastName: "Lathim", + Address: validAddress, }, - Status: shared.AttorneyStatusActive, + DateOfBirth: newDate("1928-01-18", false), + Status: shared.AttorneyStatusActive, } - errors := validateAttorney(attorney, "/test", []shared.FieldError{}) + errors := validateAttorney("/test", attorney) assert.Empty(t, errors) } @@ -77,14 +159,14 @@ func TestValidateAttorneyValid(t *testing.T) { func TestValidateAttorneyMalformedDateOfBirth(t *testing.T) { attorney := shared.Attorney{ Person: shared.Person{ - FirstNames: "Lesia", - Surname: "Lathim", - Address: validAddress, - DateOfBirth: newDate("bad date", true), + FirstNames: "Lesia", + LastName: "Lathim", + Address: validAddress, }, - Status: shared.AttorneyStatusActive, + DateOfBirth: newDate("bad date", true), + Status: shared.AttorneyStatusActive, } - errors := validateAttorney(attorney, "/test", []shared.FieldError{}) + errors := validateAttorney("/test", attorney) assert.Contains(t, errors, shared.FieldError{Source: "/test/dateOfBirth", Detail: "invalid format"}) } @@ -92,49 +174,265 @@ func TestValidateAttorneyMalformedDateOfBirth(t *testing.T) { func TestValidateAttorneyInvalidStatus(t *testing.T) { attorney := shared.Attorney{ Person: shared.Person{ - FirstNames: "Lesia", - Surname: "Lathim", - Address: validAddress, - DateOfBirth: newDate("1928-01-18", false), + FirstNames: "Lesia", + LastName: "Lathim", + Address: validAddress, }, + DateOfBirth: newDate("1928-01-18", false), + Status: "bad status", + } + errors := validateAttorney("/test", attorney) + + assert.Contains(t, errors, shared.FieldError{Source: "/test/status", Detail: "invalid value"}) +} + +func TestValidateTrustCorporationEmpty(t *testing.T) { + trustCorporation := shared.TrustCorporation{} + errors := validateTrustCorporation("/test", trustCorporation) + + assert.Contains(t, errors, shared.FieldError{Source: "/test/name", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/companyNumber", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/email", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/status", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/line1", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/town", Detail: "field is required"}) + assert.Contains(t, errors, shared.FieldError{Source: "/test/address/country", Detail: "field is required"}) +} + +func TestValidateTrustCorporationValid(t *testing.T) { + trustCorporation := shared.TrustCorporation{ + Name: "corp", + CompanyNumber: "5", + Email: "corp@example.com", + Address: validAddress, + Status: shared.AttorneyStatusActive, + } + errors := validateTrustCorporation("/test", trustCorporation) + + assert.Empty(t, errors) +} + +func TestValidateTrustCorporationInvalidStatus(t *testing.T) { + trustCorporation := shared.TrustCorporation{ Status: "bad status", } - errors := validateAttorney(attorney, "/test", []shared.FieldError{}) + errors := validateTrustCorporation("/test", trustCorporation) assert.Contains(t, errors, shared.FieldError{Source: "/test/status", Detail: "invalid value"}) } -func TestValidateLpaEmpty(t *testing.T) { - lpa := shared.LpaInit{} - errors := Validate(lpa) +func TestValidateLpaInvalid(t *testing.T) { + testcases := map[string]struct { + lpa shared.LpaInit + contains []shared.FieldError + }{ + "empty": { + contains: []shared.FieldError{ + {Source: "/lpaType", Detail: "field is required"}, + {Source: "/donor/firstNames", Detail: "field is required"}, + {Source: "/donor/lastName", Detail: "field is required"}, + {Source: "/donor/dateOfBirth", Detail: "field is required"}, + {Source: "/attorneys", Detail: "at least one attorney is required"}, + }, + }, + "online certificate provider missing email": { + lpa: shared.LpaInit{ + CertificateProvider: shared.CertificateProvider{ + Channel: shared.ChannelOnline, + }, + }, + contains: []shared.FieldError{ + {Source: "/certificateProvider/email", Detail: "field is required"}, + }, + }, + "paper certificate provider with email": { + lpa: shared.LpaInit{ + CertificateProvider: shared.CertificateProvider{ + Channel: shared.ChannelPaper, + Email: "something", + }, + }, + contains: []shared.FieldError{ + {Source: "/certificateProvider/email", Detail: "field must not be provided"}, + }, + }, + "single attorney with decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusActive}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointly, + }, + contains: []shared.FieldError{ + {Source: "/howAttorneysMakeDecisions", Detail: "field must not be provided"}, + }, + }, + "multiple attorneys without decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusActive}, {Status: shared.AttorneyStatusActive}}, + }, + contains: []shared.FieldError{ + {Source: "/howAttorneysMakeDecisions", Detail: "field is required"}, + }, + }, + "multiple attorneys mixed without details": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusActive}, {Status: shared.AttorneyStatusActive}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyForSomeSeverallyForOthers, + }, + contains: []shared.FieldError{ + {Source: "/howAttorneysMakeDecisionsDetails", Detail: "field is required"}, + }, + }, + "multiple attorneys not mixed with details": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusActive}, {Status: shared.AttorneyStatusActive}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointly, + HowAttorneysMakeDecisionsDetails: "something", + }, + contains: []shared.FieldError{ + {Source: "/howAttorneysMakeDecisionsDetails", Detail: "field must not be provided"}, + }, + }, + "single replacement attorney with decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}}, + HowReplacementAttorneysMakeDecisions: shared.HowMakeDecisionsJointly, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisions", Detail: "field must not be provided"}, + }, + }, + "multiple replacement attorneys without decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisions", Detail: "field is required"}, + }, + }, + "attorneys jointly and severally multiple replacement attorneys without step in": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyAndSeverally, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysStepIn", Detail: "field is required"}, + }, + }, + "attorneys jointly and severally multiple replacement attorneys with step in another way no details": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyAndSeverally, + HowReplacementAttorneysStepIn: shared.HowStepInAnotherWay, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysStepInDetails", Detail: "field is required"}, + }, + }, + "attorneys jointly and severally multiple replacement attorneys with step in all no decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyAndSeverally, + HowReplacementAttorneysStepIn: shared.HowStepInAllCanNoLongerAct, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisions", Detail: "field is required"}, + }, + }, + "attorneys jointly and severally multiple replacement attorneys with step in one with decisions": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyAndSeverally, + HowReplacementAttorneysStepIn: shared.HowStepInOneCanNoLongerAct, + HowReplacementAttorneysMakeDecisions: shared.HowMakeDecisionsJointly, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisions", Detail: "field must not be provided"}, + }, + }, + "multiple replacement attorneys mixed without details": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowReplacementAttorneysMakeDecisions: shared.HowMakeDecisionsJointlyForSomeSeverallyForOthers, + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisionsDetails", Detail: "field is required"}, + }, + }, + "multiple replacement attorneys not mixed with details": { + lpa: shared.LpaInit{ + Attorneys: []shared.Attorney{{Status: shared.AttorneyStatusReplacement}, {Status: shared.AttorneyStatusReplacement}}, + HowReplacementAttorneysMakeDecisions: shared.HowMakeDecisionsJointly, + HowReplacementAttorneysMakeDecisionsDetails: "something", + }, + contains: []shared.FieldError{ + {Source: "/howReplacementAttorneysMakeDecisionsDetails", Detail: "field must not be provided"}, + }, + }, + "health welfare with when can be used": { + lpa: shared.LpaInit{ + LpaType: shared.LpaTypePersonalWelfare, + WhenTheLpaCanBeUsed: shared.CanUseWhenHasCapacity, + }, + contains: []shared.FieldError{ + {Source: "/whenTheLpaCanBeUsed", Detail: "field must not be provided"}, + {Source: "/lifeSustainingTreatmentOption", Detail: "field is required"}, + }, + }, + "property finance with life sustaining treatment": { + lpa: shared.LpaInit{ + LpaType: shared.LpaTypePropertyAndAffairs, + LifeSustainingTreatmentOption: shared.LifeSustainingTreatmentOptionA, + }, + contains: []shared.FieldError{ + {Source: "/whenTheLpaCanBeUsed", Detail: "field is required"}, + {Source: "/lifeSustainingTreatmentOption", Detail: "field must not be provided"}, + }, + }, + } - assert.Contains(t, errors, shared.FieldError{Source: "/donor/firstNames", Detail: "field is required"}) - assert.Contains(t, errors, shared.FieldError{Source: "/donor/surname", Detail: "field is required"}) - assert.Contains(t, errors, shared.FieldError{Source: "/donor/dateOfBirth", Detail: "field is required"}) - assert.Contains(t, errors, shared.FieldError{Source: "/attorneys", Detail: "at least one attorney is required"}) + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + errors := Validate(tc.lpa) + for _, e := range tc.contains { + assert.Contains(t, errors, e) + } + }) + } } func TestValidateLpaValid(t *testing.T) { lpa := shared.LpaInit{ + LpaType: shared.LpaTypePersonalWelfare, Donor: shared.Donor{ Person: shared.Person{ - FirstNames: "Otto", - Surname: "Boudreau", - DateOfBirth: newDate("1956-08-08", false), - Address: validAddress, + FirstNames: "Otto", + LastName: "Boudreau", + Address: validAddress, }, + DateOfBirth: newDate("1956-08-08", false), }, Attorneys: []shared.Attorney{ { Person: shared.Person{ - FirstNames: "Sharonda", - Surname: "Graciani", - DateOfBirth: newDate("1977-10-30", false), - Address: validAddress, + FirstNames: "Sharonda", + LastName: "Graciani", + Address: validAddress, }, - Status: shared.AttorneyStatusActive, + DateOfBirth: newDate("1977-10-30", false), + Status: shared.AttorneyStatusActive, + }, + }, + CertificateProvider: shared.CertificateProvider{ + Person: shared.Person{ + FirstNames: "Some", + LastName: "Person", + Address: validAddress, }, + Email: "some@example.com", + Channel: shared.ChannelOnline, }, + LifeSustainingTreatmentOption: shared.LifeSustainingTreatmentOptionA, + SignedAt: time.Now(), } errors := Validate(lpa) diff --git a/mock-apigw/main.go b/mock-apigw/main.go index b9faa20a..5c5865ee 100644 --- a/mock-apigw/main.go +++ b/mock-apigw/main.go @@ -105,9 +105,10 @@ func handlePactState(r *http.Request) error { if match := re.FindStringSubmatch(state.State); len(match) > 0 { url := fmt.Sprintf("http://localhost:8080/lpas/%s", match[1]) body := `{ + "lpaType": "personal-welfare", "donor": { "firstNames": "Homer", - "surname": "Zoller", + "lastName": "Zoller", "dateOfBirth": "1960-04-06", "address": { "line1": "79 Bury Rd", @@ -119,7 +120,7 @@ func handlePactState(r *http.Request) error { "attorneys": [ { "firstNames": "Jake", - "surname": "Vallar", + "lastName": "Vallar", "dateOfBirth": "2001-01-17", "status": "active", "address": { @@ -128,7 +129,20 @@ func handlePactState(r *http.Request) error { "country": "AU" } } - ] + ], + "certificateProvider": { + "firstNames": "Some", + "lastName": "Provider", + "email": "some@example.com", + "channel": "online", + "address": { + "line1": "71 South Western Terrace", + "town": "Milton", + "country": "AU" + } + }, + "lifeSustainingTreatmentOption": "option-a", + "signedAt": "2000-01-02T12:13:14Z" }` req, err := http.NewRequest("PUT", url, strings.NewReader(body)) @@ -165,5 +179,5 @@ func main() { log.Fatal(err) } - fmt.Printf("running on port 8080\n") + log.Println("running on port 8080") }