diff --git a/cypress/e2e/supporter/dashboard.cy.js b/cypress/e2e/supporter/dashboard.cy.js new file mode 100644 index 0000000000..24bf6ad186 --- /dev/null +++ b/cypress/e2e/supporter/dashboard.cy.js @@ -0,0 +1,12 @@ +describe('Dashboard', () => { + beforeEach(() => { + cy.visit('/fixtures/supporter?redirect=/supporter-dashboard&organisation=1'); + }); + + it('can create a new LPA', () => { + cy.checkA11yApp(); + cy.contains('button', 'Make a new LPA').click(); + + cy.url().should('contain', '/your-details'); + }); +}); diff --git a/internal/app/donor_store.go b/internal/app/donor_store.go index d70ca03a51..cdaa384c8c 100644 --- a/internal/app/donor_store.go +++ b/internal/app/donor_store.go @@ -121,8 +121,14 @@ func (s *donorStore) Get(ctx context.Context) (*actor.DonorProvidedDetails, erro return nil, errors.New("donorStore.Get requires LpaID and SessionID") } + sk := donorKey(data.SessionID) + + if data.OrganisationID != "" { + sk = organisationKey(data.OrganisationID) + } + var donor *actor.DonorProvidedDetails - err = s.dynamoClient.One(ctx, lpaKey(data.LpaID), donorKey(data.SessionID), &donor) + err = s.dynamoClient.One(ctx, lpaKey(data.LpaID), sk, &donor) return donor, err } diff --git a/internal/app/donor_store_test.go b/internal/app/donor_store_test.go index 82a6dc6e0e..4724cb0e82 100644 --- a/internal/app/donor_store_test.go +++ b/internal/app/donor_store_test.go @@ -116,16 +116,34 @@ func TestDonorStoreGetAnyWhenDataStoreError(t *testing.T) { } func TestDonorStoreGet(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) + testCases := map[string]struct { + sessionData *page.SessionData + expectedSK string + }{ + "donor": { + sessionData: &page.SessionData{LpaID: "an-id", SessionID: "456"}, + expectedSK: "#DONOR#456", + }, + "organisation": { + sessionData: &page.SessionData{LpaID: "an-id", SessionID: "456", OrganisationID: "789"}, + expectedSK: "ORGANISATION#789", + }, + } - dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectOne(ctx, "LPA#an-id", "#DONOR#456", &actor.DonorProvidedDetails{LpaID: "an-id"}, nil) + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), tc.sessionData) - donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} + dynamoClient := newMockDynamoClient(t) + dynamoClient.ExpectOne(ctx, "LPA#an-id", tc.expectedSK, &actor.DonorProvidedDetails{LpaID: "an-id"}, nil) - lpa, err := donorStore.Get(ctx) - assert.Nil(t, err) - assert.Equal(t, &actor.DonorProvidedDetails{LpaID: "an-id"}, lpa) + donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} + + lpa, err := donorStore.Get(ctx) + assert.Nil(t, err) + assert.Equal(t, &actor.DonorProvidedDetails{LpaID: "an-id"}, lpa) + }) + } } func TestDonorStoreGetWithSessionMissing(t *testing.T) { diff --git a/internal/app/organisation_store.go b/internal/app/organisation_store.go index 8392f4a3d2..822ee1cfd7 100644 --- a/internal/app/organisation_store.go +++ b/internal/app/organisation_store.go @@ -17,14 +17,14 @@ type organisationStore struct { now func() time.Time } -func (s *organisationStore) Create(ctx context.Context, name string) error { +func (s *organisationStore) Create(ctx context.Context, name string) (*actor.Organisation, error) { data, err := page.SessionDataFromContext(ctx) if err != nil { - return err + return nil, err } if data.SessionID == "" { - return errors.New("organisationStore.Create requires SessionID") + return nil, errors.New("organisationStore.Create requires SessionID") } organisationID := s.uuidString() @@ -38,7 +38,7 @@ func (s *organisationStore) Create(ctx context.Context, name string) error { } if err := s.dynamoClient.Create(ctx, organisation); err != nil { - return fmt.Errorf("error creating organisation: %w", err) + return nil, fmt.Errorf("error creating organisation: %w", err) } member := &actor.Member{ @@ -48,10 +48,10 @@ func (s *organisationStore) Create(ctx context.Context, name string) error { } if err := s.dynamoClient.Create(ctx, member); err != nil { - return fmt.Errorf("error creating organisation member: %w", err) + return nil, fmt.Errorf("error creating organisation member: %w", err) } - return nil + return organisation, nil } func (s *organisationStore) Get(ctx context.Context) (*actor.Organisation, error) { @@ -98,6 +98,37 @@ func (s *organisationStore) CreateMemberInvite(ctx context.Context, organisation return nil } +func (s *organisationStore) CreateLPA(ctx context.Context, organisationID string) (*actor.DonorProvidedDetails, error) { + data, err := page.SessionDataFromContext(ctx) + if err != nil { + return nil, err + } + + if data.SessionID == "" { + return nil, errors.New("donorStore.Create requires SessionID") + } + + lpaID := s.uuidString() + + donor := &actor.DonorProvidedDetails{ + PK: lpaKey(lpaID), + SK: organisationKey(organisationID), + LpaID: lpaID, + CreatedAt: s.now(), + Version: 1, + } + + if donor.Hash, err = donor.GenerateHash(); err != nil { + return nil, err + } + + if err := s.dynamoClient.Create(ctx, donor); err != nil { + return nil, err + } + + return donor, err +} + func organisationKey(s string) string { return "ORGANISATION#" + s } diff --git a/internal/app/organisation_store_test.go b/internal/app/organisation_store_test.go index 3bd0f57fbc..48487eafc1 100644 --- a/internal/app/organisation_store_test.go +++ b/internal/app/organisation_store_test.go @@ -33,8 +33,15 @@ func TestOrganisationStoreCreate(t *testing.T) { organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} - err := organisationStore.Create(ctx, "A name") + organisation, err := organisationStore.Create(ctx, "A name") assert.Nil(t, err) + assert.Equal(t, &actor.Organisation{ + PK: "ORGANISATION#a-uuid", + SK: "ORGANISATION#a-uuid", + ID: "a-uuid", + CreatedAt: testNow, + Name: "A name", + }, organisation) } func TestOrganisationStoreCreateWithSessionMissing(t *testing.T) { @@ -47,8 +54,9 @@ func TestOrganisationStoreCreateWithSessionMissing(t *testing.T) { t.Run(name, func(t *testing.T) { organisationStore := &organisationStore{} - err := organisationStore.Create(ctx, "A name") + organisation, err := organisationStore.Create(ctx, "A name") assert.Error(t, err) + assert.Nil(t, organisation) }) } } @@ -84,8 +92,9 @@ func TestOrganisationStoreCreateWhenErrors(t *testing.T) { dynamoClient := makeMockDynamoClient(t) organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} - err := organisationStore.Create(ctx, "A name") + organisation, err := organisationStore.Create(ctx, "A name") assert.ErrorIs(t, err, expectedError) + assert.Nil(t, organisation) }) } } @@ -211,3 +220,62 @@ func TestOrganisationStoreCreateMemberInviteWhenErrors(t *testing.T) { err := organisationStore.CreateMemberInvite(ctx, &actor.Organisation{}, "email@example.com", "abcde") assert.ErrorIs(t, err, expectedError) } + +func TestOrganisationStoreCreateLPA(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + expectedDonor := &actor.DonorProvidedDetails{ + PK: "LPA#a-uuid", + SK: "ORGANISATION#an-id", + LpaID: "a-uuid", + CreatedAt: testNow, + Version: 1, + } + expectedDonor.Hash, _ = expectedDonor.GenerateHash() + + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + Create(ctx, expectedDonor). + Return(nil) + + organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + donor, err := organisationStore.CreateLPA(ctx, "an-id") + + assert.Nil(t, err) + assert.Equal(t, expectedDonor, donor) +} + +func TestOrganisationStoreCreateLPAWithSessionMissing(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: ""}) + + organisationStore := &organisationStore{dynamoClient: nil, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + _, err := organisationStore.CreateLPA(ctx, "an-id") + + assert.NotNil(t, err) +} + +func TestOrganisationStoreCreateLPAMissingSessionID(t *testing.T) { + ctx := context.Background() + + organisationStore := &organisationStore{dynamoClient: nil, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + _, err := organisationStore.CreateLPA(ctx, "an-id") + + assert.Equal(t, page.SessionMissingError{}, err) +} + +func TestOrganisationStoreCreateLPAWhenDynamoError(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + Create(ctx, mock.Anything). + Return(expectedError) + + organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + _, err := organisationStore.CreateLPA(ctx, "an-id") + + assert.Equal(t, expectedError, err) +} diff --git a/internal/page/data.go b/internal/page/data.go index d55daf53b9..ee67e873cd 100644 --- a/internal/page/data.go +++ b/internal/page/data.go @@ -20,8 +20,9 @@ import ( ) type SessionData struct { - SessionID string - LpaID string + SessionID string + LpaID string + OrganisationID string } type SessionMissingError struct{} diff --git a/internal/page/donor/register.go b/internal/page/donor/register.go index 6e3a1443b5..86388b26d5 100644 --- a/internal/page/donor/register.go +++ b/internal/page/donor/register.go @@ -436,22 +436,30 @@ func makeLpaHandle(mux *http.ServeMux, store sesh.Store, defaultOptions page.Han appData.ActorType = actor.TypeDonor appData.AppPublicURL = appPublicURL - donorSession, err := sesh.Login(store, r) + loginSession, err := sesh.Login(store, r) if err != nil { http.Redirect(w, r, page.Paths.Start.Format(), http.StatusFound) return } - appData.SessionID = donorSession.SessionID() - + appData.SessionID = loginSession.SessionID() sessionData, err := page.SessionDataFromContext(ctx) + if err == nil { sessionData.SessionID = appData.SessionID ctx = page.ContextWithSessionData(ctx, sessionData) appData.LpaID = sessionData.LpaID } else { - ctx = page.ContextWithSessionData(ctx, &page.SessionData{SessionID: appData.SessionID, LpaID: appData.LpaID}) + sessionData = &page.SessionData{SessionID: appData.SessionID, LpaID: appData.LpaID} + ctx = page.ContextWithSessionData(ctx, sessionData) + } + + if loginSession.OrganisationID != "" { + appData.IsSupporter = true + appData.OrganisationName = loginSession.OrganisationName + + sessionData.OrganisationID = loginSession.OrganisationID } appData.Page = path.Format(appData.LpaID) diff --git a/internal/page/donor/register_test.go b/internal/page/donor/register_test.go index 23c7186625..01c749b457 100644 --- a/internal/page/donor/register_test.go +++ b/internal/page/donor/register_test.go @@ -184,52 +184,80 @@ func TestMakeHandleNoSessionRequired(t *testing.T) { } func TestMakeLpaHandleWhenDetailsProvidedAndUIDExists(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/path", nil) + testCases := map[string]struct { + expectedAppData page.AppData + loginSesh *sessions.Session + expectedSessionData *page.SessionData + }{ + "donor": { + expectedAppData: page.AppData{ + Page: "/lpa//path", + ActorType: actor.TypeDonor, + SessionID: "cmFuZG9t", + AppPublicURL: "http://example.org", + }, + loginSesh: &sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random"}}}, + expectedSessionData: &page.SessionData{SessionID: "cmFuZG9t"}, + }, + "organisation": { + expectedAppData: page.AppData{ + Page: "/lpa//path", + ActorType: actor.TypeDonor, + SessionID: "cmFuZG9t", + AppPublicURL: "http://example.org", + IsSupporter: true, + }, + loginSesh: &sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random", OrganisationID: "org-id"}}}, + expectedSessionData: &page.SessionData{SessionID: "cmFuZG9t", OrganisationID: "org-id"}, + }, + } - mux := http.NewServeMux() + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/path", nil) - sessionStore := newMockSessionStore(t) - sessionStore.EXPECT(). - Get(r, "session"). - Return(&sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random"}}}, nil) + mux := http.NewServeMux() - donorStore := newMockDonorStore(t) - donorStore.EXPECT(). - Get(mock.Anything). - Return(&actor.DonorProvidedDetails{Donor: actor.Donor{ - FirstNames: "Jane", - LastName: "Smith", - DateOfBirth: date.New("2000", "1", "2"), - Address: place.Address{Postcode: "ABC123"}, - }, - Type: actor.LpaTypePropertyAndAffairs, - Tasks: actor.DonorTasks{YourDetails: actor.TaskCompleted}, - LpaUID: "a-uid", - }, nil) + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + Get(r, "session"). + Return(tc.loginSesh, nil) - handle := makeLpaHandle(mux, sessionStore, page.RequireSession, nil, donorStore, "http://example.org") - handle("/path", page.None, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, _ *actor.DonorProvidedDetails) error { - assert.Equal(t, page.AppData{ - Page: "/lpa//path", - ActorType: actor.TypeDonor, - SessionID: "cmFuZG9t", - AppPublicURL: "http://example.org", - }, appData) + donorStore := newMockDonorStore(t) + donorStore.EXPECT(). + Get(mock.Anything). + Return(&actor.DonorProvidedDetails{Donor: actor.Donor{ + FirstNames: "Jane", + LastName: "Smith", + DateOfBirth: date.New("2000", "1", "2"), + Address: place.Address{Postcode: "ABC123"}, + }, + Type: actor.LpaTypePropertyAndAffairs, + Tasks: actor.DonorTasks{YourDetails: actor.TaskCompleted}, + LpaUID: "a-uid", + }, nil) - assert.Equal(t, w, hw) + handle := makeLpaHandle(mux, sessionStore, page.RequireSession, nil, donorStore, "http://example.org") + handle("/path", page.None, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, _ *actor.DonorProvidedDetails) error { + assert.Equal(t, tc.expectedAppData, appData) - sessionData, _ := page.SessionDataFromContext(hr.Context()) - assert.Equal(t, &page.SessionData{SessionID: "cmFuZG9t"}, sessionData) + assert.Equal(t, w, hw) - hw.WriteHeader(http.StatusTeapot) - return nil - }) + sessionData, _ := page.SessionDataFromContext(hr.Context()) + assert.Equal(t, tc.expectedSessionData, sessionData) - mux.ServeHTTP(w, r) - resp := w.Result() + hw.WriteHeader(http.StatusTeapot) + return nil + }) + + mux.ServeHTTP(w, r) + resp := w.Result() + + assert.Equal(t, http.StatusTeapot, resp.StatusCode) + }) + } - assert.Equal(t, http.StatusTeapot, resp.StatusCode) } func TestMakeLpaHandleWhenSessionStoreError(t *testing.T) { diff --git a/internal/page/fixtures/supporter.go b/internal/page/fixtures/supporter.go index f0d0e5da47..7d4c3c6057 100644 --- a/internal/page/fixtures/supporter.go +++ b/internal/page/fixtures/supporter.go @@ -5,13 +5,14 @@ import ( "encoding/base64" "net/http" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/random" "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" ) type OrganisationStore interface { - Create(context.Context, string) error + Create(context.Context, string) (*actor.Organisation, error) } func Supporter(sessionStore sesh.Store, organisationStore OrganisationStore) page.Handler { @@ -25,14 +26,20 @@ func Supporter(sessionStore sesh.Store, organisationStore OrganisationStore) pag ctx = page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: supporterSessionID}) ) - if err := sesh.SetLoginSession(sessionStore, r, w, &sesh.LoginSession{Sub: supporterSub, Email: testEmail}); err != nil { - return err - } + loginSession := &sesh.LoginSession{Sub: supporterSub, Email: testEmail} if organisation == "1" { - if err := organisationStore.Create(ctx, random.String(12)); err != nil { + org, err := organisationStore.Create(ctx, random.String(12)) + + if err != nil { return err } + + loginSession.OrganisationID = org.ID + } + + if err := sesh.SetLoginSession(sessionStore, r, w, loginSession); err != nil { + return err } if redirect != page.Paths.Supporter.EnterOrganisationName.Format() { diff --git a/internal/page/paths.go b/internal/page/paths.go index e1e342424e..b69ce009db 100644 --- a/internal/page/paths.go +++ b/internal/page/paths.go @@ -346,11 +346,11 @@ var Paths = AppPaths{ }, Supporter: SupporterPaths{ - Start: "/supporter-start", - Login: "/supporter-login", - LoginCallback: "/supporter-login-callback", + Start: "/supporter-start", + Login: "/supporter-login", + LoginCallback: "/supporter-login-callback", + EnterOrganisationName: "/enter-the-name-of-your-organisation-or-company", - EnterOrganisationName: "/enter-the-name-of-your-organisation-or-company", OrganisationCreated: "/organisation-or-company-created", Dashboard: "/supporter-dashboard", InviteMember: "/invite-member", diff --git a/internal/page/supporter/dashboard.go b/internal/page/supporter/dashboard.go new file mode 100644 index 0000000000..ccdcc1243f --- /dev/null +++ b/internal/page/supporter/dashboard.go @@ -0,0 +1,30 @@ +package supporter + +import ( + "net/http" + + "github.com/ministryofjustice/opg-go-common/template" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +type DashboardData struct { + App page.AppData + Errors validation.List +} + +func Dashboard(tmpl template.Template, organisationStore OrganisationStore) Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request, organisation *actor.Organisation) error { + if r.Method == http.MethodPost { + donorProvided, err := organisationStore.CreateLPA(r.Context(), organisation.ID) + if err != nil { + return err + } + + return page.Paths.YourDetails.Redirect(w, r.WithContext(r.Context()), appData, donorProvided) + } + + return tmpl(w, &DashboardData{App: appData}) + } +} diff --git a/internal/page/supporter/dashboard_test.go b/internal/page/supporter/dashboard_test.go new file mode 100644 index 0000000000..949d9ffe97 --- /dev/null +++ b/internal/page/supporter/dashboard_test.go @@ -0,0 +1,62 @@ +package supporter + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/stretchr/testify/assert" +) + +func TestGetDashboard(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &DashboardData{ + App: testAppData, + }). + Return(nil) + + err := Dashboard(template.Execute, nil)(testAppData, w, r, nil) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostDashboard(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", nil) + + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + CreateLPA(r.Context(), "org-id"). + Return(&actor.DonorProvidedDetails{LpaID: "lpa-id"}, nil) + + err := Dashboard(nil, organisationStore)(testAppData, w, r, &actor.Organisation{ID: "org-id"}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.YourDetails.Format("lpa-id"), resp.Header.Get("Location")) +} + +func TestPostDashboardWhenOrganisationStoreError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", nil) + + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + CreateLPA(r.Context(), "org-id"). + Return(&actor.DonorProvidedDetails{}, expectedError) + + err := Dashboard(nil, organisationStore)(testAppData, w, r, &actor.Organisation{ID: "org-id"}) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/internal/page/supporter/enter_organisation_name.go b/internal/page/supporter/enter_organisation_name.go index e950327340..746ac5e2a7 100644 --- a/internal/page/supporter/enter_organisation_name.go +++ b/internal/page/supporter/enter_organisation_name.go @@ -5,6 +5,7 @@ import ( "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" ) @@ -14,7 +15,7 @@ type enterOrganisationNameData struct { Form *organisationNameForm } -func EnterOrganisationName(tmpl template.Template, organisationStore OrganisationStore) page.Handler { +func EnterOrganisationName(tmpl template.Template, organisationStore OrganisationStore, sessionStore sesh.Store) page.Handler { return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { data := &enterOrganisationNameData{ App: appData, @@ -26,7 +27,18 @@ func EnterOrganisationName(tmpl template.Template, organisationStore Organisatio data.Errors = data.Form.Validate() if !data.Errors.Any() { - if err := organisationStore.Create(r.Context(), data.Form.Name); err != nil { + organisation, err := organisationStore.Create(r.Context(), data.Form.Name) + if err != nil { + return err + } + + loginSession, err := sesh.Login(sessionStore, r) + if err != nil { + return page.Paths.Supporter.Start.Redirect(w, r, appData) + } + + loginSession.OrganisationID = organisation.ID + if err := sesh.SetLoginSession(sessionStore, r, w, loginSession); err != nil { return err } diff --git a/internal/page/supporter/enter_organisation_name_test.go b/internal/page/supporter/enter_organisation_name_test.go index fc4015a2d2..2e23b3287b 100644 --- a/internal/page/supporter/enter_organisation_name_test.go +++ b/internal/page/supporter/enter_organisation_name_test.go @@ -7,7 +7,10 @@ import ( "strings" "testing" + "github.com/gorilla/sessions" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -25,7 +28,7 @@ func TestGetEnterOrganisationName(t *testing.T) { }). Return(nil) - err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + err := EnterOrganisationName(template.Execute, nil, nil)(testAppData, w, r) resp := w.Result() assert.Nil(t, err) @@ -41,7 +44,7 @@ func TestGetEnterOrganisationNameWhenTemplateErrors(t *testing.T) { Execute(w, mock.Anything). Return(expectedError) - err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + err := EnterOrganisationName(template.Execute, nil, nil)(testAppData, w, r) resp := w.Result() assert.Equal(t, expectedError, err) @@ -58,9 +61,40 @@ func TestPostEnterOrganisationName(t *testing.T) { organisationStore := newMockOrganisationStore(t) organisationStore.EXPECT(). Create(r.Context(), "My organisation"). + Return(&actor.Organisation{ID: "org-id"}, nil) + + sessionStore := newMockSessionStore(t) + + session := sessions.NewSession(sessionStore, "session") + session.Options = &sessions.Options{ + Path: "/", + MaxAge: 86400, + SameSite: http.SameSiteLaxMode, + HttpOnly: true, + Secure: true, + } + session.Values = map[any]any{"session": &sesh.LoginSession{ + IDToken: "id-token", + Sub: "random", + Email: "name@example.com", + }} + + sessionStore.EXPECT(). + Get(r, "session"). + Return(session, nil) + + session.Values = map[any]any{"session": &sesh.LoginSession{ + IDToken: "id-token", + Sub: "random", + Email: "name@example.com", + OrganisationID: "org-id", + }} + + sessionStore.EXPECT(). + Save(r, w, session). Return(nil) - err := EnterOrganisationName(nil, organisationStore)(testAppData, w, r) + err := EnterOrganisationName(nil, organisationStore, sessionStore)(testAppData, w, r) resp := w.Result() assert.Nil(t, err) @@ -68,6 +102,70 @@ func TestPostEnterOrganisationName(t *testing.T) { assert.Equal(t, page.Paths.Supporter.OrganisationCreated.Format(), resp.Header.Get("Location")) } +func TestPostEnterOrganisationNameWhenSessionStoreSaveError(t *testing.T) { + form := url.Values{"name": {"My organisation"}} + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + Create(r.Context(), mock.Anything). + Return(&actor.Organisation{}, nil) + + sessionStore := newMockSessionStore(t) + + session := sessions.NewSession(sessionStore, "session") + session.Values = map[any]any{"session": &sesh.LoginSession{ + Sub: "random", + }} + + sessionStore.EXPECT(). + Get(r, mock.Anything). + Return(session, nil) + sessionStore.EXPECT(). + Save(r, w, mock.Anything). + Return(expectedError) + + err := EnterOrganisationName(nil, organisationStore, sessionStore)(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostEnterOrganisationNameWhenSessionStoreGetError(t *testing.T) { + form := url.Values{"name": {"My organisation"}} + + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + Create(r.Context(), mock.Anything). + Return(&actor.Organisation{}, nil) + + sessionStore := newMockSessionStore(t) + + session := sessions.NewSession(sessionStore, "session") + session.Values = map[any]any{"session": &sesh.LoginSession{ + Sub: "random", + }} + + sessionStore.EXPECT(). + Get(r, mock.Anything). + Return(nil, expectedError) + + err := EnterOrganisationName(nil, organisationStore, sessionStore)(testAppData, w, r) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.Supporter.Start.Format(), resp.Header.Get("Location")) +} + func TestPostEnterOrganisationNameWhenValidationError(t *testing.T) { w := httptest.NewRecorder() form := url.Values{} @@ -86,7 +184,7 @@ func TestPostEnterOrganisationNameWhenValidationError(t *testing.T) { })). Return(nil) - err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + err := EnterOrganisationName(template.Execute, nil, nil)(testAppData, w, r) resp := w.Result() assert.Nil(t, err) @@ -106,9 +204,9 @@ func TestPostEnterOrganisationNameWhenOrganisationStoreErrors(t *testing.T) { organisationStore := newMockOrganisationStore(t) organisationStore.EXPECT(). Create(r.Context(), mock.Anything). - Return(expectedError) + Return(nil, expectedError) - err := EnterOrganisationName(nil, organisationStore)(testAppData, w, r) + err := EnterOrganisationName(nil, organisationStore, nil)(testAppData, w, r) resp := w.Result() assert.Equal(t, expectedError, err) diff --git a/internal/page/supporter/login_callback.go b/internal/page/supporter/login_callback.go index 0ec726fda7..27220618d1 100644 --- a/internal/page/supporter/login_callback.go +++ b/internal/page/supporter/login_callback.go @@ -35,21 +35,32 @@ func LoginCallback(oneLoginClient LoginCallbackOneLoginClient, sessionStore sesh session := &sesh.LoginSession{ IDToken: idToken, - Sub: userInfo.Sub, + Sub: "supporter-" + userInfo.Sub, Email: userInfo.Email, } - if err := sesh.SetLoginSession(sessionStore, r, w, session); err != nil { - return err - } - ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: session.SessionID()}) - _, err = organisationStore.Get(ctx) + organisation, err := organisationStore.Get(ctx) if err == nil { + session.OrganisationID = organisation.ID + session.OrganisationName = organisation.Name + if err := sesh.SetLoginSession(sessionStore, r, w, session); err != nil { + return err + } + return page.Paths.Supporter.Dashboard.Redirect(w, r, appData) } - if !errors.Is(err, dynamo.NotFoundError{}) { + + if errors.Is(err, dynamo.NotFoundError{}) { + if err := sesh.SetLoginSession(sessionStore, r, w, &sesh.LoginSession{ + IDToken: idToken, + Sub: "supporter-" + userInfo.Sub, + Email: userInfo.Email, + }); err != nil { + return err + } + } else { return err } diff --git a/internal/page/supporter/login_callback_test.go b/internal/page/supporter/login_callback_test.go index 31a094823b..313eeefc31 100644 --- a/internal/page/supporter/login_callback_test.go +++ b/internal/page/supporter/login_callback_test.go @@ -20,17 +20,35 @@ func TestLoginCallback(t *testing.T) { getError error redirect string expectedError error + loginSession *sesh.LoginSession }{ "no organisation": { getError: dynamo.NotFoundError{}, redirect: page.Paths.Supporter.EnterOrganisationName.Format(), + loginSession: &sesh.LoginSession{ + IDToken: "id-token", + Sub: "supporter-random", + Email: "name@example.com", + }, }, "has organisation": { redirect: page.Paths.Supporter.Dashboard.Format(), + loginSession: &sesh.LoginSession{ + IDToken: "id-token", + Sub: "supporter-random", + Email: "name@example.com", + OrganisationID: "org-id", + OrganisationName: "org name", + }, }, "error getting organisation": { getError: expectedError, expectedError: expectedError, + loginSession: &sesh.LoginSession{ + IDToken: "id-token", + Sub: "supporter-random", + Email: "name@example.com", + }, }, } @@ -40,12 +58,6 @@ func TestLoginCallback(t *testing.T) { w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodGet, "/?code=auth-code&state=my-state", nil) - loginSession := &sesh.LoginSession{ - IDToken: "id-token", - Sub: "random", - Email: "name@example.com", - } - client := newMockOneLoginClient(t) client.EXPECT(). Exchange(r.Context(), "auth-code", "my-nonce"). @@ -64,7 +76,7 @@ func TestLoginCallback(t *testing.T) { HttpOnly: true, Secure: true, } - session.Values = map[any]any{"session": loginSession} + session.Values = map[any]any{"session": tc.loginSession} sessionStore.EXPECT(). Get(r, "params"). @@ -78,14 +90,17 @@ func TestLoginCallback(t *testing.T) { }, }, }, nil) - sessionStore.EXPECT(). - Save(r, w, session). - Return(nil) + + if tc.expectedError == nil { + sessionStore.EXPECT(). + Save(r, w, session). + Return(nil) + } organisationStore := newMockOrganisationStore(t) organisationStore.EXPECT(). - Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: loginSession.SessionID()})). - Return(&actor.Organisation{}, tc.getError) + Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: tc.loginSession.SessionID()})). + Return(&actor.Organisation{ID: "org-id", Name: "org name"}, tc.getError) err := LoginCallback(client, sessionStore, organisationStore)(page.AppData{}, w, r) if tc.expectedError != nil { @@ -231,6 +246,11 @@ func TestLoginCallbackWhenSessionError(t *testing.T) { Save(r, w, mock.Anything). Return(expectedError) - err := LoginCallback(client, sessionStore, nil)(page.AppData{}, w, r) + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + Get(mock.Anything). + Return(&actor.Organisation{}, nil) + + err := LoginCallback(client, sessionStore, organisationStore)(page.AppData{}, w, r) assert.Equal(t, expectedError, err) } diff --git a/internal/page/supporter/mock_DonorStore_test.go b/internal/page/supporter/mock_DonorStore_test.go new file mode 100644 index 0000000000..7d7a85ca0c --- /dev/null +++ b/internal/page/supporter/mock_DonorStore_test.go @@ -0,0 +1,247 @@ +// Code generated by mockery v2.40.1. DO NOT EDIT. + +package supporter + +import ( + context "context" + + actor "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + + mock "github.com/stretchr/testify/mock" +) + +// mockDonorStore is an autogenerated mock type for the DonorStore type +type mockDonorStore struct { + mock.Mock +} + +type mockDonorStore_Expecter struct { + mock *mock.Mock +} + +func (_m *mockDonorStore) EXPECT() *mockDonorStore_Expecter { + return &mockDonorStore_Expecter{mock: &_m.Mock} +} + +// Delete provides a mock function with given fields: ctx +func (_m *mockDonorStore) Delete(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockDonorStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type mockDonorStore_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockDonorStore_Expecter) Delete(ctx interface{}) *mockDonorStore_Delete_Call { + return &mockDonorStore_Delete_Call{Call: _e.mock.On("Delete", ctx)} +} + +func (_c *mockDonorStore_Delete_Call) Run(run func(ctx context.Context)) *mockDonorStore_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockDonorStore_Delete_Call) Return(_a0 error) *mockDonorStore_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockDonorStore_Delete_Call) RunAndReturn(run func(context.Context) error) *mockDonorStore_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx +func (_m *mockDonorStore) Get(ctx context.Context) (*actor.DonorProvidedDetails, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *actor.DonorProvidedDetails + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*actor.DonorProvidedDetails, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *actor.DonorProvidedDetails); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*actor.DonorProvidedDetails) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockDonorStore_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type mockDonorStore_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockDonorStore_Expecter) Get(ctx interface{}) *mockDonorStore_Get_Call { + return &mockDonorStore_Get_Call{Call: _e.mock.On("Get", ctx)} +} + +func (_c *mockDonorStore_Get_Call) Run(run func(ctx context.Context)) *mockDonorStore_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockDonorStore_Get_Call) Return(_a0 *actor.DonorProvidedDetails, _a1 error) *mockDonorStore_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockDonorStore_Get_Call) RunAndReturn(run func(context.Context) (*actor.DonorProvidedDetails, error)) *mockDonorStore_Get_Call { + _c.Call.Return(run) + return _c +} + +// Latest provides a mock function with given fields: ctx +func (_m *mockDonorStore) Latest(ctx context.Context) (*actor.DonorProvidedDetails, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Latest") + } + + var r0 *actor.DonorProvidedDetails + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*actor.DonorProvidedDetails, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *actor.DonorProvidedDetails); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*actor.DonorProvidedDetails) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockDonorStore_Latest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Latest' +type mockDonorStore_Latest_Call struct { + *mock.Call +} + +// Latest is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockDonorStore_Expecter) Latest(ctx interface{}) *mockDonorStore_Latest_Call { + return &mockDonorStore_Latest_Call{Call: _e.mock.On("Latest", ctx)} +} + +func (_c *mockDonorStore_Latest_Call) Run(run func(ctx context.Context)) *mockDonorStore_Latest_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockDonorStore_Latest_Call) Return(_a0 *actor.DonorProvidedDetails, _a1 error) *mockDonorStore_Latest_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockDonorStore_Latest_Call) RunAndReturn(run func(context.Context) (*actor.DonorProvidedDetails, error)) *mockDonorStore_Latest_Call { + _c.Call.Return(run) + return _c +} + +// Put provides a mock function with given fields: ctx, donor +func (_m *mockDonorStore) Put(ctx context.Context, donor *actor.DonorProvidedDetails) error { + ret := _m.Called(ctx, donor) + + if len(ret) == 0 { + panic("no return value specified for Put") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *actor.DonorProvidedDetails) error); ok { + r0 = rf(ctx, donor) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockDonorStore_Put_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Put' +type mockDonorStore_Put_Call struct { + *mock.Call +} + +// Put is a helper method to define mock.On call +// - ctx context.Context +// - donor *actor.DonorProvidedDetails +func (_e *mockDonorStore_Expecter) Put(ctx interface{}, donor interface{}) *mockDonorStore_Put_Call { + return &mockDonorStore_Put_Call{Call: _e.mock.On("Put", ctx, donor)} +} + +func (_c *mockDonorStore_Put_Call) Run(run func(ctx context.Context, donor *actor.DonorProvidedDetails)) *mockDonorStore_Put_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*actor.DonorProvidedDetails)) + }) + return _c +} + +func (_c *mockDonorStore_Put_Call) Return(_a0 error) *mockDonorStore_Put_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockDonorStore_Put_Call) RunAndReturn(run func(context.Context, *actor.DonorProvidedDetails) error) *mockDonorStore_Put_Call { + _c.Call.Return(run) + return _c +} + +// newMockDonorStore creates a new instance of mockDonorStore. 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 newMockDonorStore(t interface { + mock.TestingT + Cleanup(func()) +}) *mockDonorStore { + mock := &mockDonorStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/page/supporter/mock_OrganisationStore_test.go b/internal/page/supporter/mock_OrganisationStore_test.go index c6e883033a..de7a18bf32 100644 --- a/internal/page/supporter/mock_OrganisationStore_test.go +++ b/internal/page/supporter/mock_OrganisationStore_test.go @@ -23,22 +23,34 @@ func (_m *mockOrganisationStore) EXPECT() *mockOrganisationStore_Expecter { return &mockOrganisationStore_Expecter{mock: &_m.Mock} } -// Create provides a mock function with given fields: _a0, _a1 -func (_m *mockOrganisationStore) Create(_a0 context.Context, _a1 string) error { - ret := _m.Called(_a0, _a1) +// Create provides a mock function with given fields: ctx, name +func (_m *mockOrganisationStore) Create(ctx context.Context, name string) (*actor.Organisation, error) { + ret := _m.Called(ctx, name) if len(ret) == 0 { panic("no return value specified for Create") } - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(_a0, _a1) + var r0 *actor.Organisation + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*actor.Organisation, error)); ok { + return rf(ctx, name) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *actor.Organisation); ok { + r0 = rf(ctx, name) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*actor.Organisation) + } } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // mockOrganisationStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' @@ -47,32 +59,91 @@ type mockOrganisationStore_Create_Call struct { } // Create is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 string -func (_e *mockOrganisationStore_Expecter) Create(_a0 interface{}, _a1 interface{}) *mockOrganisationStore_Create_Call { - return &mockOrganisationStore_Create_Call{Call: _e.mock.On("Create", _a0, _a1)} +// - ctx context.Context +// - name string +func (_e *mockOrganisationStore_Expecter) Create(ctx interface{}, name interface{}) *mockOrganisationStore_Create_Call { + return &mockOrganisationStore_Create_Call{Call: _e.mock.On("Create", ctx, name)} } -func (_c *mockOrganisationStore_Create_Call) Run(run func(_a0 context.Context, _a1 string)) *mockOrganisationStore_Create_Call { +func (_c *mockOrganisationStore_Create_Call) Run(run func(ctx context.Context, name string)) *mockOrganisationStore_Create_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(string)) }) return _c } -func (_c *mockOrganisationStore_Create_Call) Return(_a0 error) *mockOrganisationStore_Create_Call { - _c.Call.Return(_a0) +func (_c *mockOrganisationStore_Create_Call) Return(_a0 *actor.Organisation, _a1 error) *mockOrganisationStore_Create_Call { + _c.Call.Return(_a0, _a1) return _c } -func (_c *mockOrganisationStore_Create_Call) RunAndReturn(run func(context.Context, string) error) *mockOrganisationStore_Create_Call { +func (_c *mockOrganisationStore_Create_Call) RunAndReturn(run func(context.Context, string) (*actor.Organisation, error)) *mockOrganisationStore_Create_Call { _c.Call.Return(run) return _c } -// CreateMemberInvite provides a mock function with given fields: _a0, _a1, _a2, _a3 -func (_m *mockOrganisationStore) CreateMemberInvite(_a0 context.Context, _a1 *actor.Organisation, _a2 string, _a3 string) error { - ret := _m.Called(_a0, _a1, _a2, _a3) +// CreateLPA provides a mock function with given fields: ctx, organisationID +func (_m *mockOrganisationStore) CreateLPA(ctx context.Context, organisationID string) (*actor.DonorProvidedDetails, error) { + ret := _m.Called(ctx, organisationID) + + if len(ret) == 0 { + panic("no return value specified for CreateLPA") + } + + var r0 *actor.DonorProvidedDetails + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*actor.DonorProvidedDetails, error)); ok { + return rf(ctx, organisationID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *actor.DonorProvidedDetails); ok { + r0 = rf(ctx, organisationID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*actor.DonorProvidedDetails) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, organisationID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockOrganisationStore_CreateLPA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateLPA' +type mockOrganisationStore_CreateLPA_Call struct { + *mock.Call +} + +// CreateLPA is a helper method to define mock.On call +// - ctx context.Context +// - organisationID string +func (_e *mockOrganisationStore_Expecter) CreateLPA(ctx interface{}, organisationID interface{}) *mockOrganisationStore_CreateLPA_Call { + return &mockOrganisationStore_CreateLPA_Call{Call: _e.mock.On("CreateLPA", ctx, organisationID)} +} + +func (_c *mockOrganisationStore_CreateLPA_Call) Run(run func(ctx context.Context, organisationID string)) *mockOrganisationStore_CreateLPA_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *mockOrganisationStore_CreateLPA_Call) Return(_a0 *actor.DonorProvidedDetails, _a1 error) *mockOrganisationStore_CreateLPA_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockOrganisationStore_CreateLPA_Call) RunAndReturn(run func(context.Context, string) (*actor.DonorProvidedDetails, error)) *mockOrganisationStore_CreateLPA_Call { + _c.Call.Return(run) + return _c +} + +// CreateMemberInvite provides a mock function with given fields: ctx, organisation, email, code +func (_m *mockOrganisationStore) CreateMemberInvite(ctx context.Context, organisation *actor.Organisation, email string, code string) error { + ret := _m.Called(ctx, organisation, email, code) if len(ret) == 0 { panic("no return value specified for CreateMemberInvite") @@ -80,7 +151,7 @@ func (_m *mockOrganisationStore) CreateMemberInvite(_a0 context.Context, _a1 *ac var r0 error if rf, ok := ret.Get(0).(func(context.Context, *actor.Organisation, string, string) error); ok { - r0 = rf(_a0, _a1, _a2, _a3) + r0 = rf(ctx, organisation, email, code) } else { r0 = ret.Error(0) } @@ -94,15 +165,15 @@ type mockOrganisationStore_CreateMemberInvite_Call struct { } // CreateMemberInvite is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 *actor.Organisation -// - _a2 string -// - _a3 string -func (_e *mockOrganisationStore_Expecter) CreateMemberInvite(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *mockOrganisationStore_CreateMemberInvite_Call { - return &mockOrganisationStore_CreateMemberInvite_Call{Call: _e.mock.On("CreateMemberInvite", _a0, _a1, _a2, _a3)} +// - ctx context.Context +// - organisation *actor.Organisation +// - email string +// - code string +func (_e *mockOrganisationStore_Expecter) CreateMemberInvite(ctx interface{}, organisation interface{}, email interface{}, code interface{}) *mockOrganisationStore_CreateMemberInvite_Call { + return &mockOrganisationStore_CreateMemberInvite_Call{Call: _e.mock.On("CreateMemberInvite", ctx, organisation, email, code)} } -func (_c *mockOrganisationStore_CreateMemberInvite_Call) Run(run func(_a0 context.Context, _a1 *actor.Organisation, _a2 string, _a3 string)) *mockOrganisationStore_CreateMemberInvite_Call { +func (_c *mockOrganisationStore_CreateMemberInvite_Call) Run(run func(ctx context.Context, organisation *actor.Organisation, email string, code string)) *mockOrganisationStore_CreateMemberInvite_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context), args[1].(*actor.Organisation), args[2].(string), args[3].(string)) }) @@ -119,9 +190,9 @@ func (_c *mockOrganisationStore_CreateMemberInvite_Call) RunAndReturn(run func(c return _c } -// Get provides a mock function with given fields: _a0 -func (_m *mockOrganisationStore) Get(_a0 context.Context) (*actor.Organisation, error) { - ret := _m.Called(_a0) +// Get provides a mock function with given fields: ctx +func (_m *mockOrganisationStore) Get(ctx context.Context) (*actor.Organisation, error) { + ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for Get") @@ -130,10 +201,10 @@ func (_m *mockOrganisationStore) Get(_a0 context.Context) (*actor.Organisation, var r0 *actor.Organisation var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*actor.Organisation, error)); ok { - return rf(_a0) + return rf(ctx) } if rf, ok := ret.Get(0).(func(context.Context) *actor.Organisation); ok { - r0 = rf(_a0) + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*actor.Organisation) @@ -141,7 +212,7 @@ func (_m *mockOrganisationStore) Get(_a0 context.Context) (*actor.Organisation, } if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -155,12 +226,12 @@ type mockOrganisationStore_Get_Call struct { } // Get is a helper method to define mock.On call -// - _a0 context.Context -func (_e *mockOrganisationStore_Expecter) Get(_a0 interface{}) *mockOrganisationStore_Get_Call { - return &mockOrganisationStore_Get_Call{Call: _e.mock.On("Get", _a0)} +// - ctx context.Context +func (_e *mockOrganisationStore_Expecter) Get(ctx interface{}) *mockOrganisationStore_Get_Call { + return &mockOrganisationStore_Get_Call{Call: _e.mock.On("Get", ctx)} } -func (_c *mockOrganisationStore_Get_Call) Run(run func(_a0 context.Context)) *mockOrganisationStore_Get_Call { +func (_c *mockOrganisationStore_Get_Call) Run(run func(ctx context.Context)) *mockOrganisationStore_Get_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(context.Context)) }) diff --git a/internal/page/supporter/register.go b/internal/page/supporter/register.go index 9c62f5e100..0686aad4bd 100644 --- a/internal/page/supporter/register.go +++ b/internal/page/supporter/register.go @@ -16,10 +16,11 @@ import ( ) type OrganisationStore interface { - Create(context.Context, string) error - CreateMemberInvite(context.Context, *actor.Organisation, string, string) error - Get(context.Context) (*actor.Organisation, error) - Put(context.Context, *actor.Organisation) error + Create(ctx context.Context, name string) (*actor.Organisation, error) + CreateMemberInvite(ctx context.Context, organisation *actor.Organisation, email, code string) error + Get(ctx context.Context) (*actor.Organisation, error) + CreateLPA(ctx context.Context, organisationID string) (*actor.DonorProvidedDetails, error) + Put(ctx context.Context, organisation *actor.Organisation) error } type OneLoginClient interface { @@ -62,7 +63,7 @@ func Register( handleRoot(paths.LoginCallback, page.None, LoginCallback(oneLoginClient, sessionStore, organisationStore)) handleRoot(paths.EnterOrganisationName, page.RequireSession, - EnterOrganisationName(tmpls.Get("enter_organisation_name.gohtml"), organisationStore)) + EnterOrganisationName(tmpls.Get("enter_organisation_name.gohtml"), organisationStore, sessionStore)) supporterMux := http.NewServeMux() rootMux.Handle("/supporter/", http.StripPrefix("/supporter", supporterMux)) @@ -75,8 +76,7 @@ func Register( handleWithSupporter(paths.OrganisationCreated, OrganisationCreated(tmpls.Get("organisation_created.gohtml"))) handleWithSupporter(paths.Dashboard, - Guidance(tmpls.Get("dashboard.gohtml"))) - + Dashboard(tmpls.Get("dashboard.gohtml"), organisationStore)) handleWithSupporter(paths.InviteMember, InviteMember(tmpls.Get("invite_member.gohtml"), organisationStore, notifyClient, random.String)) handleWithSupporter(paths.InviteMemberConfirmation, @@ -105,6 +105,7 @@ func makeHandle(mux *http.ServeMux, store sesh.Store, errorHandler page.ErrorHan } appData.SessionID = session.SessionID() + ctx = page.ContextWithSessionData(ctx, &page.SessionData{SessionID: appData.SessionID}) } @@ -118,17 +119,24 @@ func makeHandle(mux *http.ServeMux, store sesh.Store, errorHandler page.ErrorHan func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page.ErrorHandler, organisationStore OrganisationStore) func(page.SupporterPath, Handler) { return func(path page.SupporterPath, h Handler) { mux.HandleFunc(path.String(), func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - appData := page.AppDataFromContext(ctx) - - session, err := sesh.Login(store, r) + loginSession, err := sesh.Login(store, r) if err != nil { http.Redirect(w, r, page.Paths.Supporter.Start.Format(), http.StatusFound) return } - appData.SessionID = session.SessionID() - ctx = page.ContextWithSessionData(ctx, &page.SessionData{SessionID: appData.SessionID}) + ctx := r.Context() + + appData := page.AppDataFromContext(ctx) + appData.SessionID = loginSession.SessionID() + + sessionData, err := page.SessionDataFromContext(ctx) + if err == nil { + sessionData.SessionID = appData.SessionID + ctx = page.ContextWithSessionData(ctx, sessionData) + } else { + ctx = page.ContextWithSessionData(ctx, &page.SessionData{SessionID: appData.SessionID}) + } organisation, err := organisationStore.Get(ctx) if err != nil { @@ -141,7 +149,9 @@ func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page appData.OrganisationName = organisation.Name appData.IsManageOrganisation = path.IsManageOrganisation() - if err := h(appData, w, r.WithContext(page.ContextWithAppData(ctx, appData)), organisation); err != nil { + ctx = page.ContextWithAppData(page.ContextWithSessionData(ctx, &page.SessionData{SessionID: appData.SessionID, OrganisationID: organisation.ID}), appData) + + if err := h(appData, w, r.WithContext(ctx), organisation); err != nil { errorHandler(w, r, err) } }) diff --git a/internal/sesh/sesh.go b/internal/sesh/sesh.go index 6d551a12f6..f37ac02bd2 100644 --- a/internal/sesh/sesh.go +++ b/internal/sesh/sesh.go @@ -107,9 +107,11 @@ func SetOneLogin(store sessions.Store, r *http.Request, w http.ResponseWriter, o } type LoginSession struct { - IDToken string - Sub string - Email string + IDToken string + Sub string + Email string + OrganisationID string + OrganisationName string } func (s LoginSession) SessionID() string { diff --git a/lang/cy.json b/lang/cy.json index 2d20ac1952..3c70baf835 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -816,7 +816,7 @@ "submittedToOpg": "Wedi’i chyflwyno i Swyddfa’r Gwarcheidwad Cyhoeddus", "readyToSign": "Yn barod i’w llofnodi", "lpaNumber": "Rhif LPA", - "makeNewLpa": "Gwneud LPA newydd", + "makeNewLastingPowerOfAttorney": "Gwneud LPA newydd", "startNow": "Dechrau nawr", "youCanIncludeRestrictionsAboutLst": "Gallwch hefyd gynnwys unrhyw gyfyngiadau ac amodau sydd gennych am thriniaeth cynnal bywyd.", "youMustChooseANewCertificateProvider": "Rhaid i chi ddewis darparwr tystysgrif gwahanol", diff --git a/lang/en.json b/lang/en.json index 2e25f6f73b..08e09d7769 100644 --- a/lang/en.json +++ b/lang/en.json @@ -763,7 +763,7 @@ "submittedToOpg": "Submitted to OPG", "readyToSign": "Ready to sign", "lpaNumber": "LPA number", - "makeNewLpa": "Make a new lasting power of attorney (LPA)", + "makeNewLastingPowerOfAttorney": "Make a new lasting power of attorney (LPA)", "startNow": "Start now", "youCanIncludeRestrictionsAboutLst": "You can also include any restrictions and conditions you have about life-sustaining treatment.", "youMustChooseANewCertificateProvider": "You must choose a different certificate provider", diff --git a/web/assets/scss/main.scss b/web/assets/scss/main.scss index 6c8e5a74d2..87ea20c838 100644 --- a/web/assets/scss/main.scss +++ b/web/assets/scss/main.scss @@ -102,6 +102,7 @@ body:not(.js-enabled) .govuk-back-link { @include govuk-media-query($from: tablet) { @include govuk-responsive-padding(4, "bottom"); } + color: govuk-colour("black"); } .app-padding-top-7-non-mobile { diff --git a/web/template/dashboard.gohtml b/web/template/dashboard.gohtml index 31e793c25a..9aabdb88a8 100644 --- a/web/template/dashboard.gohtml +++ b/web/template/dashboard.gohtml @@ -152,7 +152,7 @@

{{ tr .App "manageYourLpas" }}

-

{{ tr .App "makeNewLpa" }}

+

{{ tr .App "makeNewLastingPowerOfAttorney" }}

diff --git a/web/template/supporter/dashboard.gohtml b/web/template/supporter/dashboard.gohtml index 00ee8c901b..fde363274a 100644 --- a/web/template/supporter/dashboard.gohtml +++ b/web/template/supporter/dashboard.gohtml @@ -3,11 +3,17 @@ {{ define "pageTitle" }}{{ tr .App "dashboard" }}{{ end }} {{ define "main" }} -
-
-

{{ tr .App "dashboard" }}

+
+
+ +

{{ template "pageTitle" . }}

- {{ tr .App "inviteTeamMember" }} +
+ + {{ tr .App "inviteTeamMember" }} +
+ {{ template "csrf-field" . }} + +
-
{{ end }}