From e6fe3f39e23623c1b826561218324d784f1f7601 Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell Date: Mon, 19 Aug 2024 15:28:09 +0100 Subject: [PATCH] Add pages for voucher identity check --- .../e2e/voucher/confirm-your-identity.cy.js | 39 ++ internal/app/app.go | 2 + internal/dashboard/store.go | 3 +- internal/lpastore/lpadata/voucher.go | 4 + internal/lpastore/lpadata/voucher_test.go | 11 + internal/notify/email.go | 12 + internal/page/fixtures/voucher.go | 13 + internal/templatefn/fn.go | 2 - internal/templatefn/paths.go | 10 +- internal/voucher/path.go | 27 +- internal/voucher/path_test.go | 12 + internal/voucher/voucherdata/provided.go | 14 +- .../voucher/voucherpage/confirm_your_name.go | 5 + internal/voucher/voucherpage/guidance.go | 12 +- .../voucherpage/identity_with_one_login.go | 40 ++ .../identity_with_one_login_callback.go | 71 +++ .../identity_with_one_login_callback_test.go | 414 ++++++++++++++++++ .../identity_with_one_login_test.go | 73 +++ .../voucherpage/mock_NotifyClient_test.go | 134 ++++++ .../voucherpage/mock_OneLoginClient_test.go | 81 +++- internal/voucher/voucherpage/register.go | 18 + internal/voucher/voucherpage/register_test.go | 2 +- internal/voucher/voucherpage/task_list.go | 15 +- .../voucher/voucherpage/task_list_test.go | 9 +- lang/cy.json | 3 +- lang/en.json | 3 +- .../voucher/confirm_your_identity.gohtml | 2 +- web/template/voucher/confirm_your_name.gohtml | 6 +- .../voucher/one_login_identity_details.gohtml | 27 ++ web/template/voucher/task_list.gohtml | 4 +- .../voucher/unable_to_confirm_identity.gohtml | 17 + 31 files changed, 1031 insertions(+), 54 deletions(-) create mode 100644 cypress/e2e/voucher/confirm-your-identity.cy.js create mode 100644 internal/lpastore/lpadata/voucher_test.go create mode 100644 internal/voucher/voucherpage/identity_with_one_login.go create mode 100644 internal/voucher/voucherpage/identity_with_one_login_callback.go create mode 100644 internal/voucher/voucherpage/identity_with_one_login_callback_test.go create mode 100644 internal/voucher/voucherpage/identity_with_one_login_test.go create mode 100644 internal/voucher/voucherpage/mock_NotifyClient_test.go create mode 100644 web/template/voucher/one_login_identity_details.gohtml create mode 100644 web/template/voucher/unable_to_confirm_identity.gohtml diff --git a/cypress/e2e/voucher/confirm-your-identity.cy.js b/cypress/e2e/voucher/confirm-your-identity.cy.js new file mode 100644 index 0000000000..863cca8ad7 --- /dev/null +++ b/cypress/e2e/voucher/confirm-your-identity.cy.js @@ -0,0 +1,39 @@ +describe('Confirm your identity', () => { + beforeEach(() => { + cy.visit('/fixtures/voucher?redirect=/confirm-your-identity&progress=verifyDonorDetails'); + }); + + it('can be confirmed', () => { + cy.checkA11yApp(); + cy.contains('a', 'Continue').click(); + cy.contains('label', 'Vivian Vaughn').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/one-login-identity-details'); + cy.checkA11yApp(); + cy.contains('a', 'Continue').click(); + + cy.get('.govuk-task-list li:nth-child(3)').should('contain', 'Completed'); + cy.contains('a', 'Confirm your identity').click(); + + cy.url().should('contain', '/one-login-identity-details'); + cy.contains('a', 'Continue').click(); + + cy.contains('a', 'Confirm your name').click(); + cy.contains('a', 'Change').should('not.exist'); + + cy.contains('a', 'Manage your LPAs').click(); + cy.contains('I’m vouching for someone'); + }); + + it('can fail', () => { + cy.contains('a', 'Continue').click(); + cy.contains('label', 'Sam Smith').click(); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/unable-to-confirm-identity'); + + cy.contains('a', 'Manage your LPAs').click(); + cy.contains('I’m vouching for someone').should('not.exist');; + }); +}); diff --git a/internal/app/app.go b/internal/app/app.go index 632f270536..e886053527 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -165,6 +165,8 @@ func App( dashboardStore, errorHandler, lpaStoreResolvingService, + notifyClient, + appPublicURL, ) supporterpage.Register( diff --git a/internal/dashboard/store.go b/internal/dashboard/store.go index 644e5a1228..cb42b491c7 100644 --- a/internal/dashboard/store.go +++ b/internal/dashboard/store.go @@ -219,7 +219,8 @@ func (s *Store) GetAll(ctx context.Context) (results dashboarddata.Results, err lpaID := voucherProvidedDetails.LpaID - if voucherProvidedDetails.Tasks.SignTheDeclaration.IsCompleted() { + if voucherProvidedDetails.Tasks.SignTheDeclaration.IsCompleted() || + (voucherProvidedDetails.Tasks.ConfirmYourIdentity.IsCompleted() && !voucherProvidedDetails.IdentityConfirmed()) { delete(voucherMap, lpaID) } diff --git a/internal/lpastore/lpadata/voucher.go b/internal/lpastore/lpadata/voucher.go index 0b44b5451b..d4136ffaac 100644 --- a/internal/lpastore/lpadata/voucher.go +++ b/internal/lpastore/lpadata/voucher.go @@ -8,3 +8,7 @@ type Voucher struct { LastName string Email string } + +func (v Voucher) FullName() string { + return v.FirstNames + " " + v.LastName +} diff --git a/internal/lpastore/lpadata/voucher_test.go b/internal/lpastore/lpadata/voucher_test.go new file mode 100644 index 0000000000..a6b75ea6da --- /dev/null +++ b/internal/lpastore/lpadata/voucher_test.go @@ -0,0 +1,11 @@ +package lpadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVoucherFullName(t *testing.T) { + assert.Equal(t, "John Smith", Voucher{FirstNames: "John", LastName: "Smith"}.FullName()) +} diff --git a/internal/notify/email.go b/internal/notify/email.go index 23afe8356c..450c2e25ea 100644 --- a/internal/notify/email.go +++ b/internal/notify/email.go @@ -209,3 +209,15 @@ type AttorneyOptedOutEmail struct { func (e AttorneyOptedOutEmail) emailID(isProduction bool) string { return "TODO" } + +type VoucherFailedIdentityCheckEmail struct { + Greeting string + DonorFullName string + VoucherFullName string + LpaType string + DonorStartPageURL string +} + +func (e VoucherFailedIdentityCheckEmail) emailID(isProduction bool) string { + return "TODO" +} diff --git a/internal/page/fixtures/voucher.go b/internal/page/fixtures/voucher.go index 65df489d27..f3dd27a66b 100644 --- a/internal/page/fixtures/voucher.go +++ b/internal/page/fixtures/voucher.go @@ -12,6 +12,7 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" "github.com/ministryofjustice/opg-modernising-lpa/internal/donor" "github.com/ministryofjustice/opg-modernising-lpa/internal/donor/donordata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/random" @@ -32,6 +33,7 @@ func Voucher( progressValues := []string{ "confirmYourName", "verifyDonorDetails", + "confirmYourIdentity", } return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request) error { @@ -125,6 +127,8 @@ func Voucher( } if progress >= slices.Index(progressValues, "confirmYourName") { + voucherDetails.FirstNames = donorDetails.Voucher.FirstNames + voucherDetails.LastName = donorDetails.Voucher.LastName voucherDetails.Tasks.ConfirmYourName = task.StateCompleted } @@ -132,6 +136,15 @@ func Voucher( voucherDetails.Tasks.VerifyDonorDetails = task.StateCompleted } + if progress >= slices.Index(progressValues, "confirmYourIdentity") { + voucherDetails.IdentityUserData = identity.UserData{ + Status: identity.StatusConfirmed, + FirstNames: voucherDetails.FirstNames, + LastName: voucherDetails.LastName, + } + voucherDetails.Tasks.ConfirmYourIdentity = task.StateCompleted + } + if err := voucherStore.Put(voucherCtx, voucherDetails); err != nil { return err } diff --git a/internal/templatefn/fn.go b/internal/templatefn/fn.go index 985088454e..c79e909ba0 100644 --- a/internal/templatefn/fn.go +++ b/internal/templatefn/fn.go @@ -18,7 +18,6 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore" "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" - "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" ) // Globals contains values that are used in templates and do not change as the @@ -88,7 +87,6 @@ func All(globals *Globals) map[string]any { "checkboxEq": checkboxEq, "lpaDecisions": lpaDecisions, "summaryRow": summaryRow, - "voucherCanGoTo": voucher.CanGoTo, } } diff --git a/internal/templatefn/paths.go b/internal/templatefn/paths.go index e0b37571b9..386daee1e1 100644 --- a/internal/templatefn/paths.go +++ b/internal/templatefn/paths.go @@ -97,8 +97,9 @@ type voucherPaths struct { Login page.Path Start page.Path - TaskList voucher.Path - YourName voucher.Path + TaskList voucher.Path + YourName voucher.Path + IdentityWithOneLogin voucher.Path } type appPaths struct { @@ -325,8 +326,9 @@ var paths = appPaths{ Login: page.PathVoucherLogin, Start: page.PathVoucherStart, - TaskList: voucher.PathTaskList, - YourName: voucher.PathYourName, + TaskList: voucher.PathTaskList, + YourName: voucher.PathYourName, + IdentityWithOneLogin: voucher.PathIdentityWithOneLogin, }, HealthCheck: healthCheckPaths{ diff --git a/internal/voucher/path.go b/internal/voucher/path.go index df6031a8bd..09a68c0121 100644 --- a/internal/voucher/path.go +++ b/internal/voucher/path.go @@ -9,14 +9,18 @@ import ( ) const ( - PathTaskList = Path("/task-list") - PathConfirmAllowedToVouch = Path("/confirm-allowed-to-vouch") - PathConfirmYourName = Path("/confirm-your-name") - PathYourName = Path("/your-name") - PathVerifyDonorDetails = Path("/verify-donor-details") - PathDonorDetailsDoNotMatch = Path("/donor-details-do-not-match") - PathConfirmYourIdentity = Path("/confirm-your-identity") - PathSignTheDeclaration = Path("/sign-the-declaration") + PathTaskList = Path("/task-list") + PathConfirmAllowedToVouch = Path("/confirm-allowed-to-vouch") + PathConfirmYourName = Path("/confirm-your-name") + PathYourName = Path("/your-name") + PathVerifyDonorDetails = Path("/verify-donor-details") + PathDonorDetailsDoNotMatch = Path("/donor-details-do-not-match") + PathConfirmYourIdentity = Path("/confirm-your-identity") + PathSignTheDeclaration = Path("/sign-the-declaration") + PathIdentityWithOneLogin = Path("/identity-with-one-login") + PathIdentityWithOneLoginCallback = Path("/identity-with-one-login-callback") + PathOneLoginIdentityDetails = Path("/one-login-identity-details") + PathUnableToConfirmIdentity = Path("/unable-to-confirm-identity") ) type Path string @@ -39,8 +43,11 @@ func (p Path) Redirect(w http.ResponseWriter, r *http.Request, appData appcontex return nil } -func (p Path) canVisit(provided *voucherdata.Provided) bool { +func (p Path) CanGoTo(provided *voucherdata.Provided) bool { switch p { + case PathYourName: + return !provided.Tasks.ConfirmYourIdentity.IsCompleted() + case PathVerifyDonorDetails: return provided.Tasks.ConfirmYourName.IsCompleted() && !provided.Tasks.VerifyDonorDetails.IsCompleted() @@ -67,7 +74,7 @@ func CanGoTo(provided *voucherdata.Provided, url string) bool { if strings.HasPrefix(path, "/voucher/") { _, voucherPath, _ := strings.Cut(strings.TrimPrefix(path, "/voucher/"), "/") - return Path("/" + voucherPath).canVisit(provided) + return Path("/" + voucherPath).CanGoTo(provided) } return true diff --git a/internal/voucher/path_test.go b/internal/voucher/path_test.go index 89015c1965..67ba6129df 100644 --- a/internal/voucher/path_test.go +++ b/internal/voucher/path_test.go @@ -67,6 +67,18 @@ func TestCanGoTo(t *testing.T) { url: PathTaskList.Format("123"), expected: true, }, + "your name": { + provided: &voucherdata.Provided{}, + url: PathYourName.Format("123"), + expected: true, + }, + "your name when identity completed": { + provided: &voucherdata.Provided{ + Tasks: voucherdata.Tasks{ConfirmYourIdentity: task.StateCompleted}, + }, + url: PathYourName.Format("123"), + expected: false, + }, "verify donor details": { provided: &voucherdata.Provided{}, url: PathVerifyDonorDetails.Format("123"), diff --git a/internal/voucher/voucherdata/provided.go b/internal/voucher/voucherdata/provided.go index bfa0cbaa49..05629cb451 100644 --- a/internal/voucher/voucherdata/provided.go +++ b/internal/voucher/voucherdata/provided.go @@ -5,6 +5,7 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" "github.com/ministryofjustice/opg-modernising-lpa/internal/form" + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/task" ) @@ -20,15 +21,20 @@ type Provided struct { Tasks Tasks // Email is the email address of the voucher Email string - // FirstNames is the first names provided by the voucher. If set it overrides - // that provided by the donor. + // FirstNames is the first names confirmed by the voucher. FirstNames string - // LastName is a last name provided by the voucher. If set it overrides that - // provided by the donor. + // LastName is the last name confirmed by the voucher. LastName string // DonorDetailsMatch records whether the voucher confirms that the details // presented to them match the donor they expected to vouch for. DonorDetailsMatch form.YesNo + // IdentityUserData records the results of the identity check taken by the + // voucher. + IdentityUserData identity.UserData +} + +func (p Provided) IdentityConfirmed() bool { + return p.IdentityUserData.Status.IsConfirmed() && p.IdentityUserData.MatchName(p.FirstNames, p.LastName) } type Tasks struct { diff --git a/internal/voucher/voucherpage/confirm_your_name.go b/internal/voucher/voucherpage/confirm_your_name.go index 2163f51fce..ddc191bf67 100644 --- a/internal/voucher/voucherpage/confirm_your_name.go +++ b/internal/voucher/voucherpage/confirm_your_name.go @@ -16,6 +16,7 @@ type confirmYourNameData struct { App appcontext.Data Errors validation.List Lpa *lpadata.Lpa + Tasks voucherdata.Tasks FirstNames string LastName string } @@ -41,6 +42,9 @@ func ConfirmYourName(tmpl template.Template, lpaStoreResolvingService LpaStoreRe redirect := voucher.PathTaskList state := task.StateCompleted + provided.FirstNames = firstNames + provided.LastName = lastName + if lastName == lpa.Donor.LastName { redirect = voucher.PathConfirmAllowedToVouch state = task.StateInProgress @@ -57,6 +61,7 @@ func ConfirmYourName(tmpl template.Template, lpaStoreResolvingService LpaStoreRe return tmpl(w, &confirmYourNameData{ App: appData, Lpa: lpa, + Tasks: provided.Tasks, FirstNames: firstNames, LastName: lastName, }) diff --git a/internal/voucher/voucherpage/guidance.go b/internal/voucher/voucherpage/guidance.go index f1cd3f1b99..7b3385aa7b 100644 --- a/internal/voucher/voucherpage/guidance.go +++ b/internal/voucher/voucherpage/guidance.go @@ -11,15 +11,17 @@ import ( ) type guidanceData struct { - App appcontext.Data - Errors validation.List - Lpa *lpadata.Lpa + App appcontext.Data + Errors validation.List + Voucher *voucherdata.Provided + Lpa *lpadata.Lpa } func Guidance(tmpl template.Template, lpaStoreResolvingService LpaStoreResolvingService) Handler { - return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request, _ *voucherdata.Provided) error { + return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request, provided *voucherdata.Provided) error { data := &guidanceData{ - App: appData, + App: appData, + Voucher: provided, } if lpaStoreResolvingService != nil { diff --git a/internal/voucher/voucherpage/identity_with_one_login.go b/internal/voucher/voucherpage/identity_with_one_login.go new file mode 100644 index 0000000000..8f792b3917 --- /dev/null +++ b/internal/voucher/voucherpage/identity_with_one_login.go @@ -0,0 +1,40 @@ +package voucherpage + +import ( + "net/http" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" + "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher/voucherdata" +) + +func IdentityWithOneLogin(oneLoginClient OneLoginClient, sessionStore SessionStore, randomString func(int) string) Handler { + return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request, _ *voucherdata.Provided) error { + locale := "" + if appData.Lang == localize.Cy { + locale = "cy" + } + + state := randomString(12) + nonce := randomString(12) + + authCodeURL, err := oneLoginClient.AuthCodeURL(state, nonce, locale, true) + if err != nil { + return err + } + + if err := sessionStore.SetOneLogin(r, w, &sesh.OneLoginSession{ + State: state, + Nonce: nonce, + Locale: locale, + Redirect: voucher.PathIdentityWithOneLoginCallback.Format(appData.LpaID), + }); err != nil { + return err + } + + http.Redirect(w, r, authCodeURL, http.StatusFound) + return nil + } +} diff --git a/internal/voucher/voucherpage/identity_with_one_login_callback.go b/internal/voucher/voucherpage/identity_with_one_login_callback.go new file mode 100644 index 0000000000..143903309f --- /dev/null +++ b/internal/voucher/voucherpage/identity_with_one_login_callback.go @@ -0,0 +1,71 @@ +package voucherpage + +import ( + "errors" + "net/http" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/task" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher/voucherdata" +) + +func IdentityWithOneLoginCallback(oneLoginClient OneLoginClient, sessionStore SessionStore, voucherStore VoucherStore, lpaStoreResolvingService LpaStoreResolvingService, notifyClient NotifyClient, appPublicURL string) Handler { + return func(appData appcontext.Data, w http.ResponseWriter, r *http.Request, provided *voucherdata.Provided) error { + lpa, err := lpaStoreResolvingService.Get(r.Context()) + if err != nil { + return err + } + + if r.FormValue("error") == "access_denied" { + // TODO: check with team on how we want to communicate this on the page + return errors.New("access denied") + } + + oneLoginSession, err := sessionStore.OneLogin(r) + if err != nil { + return err + } + + _, accessToken, err := oneLoginClient.Exchange(r.Context(), r.FormValue("code"), oneLoginSession.Nonce) + if err != nil { + return err + } + + userInfo, err := oneLoginClient.UserInfo(r.Context(), accessToken) + if err != nil { + return err + } + + userData, err := oneLoginClient.ParseIdentityClaim(r.Context(), userInfo) + if err != nil { + return err + } + + provided.IdentityUserData = userData + provided.Tasks.ConfirmYourIdentity = task.StateCompleted + if err := voucherStore.Put(r.Context(), provided); err != nil { + return err + } + + if !provided.IdentityConfirmed() { + if !lpa.SignedAt.IsZero() { + if err = notifyClient.SendActorEmail(r.Context(), lpa.CorrespondentEmail(), lpa.LpaUID, notify.VoucherFailedIdentityCheckEmail{ + Greeting: notifyClient.EmailGreeting(lpa), + DonorFullName: lpa.Donor.FullName(), + VoucherFullName: lpa.Voucher.FullName(), + LpaType: appData.Localizer.T(lpa.Type.String()), + DonorStartPageURL: appPublicURL + page.PathStart.Format(), + }); err != nil { + return err + } + } + + return voucher.PathUnableToConfirmIdentity.Redirect(w, r, appData, appData.LpaID) + } + + return voucher.PathOneLoginIdentityDetails.Redirect(w, r, appData, appData.LpaID) + } +} diff --git a/internal/voucher/voucherpage/identity_with_one_login_callback_test.go b/internal/voucher/voucherpage/identity_with_one_login_callback_test.go new file mode 100644 index 0000000000..5b5556ff53 --- /dev/null +++ b/internal/voucher/voucherpage/identity_with_one_login_callback_test.go @@ -0,0 +1,414 @@ +package voucherpage + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" + "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/ministryofjustice/opg-modernising-lpa/internal/task" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher/voucherdata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetIdentityWithOneLoginCallback(t *testing.T) { + now := time.Now() + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/?code=a-code", nil) + + userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} + userData := identity.UserData{Status: identity.StatusConfirmed, FirstNames: "John", LastName: "Doe", RetrievedAt: now} + + updatedVoucher := &voucherdata.Provided{ + LpaID: "lpa-id", + FirstNames: "John", + LastName: "Doe", + IdentityUserData: userData, + Tasks: voucherdata.Tasks{ConfirmYourIdentity: task.StateCompleted}, + } + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(r.Context(), updatedVoucher). + Return(nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{LpaUID: "lpa-uid", Voucher: lpadata.Voucher{FirstNames: "John", LastName: "Doe"}}, nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(r). + Return(&sesh.OneLoginSession{State: "a-state", Nonce: "a-nonce", Redirect: "/redirect"}, nil) + + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(r.Context(), "a-code", "a-nonce"). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(r.Context(), "a-jwt"). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(r.Context(), userInfo). + Return(userData, nil) + + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, voucherStore, lpaStoreResolvingService, nil, "www.example.com")(testAppData, w, r, &voucherdata.Provided{ + LpaID: "lpa-id", + FirstNames: "John", + LastName: "Doe", + }) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, voucher.PathOneLoginIdentityDetails.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestGetIdentityWithOneLoginCallbackWhenFailedIdentityCheck(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/?code=a-code", nil) + + userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} + userData := identity.UserData{Status: identity.StatusFailed} + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(r.Context(), mock.Anything). + Return(nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{ + LpaUID: "lpa-uid", + Voucher: lpadata.Voucher{FirstNames: "a", LastName: "b"}, + Donor: lpadata.Donor{Email: "a@example.com", FirstNames: "c", LastName: "d"}, + Type: lpadata.LpaTypePersonalWelfare, + SignedAt: time.Now(), + }, nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(r). + Return(&sesh.OneLoginSession{State: "a-state", Nonce: "a-nonce", Redirect: "/redirect"}, nil) + + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(r.Context(), "a-code", "a-nonce"). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(r.Context(), "a-jwt"). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(r.Context(), userInfo). + Return(userData, nil) + + localizer := newMockLocalizer(t) + localizer.EXPECT(). + T("personal-welfare"). + Return("translated LPA type") + + testAppData.Localizer = localizer + + notifyClient := newMockNotifyClient(t) + notifyClient.EXPECT(). + EmailGreeting(mock.Anything). + Return("Dear donor") + notifyClient.EXPECT(). + SendActorEmail(r.Context(), "a@example.com", "lpa-uid", notify.VoucherFailedIdentityCheckEmail{ + Greeting: "Dear donor", + DonorFullName: "c d", + VoucherFullName: "a b", + LpaType: "translated LPA type", + DonorStartPageURL: "www.example.com" + page.PathStart.Format(), + }). + Return(nil) + + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, voucherStore, lpaStoreResolvingService, notifyClient, "www.example.com")(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, voucher.PathUnableToConfirmIdentity.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestGetIdentityWithOneLoginCallbackWhenSendingEmailError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/?code=a-code", nil) + + userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} + userData := identity.UserData{Status: identity.StatusFailed} + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(mock.Anything, mock.Anything). + Return(nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(mock.Anything). + Return(&lpadata.Lpa{ + LpaUID: "lpa-uid", + Voucher: lpadata.Voucher{FirstNames: "a", LastName: "b"}, + Donor: lpadata.Donor{Email: "a@example.com", FirstNames: "c", LastName: "d"}, + Type: lpadata.LpaTypePersonalWelfare, + SignedAt: time.Now(), + }, nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(mock.Anything). + Return(&sesh.OneLoginSession{State: "a-state", Nonce: "a-nonce", Redirect: "/redirect"}, nil) + + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(mock.Anything, mock.Anything). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(mock.Anything, mock.Anything). + Return(userData, nil) + + localizer := newMockLocalizer(t) + localizer.EXPECT(). + T(mock.Anything). + Return("translated LPA type") + + testAppData.Localizer = localizer + + notifyClient := newMockNotifyClient(t) + notifyClient.EXPECT(). + EmailGreeting(mock.Anything). + Return("") + notifyClient.EXPECT(). + SendActorEmail(mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(expectedError) + + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, voucherStore, lpaStoreResolvingService, notifyClient, "www.example.com")(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetIdentityWithOneLoginCallbackWhenIdentityNotConfirmed(t *testing.T) { + userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} + + sessionRetrieved := func(t *testing.T) *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(mock.Anything). + Return(&sesh.OneLoginSession{State: "a-state", Nonce: "a-nonce", Redirect: "/redirect"}, nil) + return sessionStore + } + + sessionIgnored := func(t *testing.T) *mockSessionStore { + return nil + } + voucherIgnored := func(t *testing.T) *mockVoucherStore { + return nil + } + + testCases := map[string]struct { + oneLoginClient func(t *testing.T) *mockOneLoginClient + sessionStore func(*testing.T) *mockSessionStore + voucherStore func(t *testing.T) *mockVoucherStore + url string + error error + expectedRedirectURL string + expectedStatus int + }{ + "not ok": { + url: "/?code=a-code", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(mock.Anything, mock.Anything). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(mock.Anything, mock.Anything). + Return(identity.UserData{}, nil) + return oneLoginClient + }, + sessionStore: sessionRetrieved, + voucherStore: func(t *testing.T) *mockVoucherStore { + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(context.Background(), &voucherdata.Provided{ + LpaID: "lpa-id", + Tasks: voucherdata.Tasks{ConfirmYourIdentity: task.StateCompleted}, + }). + Return(nil) + + return voucherStore + }, + expectedRedirectURL: voucher.PathUnableToConfirmIdentity.Format("lpa-id"), + expectedStatus: http.StatusFound, + }, + "errored on parse": { + url: "/?code=a-code", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(mock.Anything, mock.Anything). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(mock.Anything, mock.Anything). + Return(identity.UserData{Status: identity.StatusConfirmed}, expectedError) + return oneLoginClient + }, + sessionStore: sessionRetrieved, + error: expectedError, + voucherStore: voucherIgnored, + expectedStatus: http.StatusOK, + }, + "errored on userinfo": { + url: "/?code=a-code", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(mock.Anything, mock.Anything). + Return(onelogin.UserInfo{}, expectedError) + return oneLoginClient + }, + sessionStore: sessionRetrieved, + error: expectedError, + voucherStore: voucherIgnored, + expectedStatus: http.StatusOK, + }, + "errored on exchange": { + url: "/?code=a-code", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("", "", expectedError) + return oneLoginClient + }, + sessionStore: sessionRetrieved, + error: expectedError, + voucherStore: voucherIgnored, + expectedStatus: http.StatusOK, + }, + "errored on session store": { + url: "/?code=a-code", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + return nil + }, + sessionStore: func(t *testing.T) *mockSessionStore { + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(mock.Anything). + Return(nil, expectedError) + return sessionStore + }, + error: expectedError, + voucherStore: voucherIgnored, + expectedStatus: http.StatusOK, + }, + "provider access denied": { + url: "/?error=access_denied", + oneLoginClient: func(t *testing.T) *mockOneLoginClient { + return newMockOneLoginClient(t) + }, + sessionStore: sessionIgnored, + error: errors.New("access denied"), + voucherStore: voucherIgnored, + expectedStatus: http.StatusOK, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, tc.url, nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{Voucher: lpadata.Voucher{}}, nil) + + sessionStore := tc.sessionStore(t) + oneLoginClient := tc.oneLoginClient(t) + + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, tc.voucherStore(t), lpaStoreResolvingService, nil, "www.example.com")(testAppData, w, r, &voucherdata.Provided{LpaID: "lpa-id"}) + resp := w.Result() + + assert.Equal(t, tc.error, err) + assert.Equal(t, tc.expectedStatus, resp.StatusCode) + assert.Equal(t, tc.expectedRedirectURL, resp.Header.Get("Location")) + }) + } +} + +func TestGetIdentityWithOneLoginCallbackWhenGetLpaStoreResolvingServiceError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/?code=a-code", nil) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{Voucher: lpadata.Voucher{}}, expectedError) + + err := IdentityWithOneLoginCallback(nil, nil, nil, lpaStoreResolvingService, nil, "www.example.com")(testAppData, w, r, &voucherdata.Provided{}) + + assert.Equal(t, expectedError, err) +} + +func TestGetIdentityWithOneLoginCallbackWhenPutVoucherStoreError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/?code=a-code", nil) + userInfo := onelogin.UserInfo{CoreIdentityJWT: "an-identity-jwt"} + + voucherStore := newMockVoucherStore(t) + voucherStore.EXPECT(). + Put(r.Context(), mock.Anything). + Return(expectedError) + + lpaStoreResolvingService := newMockLpaStoreResolvingService(t) + lpaStoreResolvingService.EXPECT(). + Get(r.Context()). + Return(&lpadata.Lpa{Voucher: lpadata.Voucher{}}, nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + OneLogin(mock.Anything). + Return(&sesh.OneLoginSession{State: "a-state", Nonce: "a-nonce", Redirect: "/redirect"}, nil) + + oneLoginClient := newMockOneLoginClient(t) + oneLoginClient.EXPECT(). + Exchange(mock.Anything, mock.Anything, mock.Anything). + Return("id-token", "a-jwt", nil) + oneLoginClient.EXPECT(). + UserInfo(mock.Anything, mock.Anything). + Return(userInfo, nil) + oneLoginClient.EXPECT(). + ParseIdentityClaim(mock.Anything, mock.Anything). + Return(identity.UserData{Status: identity.StatusConfirmed}, nil) + + err := IdentityWithOneLoginCallback(oneLoginClient, sessionStore, voucherStore, lpaStoreResolvingService, nil, "www.example.com")(testAppData, w, r, &voucherdata.Provided{}) + + assert.Equal(t, expectedError, err) +} diff --git a/internal/voucher/voucherpage/identity_with_one_login_test.go b/internal/voucher/voucherpage/identity_with_one_login_test.go new file mode 100644 index 0000000000..8785a77d28 --- /dev/null +++ b/internal/voucher/voucherpage/identity_with_one_login_test.go @@ -0,0 +1,73 @@ +package voucherpage + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" + "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" + "github.com/ministryofjustice/opg-modernising-lpa/internal/voucher" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestIdentityWithOneLogin(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + client := newMockOneLoginClient(t) + client.EXPECT(). + AuthCodeURL("i am random", "i am random", "cy", true). + Return("http://auth", nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + SetOneLogin(r, w, &sesh.OneLoginSession{State: "i am random", Nonce: "i am random", Locale: "cy", Redirect: voucher.PathIdentityWithOneLoginCallback.Format("lpa-id")}). + Return(nil) + + err := IdentityWithOneLogin(client, sessionStore, func(int) string { return "i am random" })(appcontext.Data{LpaID: "lpa-id", Lang: localize.Cy}, w, r, nil) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, "http://auth", resp.Header.Get("Location")) +} + +func TestIdentityWithOneLoginWhenAuthCodeURLError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + client := newMockOneLoginClient(t) + client.EXPECT(). + AuthCodeURL("i am random", "i am random", "", true). + Return("http://auth?locale=en", expectedError) + + err := IdentityWithOneLogin(client, nil, func(int) string { return "i am random" })(testAppData, w, r, nil) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestIdentityWithOneLoginWhenStoreSaveError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + client := newMockOneLoginClient(t) + client.EXPECT(). + AuthCodeURL("i am random", "i am random", "", true). + Return("http://auth?locale=en", nil) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + SetOneLogin(r, w, mock.Anything). + Return(expectedError) + + err := IdentityWithOneLogin(client, sessionStore, func(int) string { return "i am random" })(testAppData, w, r, nil) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/internal/voucher/voucherpage/mock_NotifyClient_test.go b/internal/voucher/voucherpage/mock_NotifyClient_test.go new file mode 100644 index 0000000000..0e9d67df3c --- /dev/null +++ b/internal/voucher/voucherpage/mock_NotifyClient_test.go @@ -0,0 +1,134 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package voucherpage + +import ( + context "context" + + lpadata "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" + mock "github.com/stretchr/testify/mock" + + notify "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" +) + +// mockNotifyClient is an autogenerated mock type for the NotifyClient type +type mockNotifyClient struct { + mock.Mock +} + +type mockNotifyClient_Expecter struct { + mock *mock.Mock +} + +func (_m *mockNotifyClient) EXPECT() *mockNotifyClient_Expecter { + return &mockNotifyClient_Expecter{mock: &_m.Mock} +} + +// EmailGreeting provides a mock function with given fields: lpa +func (_m *mockNotifyClient) EmailGreeting(lpa *lpadata.Lpa) string { + ret := _m.Called(lpa) + + if len(ret) == 0 { + panic("no return value specified for EmailGreeting") + } + + var r0 string + if rf, ok := ret.Get(0).(func(*lpadata.Lpa) string); ok { + r0 = rf(lpa) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockNotifyClient_EmailGreeting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EmailGreeting' +type mockNotifyClient_EmailGreeting_Call struct { + *mock.Call +} + +// EmailGreeting is a helper method to define mock.On call +// - lpa *lpadata.Lpa +func (_e *mockNotifyClient_Expecter) EmailGreeting(lpa interface{}) *mockNotifyClient_EmailGreeting_Call { + return &mockNotifyClient_EmailGreeting_Call{Call: _e.mock.On("EmailGreeting", lpa)} +} + +func (_c *mockNotifyClient_EmailGreeting_Call) Run(run func(lpa *lpadata.Lpa)) *mockNotifyClient_EmailGreeting_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*lpadata.Lpa)) + }) + return _c +} + +func (_c *mockNotifyClient_EmailGreeting_Call) Return(_a0 string) *mockNotifyClient_EmailGreeting_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockNotifyClient_EmailGreeting_Call) RunAndReturn(run func(*lpadata.Lpa) string) *mockNotifyClient_EmailGreeting_Call { + _c.Call.Return(run) + return _c +} + +// SendActorEmail provides a mock function with given fields: ctx, to, lpaUID, email +func (_m *mockNotifyClient) SendActorEmail(ctx context.Context, to string, lpaUID string, email notify.Email) error { + ret := _m.Called(ctx, to, lpaUID, email) + + if len(ret) == 0 { + panic("no return value specified for SendActorEmail") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, notify.Email) error); ok { + r0 = rf(ctx, to, lpaUID, email) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockNotifyClient_SendActorEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendActorEmail' +type mockNotifyClient_SendActorEmail_Call struct { + *mock.Call +} + +// SendActorEmail is a helper method to define mock.On call +// - ctx context.Context +// - to string +// - lpaUID string +// - email notify.Email +func (_e *mockNotifyClient_Expecter) SendActorEmail(ctx interface{}, to interface{}, lpaUID interface{}, email interface{}) *mockNotifyClient_SendActorEmail_Call { + return &mockNotifyClient_SendActorEmail_Call{Call: _e.mock.On("SendActorEmail", ctx, to, lpaUID, email)} +} + +func (_c *mockNotifyClient_SendActorEmail_Call) Run(run func(ctx context.Context, to string, lpaUID string, email notify.Email)) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(notify.Email)) + }) + return _c +} + +func (_c *mockNotifyClient_SendActorEmail_Call) Return(_a0 error) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockNotifyClient_SendActorEmail_Call) RunAndReturn(run func(context.Context, string, string, notify.Email) error) *mockNotifyClient_SendActorEmail_Call { + _c.Call.Return(run) + return _c +} + +// newMockNotifyClient creates a new instance of mockNotifyClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockNotifyClient(t interface { + mock.TestingT + Cleanup(func()) +}) *mockNotifyClient { + mock := &mockNotifyClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/voucher/voucherpage/mock_OneLoginClient_test.go b/internal/voucher/voucherpage/mock_OneLoginClient_test.go index 59cf37e80b..c66ccb5c13 100644 --- a/internal/voucher/voucherpage/mock_OneLoginClient_test.go +++ b/internal/voucher/voucherpage/mock_OneLoginClient_test.go @@ -5,8 +5,10 @@ package voucherpage import ( context "context" - onelogin "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" + identity "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" mock "github.com/stretchr/testify/mock" + + onelogin "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" ) // mockOneLoginClient is an autogenerated mock type for the OneLoginClient type @@ -22,9 +24,9 @@ func (_m *mockOneLoginClient) EXPECT() *mockOneLoginClient_Expecter { return &mockOneLoginClient_Expecter{mock: &_m.Mock} } -// AuthCodeURL provides a mock function with given fields: state, nonce, locale, identity -func (_m *mockOneLoginClient) AuthCodeURL(state string, nonce string, locale string, identity bool) (string, error) { - ret := _m.Called(state, nonce, locale, identity) +// AuthCodeURL provides a mock function with given fields: state, nonce, locale, _a3 +func (_m *mockOneLoginClient) AuthCodeURL(state string, nonce string, locale string, _a3 bool) (string, error) { + ret := _m.Called(state, nonce, locale, _a3) if len(ret) == 0 { panic("no return value specified for AuthCodeURL") @@ -33,16 +35,16 @@ func (_m *mockOneLoginClient) AuthCodeURL(state string, nonce string, locale str var r0 string var r1 error if rf, ok := ret.Get(0).(func(string, string, string, bool) (string, error)); ok { - return rf(state, nonce, locale, identity) + return rf(state, nonce, locale, _a3) } if rf, ok := ret.Get(0).(func(string, string, string, bool) string); ok { - r0 = rf(state, nonce, locale, identity) + r0 = rf(state, nonce, locale, _a3) } else { r0 = ret.Get(0).(string) } if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok { - r1 = rf(state, nonce, locale, identity) + r1 = rf(state, nonce, locale, _a3) } else { r1 = ret.Error(1) } @@ -59,12 +61,12 @@ type mockOneLoginClient_AuthCodeURL_Call struct { // - state string // - nonce string // - locale string -// - identity bool -func (_e *mockOneLoginClient_Expecter) AuthCodeURL(state interface{}, nonce interface{}, locale interface{}, identity interface{}) *mockOneLoginClient_AuthCodeURL_Call { - return &mockOneLoginClient_AuthCodeURL_Call{Call: _e.mock.On("AuthCodeURL", state, nonce, locale, identity)} +// - _a3 bool +func (_e *mockOneLoginClient_Expecter) AuthCodeURL(state interface{}, nonce interface{}, locale interface{}, _a3 interface{}) *mockOneLoginClient_AuthCodeURL_Call { + return &mockOneLoginClient_AuthCodeURL_Call{Call: _e.mock.On("AuthCodeURL", state, nonce, locale, _a3)} } -func (_c *mockOneLoginClient_AuthCodeURL_Call) Run(run func(state string, nonce string, locale string, identity bool)) *mockOneLoginClient_AuthCodeURL_Call { +func (_c *mockOneLoginClient_AuthCodeURL_Call) Run(run func(state string, nonce string, locale string, _a3 bool)) *mockOneLoginClient_AuthCodeURL_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool)) }) @@ -146,6 +148,63 @@ func (_c *mockOneLoginClient_Exchange_Call) RunAndReturn(run func(context.Contex return _c } +// ParseIdentityClaim provides a mock function with given fields: ctx, userInfo +func (_m *mockOneLoginClient) ParseIdentityClaim(ctx context.Context, userInfo onelogin.UserInfo) (identity.UserData, error) { + ret := _m.Called(ctx, userInfo) + + if len(ret) == 0 { + panic("no return value specified for ParseIdentityClaim") + } + + var r0 identity.UserData + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, onelogin.UserInfo) (identity.UserData, error)); ok { + return rf(ctx, userInfo) + } + if rf, ok := ret.Get(0).(func(context.Context, onelogin.UserInfo) identity.UserData); ok { + r0 = rf(ctx, userInfo) + } else { + r0 = ret.Get(0).(identity.UserData) + } + + if rf, ok := ret.Get(1).(func(context.Context, onelogin.UserInfo) error); ok { + r1 = rf(ctx, userInfo) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockOneLoginClient_ParseIdentityClaim_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ParseIdentityClaim' +type mockOneLoginClient_ParseIdentityClaim_Call struct { + *mock.Call +} + +// ParseIdentityClaim is a helper method to define mock.On call +// - ctx context.Context +// - userInfo onelogin.UserInfo +func (_e *mockOneLoginClient_Expecter) ParseIdentityClaim(ctx interface{}, userInfo interface{}) *mockOneLoginClient_ParseIdentityClaim_Call { + return &mockOneLoginClient_ParseIdentityClaim_Call{Call: _e.mock.On("ParseIdentityClaim", ctx, userInfo)} +} + +func (_c *mockOneLoginClient_ParseIdentityClaim_Call) Run(run func(ctx context.Context, userInfo onelogin.UserInfo)) *mockOneLoginClient_ParseIdentityClaim_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(onelogin.UserInfo)) + }) + return _c +} + +func (_c *mockOneLoginClient_ParseIdentityClaim_Call) Return(_a0 identity.UserData, _a1 error) *mockOneLoginClient_ParseIdentityClaim_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockOneLoginClient_ParseIdentityClaim_Call) RunAndReturn(run func(context.Context, onelogin.UserInfo) (identity.UserData, error)) *mockOneLoginClient_ParseIdentityClaim_Call { + _c.Call.Return(run) + return _c +} + // UserInfo provides a mock function with given fields: ctx, accessToken func (_m *mockOneLoginClient) UserInfo(ctx context.Context, accessToken string) (onelogin.UserInfo, error) { ret := _m.Called(ctx, accessToken) diff --git a/internal/voucher/voucherpage/register.go b/internal/voucher/voucherpage/register.go index da16b4c723..67504ae7d2 100644 --- a/internal/voucher/voucherpage/register.go +++ b/internal/voucher/voucherpage/register.go @@ -10,7 +10,9 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/appcontext" "github.com/ministryofjustice/opg-modernising-lpa/internal/dashboard/dashboarddata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/identity" "github.com/ministryofjustice/opg-modernising-lpa/internal/lpastore/lpadata" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/random" @@ -34,6 +36,11 @@ type LpaStoreResolvingService interface { Get(ctx context.Context) (*lpadata.Lpa, error) } +type NotifyClient interface { + EmailGreeting(lpa *lpadata.Lpa) string + SendActorEmail(ctx context.Context, to, lpaUID string, email notify.Email) error +} + type Logger interface { InfoContext(ctx context.Context, msg string, args ...any) WarnContext(ctx context.Context, msg string, args ...any) @@ -53,6 +60,7 @@ type OneLoginClient interface { AuthCodeURL(state, nonce, locale string, identity bool) (string, error) Exchange(ctx context.Context, code, nonce string) (idToken, accessToken string, err error) UserInfo(ctx context.Context, accessToken string) (onelogin.UserInfo, error) + ParseIdentityClaim(ctx context.Context, userInfo onelogin.UserInfo) (identity.UserData, error) } type ShareCodeStore interface { @@ -83,6 +91,8 @@ func Register( dashboardStore DashboardStore, errorHandler page.ErrorHandler, lpaStoreResolvingService LpaStoreResolvingService, + notifyClient NotifyClient, + appPublicURL string, ) { handleRoot := makeHandle(rootMux, sessionStore, errorHandler) @@ -112,6 +122,14 @@ func Register( handleVoucher(voucher.PathConfirmYourIdentity, None, Guidance(tmpls.Get("confirm_your_identity.gohtml"), lpaStoreResolvingService)) + handleVoucher(voucher.PathIdentityWithOneLogin, None, + IdentityWithOneLogin(oneLoginClient, sessionStore, random.String)) + handleVoucher(voucher.PathIdentityWithOneLoginCallback, None, + IdentityWithOneLoginCallback(oneLoginClient, sessionStore, voucherStore, lpaStoreResolvingService, notifyClient, appPublicURL)) + handleVoucher(voucher.PathOneLoginIdentityDetails, None, + Guidance(tmpls.Get("one_login_identity_details.gohtml"), lpaStoreResolvingService)) + handleVoucher(voucher.PathUnableToConfirmIdentity, None, + Guidance(tmpls.Get("unable_to_confirm_identity.gohtml"), lpaStoreResolvingService)) } type handleOpt byte diff --git a/internal/voucher/voucherpage/register_test.go b/internal/voucher/voucherpage/register_test.go index 5882b617a3..6fb52faa00 100644 --- a/internal/voucher/voucherpage/register_test.go +++ b/internal/voucher/voucherpage/register_test.go @@ -19,7 +19,7 @@ import ( func TestRegister(t *testing.T) { mux := http.NewServeMux() - Register(mux, &mockLogger{}, template.Templates{}, &mockSessionStore{}, &mockVoucherStore{}, &mockOneLoginClient{}, &mockShareCodeStore{}, &mockDashboardStore{}, nil, &mockLpaStoreResolvingService{}) + Register(mux, &mockLogger{}, template.Templates{}, &mockSessionStore{}, &mockVoucherStore{}, &mockOneLoginClient{}, &mockShareCodeStore{}, &mockDashboardStore{}, nil, &mockLpaStoreResolvingService{}, &mockNotifyClient{}, "http://app") assert.Implements(t, (*http.Handler)(nil), mux) } diff --git a/internal/voucher/voucherpage/task_list.go b/internal/voucher/voucherpage/task_list.go index 0ac82fe1e4..b14fb9d562 100644 --- a/internal/voucher/voucherpage/task_list.go +++ b/internal/voucher/voucherpage/task_list.go @@ -20,7 +20,7 @@ type taskListData struct { type taskListItem struct { Name string - Path string + Path voucher.Path State task.State } @@ -31,27 +31,32 @@ func TaskList(tmpl template.Template, lpaStoreResolvingService LpaStoreResolving return err } + confirmYourIdentityPath := voucher.PathConfirmYourIdentity + if provided.Tasks.ConfirmYourIdentity.IsCompleted() { + confirmYourIdentityPath = voucher.PathOneLoginIdentityDetails + } + items := []taskListItem{ { Name: "confirmYourName", - Path: voucher.PathConfirmYourName.Format(appData.LpaID), + Path: voucher.PathConfirmYourName, State: provided.Tasks.ConfirmYourName, }, { Name: appData.Localizer.Format("verifyPersonDetails", map[string]any{ "DonorFullNamePossessive": appData.Localizer.Possessive(lpa.Donor.FullName()), }), - Path: voucher.PathVerifyDonorDetails.Format(appData.LpaID), + Path: voucher.PathVerifyDonorDetails, State: provided.Tasks.VerifyDonorDetails, }, { Name: "confirmYourIdentity", - Path: voucher.PathConfirmYourIdentity.Format(appData.LpaID), + Path: confirmYourIdentityPath, State: provided.Tasks.ConfirmYourIdentity, }, { Name: "signTheDeclaration", - Path: voucher.PathSignTheDeclaration.Format(appData.LpaID), + Path: voucher.PathSignTheDeclaration, State: provided.Tasks.SignTheDeclaration, }, } diff --git a/internal/voucher/voucherpage/task_list_test.go b/internal/voucher/voucherpage/task_list_test.go index ac6684ebc1..95890da05d 100644 --- a/internal/voucher/voucherpage/task_list_test.go +++ b/internal/voucher/voucherpage/task_list_test.go @@ -46,6 +46,7 @@ func TestGetTaskList(t *testing.T) { items[0].State = task.StateCompleted items[1].State = task.StateCompleted items[2].State = task.StateCompleted + items[2].Path = voucher.PathOneLoginIdentityDetails items[3].State = task.StateCompleted return items }, @@ -79,10 +80,10 @@ func TestGetTaskList(t *testing.T) { App: appData, Voucher: tc.voucher, Items: tc.expected([]taskListItem{ - {Name: "confirmYourName", Path: voucher.PathConfirmYourName.Format("lpa-id")}, - {Name: "verifyJohnSmithsDetails", Path: voucher.PathVerifyDonorDetails.Format("lpa-id")}, - {Name: "confirmYourIdentity", Path: voucher.PathConfirmYourIdentity.Format("lpa-id")}, - {Name: "signTheDeclaration", Path: voucher.PathSignTheDeclaration.Format("lpa-id")}, + {Name: "confirmYourName", Path: voucher.PathConfirmYourName}, + {Name: "verifyJohnSmithsDetails", Path: voucher.PathVerifyDonorDetails}, + {Name: "confirmYourIdentity", Path: voucher.PathConfirmYourIdentity}, + {Name: "signTheDeclaration", Path: voucher.PathSignTheDeclaration}, }), }). Return(nil) diff --git a/lang/cy.json b/lang/cy.json index 439b25ac3e..748644d229 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -1337,5 +1337,6 @@ "youHaveToldUsThatTheDetailsDoNotMatch": "Welsh", "youHaveToldUsDetailsDoNotMatchIdentity": "Welsh {{.DonorFullNamePossessive}}", "youHaveToldUsDetailsDoNotMatchIdentityContent": "

Welsh {{.DonorFirstNames}}

", - "voucherConfirmYourIdentityContent": "

Welsh {{.DonorFullName}}

" + "voucherConfirmYourIdentityContent": "

Welsh {{.DonorFullName}}

", + "voucherUnableToConfirmIdentityContent": "

Welsh {{.DonorFullName}} {{.DonorFirstNames}}

" } diff --git a/lang/en.json b/lang/en.json index c29681a00f..5199546c5a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1266,5 +1266,6 @@ "youHaveToldUsThatTheDetailsDoNotMatch": "You have told us that the details do not match", "youHaveToldUsDetailsDoNotMatchIdentity": "You have told us that the details do not match {{.DonorFullNamePossessive}} identity.", "youHaveToldUsDetailsDoNotMatchIdentityContent": "

We have contacted {{.DonorFirstNames}} to let them know. You do not need to do anything else.

You can now close this browsing window.

", - "voucherConfirmYourIdentityContent": "

Before you can vouch for {{.DonorFullName}}, you need to confirm your own identity.

You can do this either:

You will need an identity document, for example your passport or driving licence.

It takes longer to do it at a Post Office, but you can usually get an outcome within a day.

Choose how to confirm your identity

When you continue, you’ll go to GOV.UK One Login where you can choose if you want to confirm your identity online or in person.

" + "voucherConfirmYourIdentityContent": "

Before you can vouch for {{.DonorFullName}}, you need to confirm your own identity.

You can do this either:

You will need an identity document, for example your passport or driving licence.

It takes longer to do it at a Post Office, but you can usually get an outcome within a day.

Choose how to confirm your identity

When you continue, you’ll go to GOV.UK One Login where you can choose if you want to confirm your identity online or in person.

", + "voucherUnableToConfirmIdentityContent": "

This means you cannot vouch for {{.DonorFullName}}. We have contacted {{.DonorFirstNames}} to let them know.

You do not need to do anything else.

You can now close this browsing window.

" } diff --git a/web/template/voucher/confirm_your_identity.gohtml b/web/template/voucher/confirm_your_identity.gohtml index 9ece341c24..be67813c20 100644 --- a/web/template/voucher/confirm_your_identity.gohtml +++ b/web/template/voucher/confirm_your_identity.gohtml @@ -10,7 +10,7 @@ {{ trFormatHtml .App "voucherConfirmYourIdentityContent" "DonorFullName" .Lpa.Donor.FullName }} - {{ template "button" (button .App "continue" "link" (global.Paths.Voucher.TaskList.Format .App.LpaID)) }} + {{ template "button" (button .App "continue" "link" (global.Paths.Voucher.IdentityWithOneLogin.Format .App.LpaID)) }} {{ end }} diff --git a/web/template/voucher/confirm_your_name.gohtml b/web/template/voucher/confirm_your_name.gohtml index dbec8d2294..849d77f50d 100644 --- a/web/template/voucher/confirm_your_name.gohtml +++ b/web/template/voucher/confirm_your_name.gohtml @@ -3,6 +3,8 @@ {{ define "pageTitle" }}{{ tr .App "confirmYourName" }}{{ end }} {{ define "main" }} + {{ $canChange := not .Tasks.ConfirmYourIdentity.IsCompleted }} +

{{ tr .App "confirmYourName" }}

@@ -13,12 +15,12 @@ {{ template "summary-row" (summaryRow .App "firstNames" .FirstNames (fromLink .App global.Paths.Voucher.YourName "#f-first-names") - "" true true) }} + "" $canChange true) }} {{ template "summary-row" (summaryRow .App "lastName" .LastName (fromLink .App global.Paths.Voucher.YourName "#f-last-name") - "" true true) }} + "" $canChange true) }} {{ template "warning" (content .App "thisNameMustMatchYourConfirmIdentityDetailsWarning") }} diff --git a/web/template/voucher/one_login_identity_details.gohtml b/web/template/voucher/one_login_identity_details.gohtml new file mode 100644 index 0000000000..6458dd1c17 --- /dev/null +++ b/web/template/voucher/one_login_identity_details.gohtml @@ -0,0 +1,27 @@ +{{ template "page" . }} + +{{ define "pageTitle" }} + {{ tr .App "yourIdentityConfirmedWithOneLogin" }} +{{ end }} + +{{ define "main" }} +
+
+

{{ tr .App "yourIdentityConfirmedWithOneLogin" }}

+ +
+ {{ template "summary-row" (summaryRow .App "firstNames" + .Voucher.IdentityUserData.FirstNames + "" "" false false) }} + {{ template "summary-row" (summaryRow .App "lastName" + .Voucher.IdentityUserData.LastName + "" "" false false) }} +
+ +
+ {{ template "buttons" (button .App "continue" "link" (global.Paths.Voucher.TaskList.Format .App.LpaID)) }} + {{ template "csrf-field" . }} +
+
+
+{{ end }} diff --git a/web/template/voucher/task_list.gohtml b/web/template/voucher/task_list.gohtml index 1b8b46aaa8..de1df56f67 100644 --- a/web/template/voucher/task_list.gohtml +++ b/web/template/voucher/task_list.gohtml @@ -10,12 +10,12 @@
    {{ range .Items }} - {{ $hasLink := voucherCanGoTo $.Voucher .Path }} + {{ $hasLink := .Path.CanGoTo $.Voucher }}