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.
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.
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 }} +