diff --git a/cypress/e2e/supporter/edit-member.cy.js b/cypress/e2e/supporter/edit-member.cy.js index cedef0b7bf..b06820e6ea 100644 --- a/cypress/e2e/supporter/edit-member.cy.js +++ b/cypress/e2e/supporter/edit-member.cy.js @@ -1,60 +1,111 @@ describe('Edit member', () => { - beforeEach(() => { - cy.visit("/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&members=1"); + describe('admin', () => { + it('can edit a team members name', () => { + cy.visit("/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&members=1&permission=admin"); - cy.url().should('contain', "/manage-organisation/manage-team-members"); - cy.contains('a', "Alice Moxom").click() + cy.url().should('contain', "/manage-organisation/manage-team-members"); + cy.contains('a', "Alice Moxom").click() - cy.url().should('contain', "/manage-organisation/manage-team-members/edit-team-member"); - }); + cy.url().should('contain', "/manage-organisation/manage-team-members/edit-team-member"); - it('can edit a team members name', () => { - cy.checkA11yApp(); + cy.checkA11yApp(); - cy.get('#f-first-names').clear().type('John'); - cy.get('#f-last-name').clear().type('Doe'); + cy.get('#f-first-names').clear().type('John'); + cy.get('#f-last-name').clear().type('Doe'); - cy.contains('button', "Save").click() + cy.contains('button', "Save").click() - cy.url().should('contain', "/manage-organisation/manage-team-members"); + cy.url().should('contain', "/manage-organisation/manage-team-members"); - cy.contains('Team member’s name updated to John Doe'); - cy.contains('a', "John Doe") - }) + cy.checkA11yApp(); + + cy.contains('Team member’s name updated to John Doe'); + cy.contains('a', "John Doe") + }) + + it('can edit own name', () => { + cy.visit("/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&members=1&asMember=alice-moxom@example.org&permission=admin"); + + cy.url().should('contain', "/manage-organisation/manage-team-members"); + cy.contains('a', "Alice Moxom").click() + + cy.url().should('contain', "/manage-organisation/manage-team-members/edit-team-member"); - it('can edit own name', () => { - // TODO update to a full test when admins can set their own names during org creation - cy.visit("/supporter/manage-organisation/manage-team-members?nameUpdated=John+Doe&selfUpdated=1"); + cy.checkA11yApp(); - cy.contains('Your name has been updated to John Doe'); + cy.get('#f-first-names').clear().type('John'); + cy.get('#f-last-name').clear().type('Doe'); + + cy.contains('button', "Save").click() + + cy.url().should('contain', "/manage-organisation/manage-team-members"); + + cy.checkA11yApp(); + + cy.contains('Your name has been updated to John Doe'); + cy.contains('a', "John Doe") + }) }) - it('errors when empty', () => { - cy.get('#f-first-names').clear(); - cy.get('#f-last-name').clear(); + describe('non-admin', () => { + it('can edit own name', () => { + cy.visit("/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&members=1&asMember=alice-moxom@example.org"); + + cy.contains('a', 'Manage your details').click(); + cy.url().should('contain', "/manage-organisation/manage-team-members/edit-team-member"); + + cy.checkA11yApp(); + cy.contains('Your name'); - cy.contains('button', "Save").click() + cy.get('#f-first-names').clear ().type('John'); + cy.get('#f-last-name').clear().type('Doe'); - cy.checkA11yApp(); + cy.contains('button', "Save").click() - cy.get('.govuk-error-summary').within(() => { - cy.contains('Enter first names'); - cy.contains('Enter last name'); + cy.url().should('contain', "/dashboard"); + + cy.checkA11yApp(); + cy.contains('Your name has been updated to John Doe'); + }) + }) + + describe('errors', () => { + beforeEach(() => { + cy.visit("/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&members=1"); + + cy.url().should('contain', "/manage-organisation/manage-team-members"); + cy.contains('a', "Alice Moxom").click() + + cy.url().should('contain', "/manage-organisation/manage-team-members/edit-team-member"); }); - cy.contains('[for=f-first-names] + .govuk-error-message', 'Enter first names'); - cy.contains('[for=f-last-name] + .govuk-error-message', 'Enter last name'); - }); + it('errors when empty', () => { + cy.get('#f-first-names').clear(); + cy.get('#f-last-name').clear(); - it('errors when names too long', () => { - cy.get('#f-first-names').invoke('val', 'a '.repeat(54)); - cy.get('#f-last-name').invoke('val', 'b '.repeat(62)); + cy.contains('button', "Save").click() - cy.contains('button', "Save").click() + cy.checkA11yApp(); - cy.checkA11yApp(); + cy.get('.govuk-error-summary').within(() => { + cy.contains('Enter first names'); + cy.contains('Enter last name'); + }); - cy.contains('[for=f-first-names] + .govuk-error-message', 'First names must be 53 characters or less'); - cy.contains('[for=f-last-name] + .govuk-error-message', 'Last name must be 61 characters or less'); + cy.contains('[for=f-first-names] + .govuk-error-message', 'Enter first names'); + cy.contains('[for=f-last-name] + .govuk-error-message', 'Enter last name'); + }); + + it('errors when names too long', () => { + cy.get('#f-first-names').invoke('val', 'a '.repeat(54)); + cy.get('#f-last-name').invoke('val', 'b '.repeat(62)); + + cy.contains('button', "Save").click() + + cy.checkA11yApp(); + + cy.contains('[for=f-first-names] + .govuk-error-message', 'First names must be 53 characters or less'); + cy.contains('[for=f-last-name] + .govuk-error-message', 'Last name must be 61 characters or less'); + }); }); }) diff --git a/cypress/e2e/supporter/manage-team-members.cy.js b/cypress/e2e/supporter/manage-team-members.cy.js index 77b32868f8..f6ddd11f8b 100644 --- a/cypress/e2e/supporter/manage-team-members.cy.js +++ b/cypress/e2e/supporter/manage-team-members.cy.js @@ -1,6 +1,6 @@ describe('Organisation details', () => { it('shows invited and joined members', () => { - cy.visit('/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&invitedMembers=2&members=2'); + cy.visit('/fixtures/supporter?organisation=1&redirect=/manage-organisation/manage-team-members&invitedMembers=2&members=2&permission=admin'); cy.checkA11yApp(); cy.contains("a", "Manage team members").click() diff --git a/internal/app/organisation_store.go b/internal/app/organisation_store.go index 519e076150..cbe2d6e71f 100644 --- a/internal/app/organisation_store.go +++ b/internal/app/organisation_store.go @@ -57,11 +57,12 @@ func (s *organisationStore) Create(ctx context.Context, name string) (*actor.Org } member := &actor.Member{ - PK: organisationKey(organisationID), - SK: memberKey(data.SessionID), - ID: s.uuidString(), - Email: data.Email, - CreatedAt: s.now(), + PK: organisationKey(organisationID), + SK: memberKey(data.SessionID), + ID: s.uuidString(), + Email: data.Email, + CreatedAt: s.now(), + Permission: actor.Admin, } if err := s.dynamoClient.Create(ctx, member); err != nil { diff --git a/internal/app/organisation_store_test.go b/internal/app/organisation_store_test.go index fe4e7fecbf..7c7cdfe93e 100644 --- a/internal/app/organisation_store_test.go +++ b/internal/app/organisation_store_test.go @@ -27,11 +27,12 @@ func TestOrganisationStoreCreate(t *testing.T) { dynamoClient.EXPECT(). Create(ctx, &actor.Member{ - PK: "ORGANISATION#a-uuid", - SK: "MEMBER#an-id", - ID: "a-uuid", - CreatedAt: testNow, - Email: "a@example.org", + PK: "ORGANISATION#a-uuid", + SK: "MEMBER#an-id", + ID: "a-uuid", + CreatedAt: testNow, + Email: "a@example.org", + Permission: actor.Admin, }). Return(nil). Once() diff --git a/internal/page/app_data.go b/internal/page/app_data.go index 88c73bcee8..54783dce8e 100644 --- a/internal/page/app_data.go +++ b/internal/page/app_data.go @@ -26,6 +26,8 @@ type AppData struct { OrganisationName string IsManageOrganisation bool LoginSessionEmail string + Permission actor.Permission + LoggedInSupporterID string } func (d AppData) Redirect(w http.ResponseWriter, r *http.Request, url string) error { @@ -58,3 +60,7 @@ func (d AppData) IsReplacementAttorney() bool { func (d AppData) IsTrustCorporation() bool { return d.ActorType == actor.TypeTrustCorporation || d.ActorType == actor.TypeReplacementTrustCorporation } + +func (d AppData) IsAdmin() bool { + return d.Permission.IsAdmin() +} diff --git a/internal/page/app_data_test.go b/internal/page/app_data_test.go index a5e9c2ee6a..f3c1ed67d0 100644 --- a/internal/page/app_data_test.go +++ b/internal/page/app_data_test.go @@ -71,3 +71,8 @@ func TestIsTrustCorporation(t *testing.T) { assert.False(t, AppData{ActorType: actor.TypeAttorney, AttorneyUID: actoruid.New()}.IsTrustCorporation()) assert.False(t, AppData{ActorType: actor.TypeReplacementAttorney, AttorneyUID: actoruid.New()}.IsTrustCorporation()) } + +func TestAppDataIsAdmin(t *testing.T) { + assert.True(t, AppData{Permission: actor.Admin}.IsAdmin()) + assert.False(t, AppData{}.IsAdmin()) +} diff --git a/internal/page/fixtures/supporter.go b/internal/page/fixtures/supporter.go index 98ea0001ff..047718b688 100644 --- a/internal/page/fixtures/supporter.go +++ b/internal/page/fixtures/supporter.go @@ -33,16 +33,18 @@ func Supporter(sessionStore sesh.Store, organisationStore OrganisationStore, don members = r.FormValue("members") organisation = r.FormValue("organisation") redirect = r.FormValue("redirect") + asMember = r.FormValue("asMember") + permission = r.FormValue("permission") supporterSub = random.String(16) supporterSessionID = base64.StdEncoding.EncodeToString([]byte(supporterSub)) - ctx = page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: supporterSessionID, Email: testEmail}) + supporterCtx = page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: supporterSessionID, Email: testEmail}) ) loginSession := &sesh.LoginSession{Sub: supporterSub, Email: testEmail} if organisation == "1" { - org, err := organisationStore.Create(ctx, random.String(12)) + org, err := organisationStore.Create(supporterCtx, random.String(12)) if err != nil { return err } @@ -87,29 +89,50 @@ func Supporter(sessionStore sesh.Store, organisationStore OrganisationStore, don if members != "" { n, err := strconv.Atoi(members) + if err != nil { + return fmt.Errorf("members should be a number") + } + + memberEmailSub := make(map[string]string) + + permission, err := actor.ParsePermission(permission) + if err != nil { + permission = actor.None + } for i, member := range orgMemberNames { if i == n { break } + email := strings.ToLower(fmt.Sprintf("%s-%s@example.org", member.Firstnames, member.Lastname)) + sub := []byte(random.String(16)) + memberCtx := page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: base64.StdEncoding.EncodeToString(sub), Email: email}) + if err = memberStore.Create( - page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: random.String(12)}), + memberCtx, &actor.MemberInvite{ PK: random.String(12), SK: random.String(12), CreatedAt: time.Now(), UpdatedAt: time.Now(), OrganisationID: org.ID, - Email: strings.ToLower(fmt.Sprintf("%s-%s@example.org", member.Firstnames, member.Lastname)), + Email: email, FirstNames: member.Firstnames, LastName: member.Lastname, - Permission: actor.Admin, + Permission: permission, ReferenceNumber: random.String(12), }, ); err != nil { return err } + + memberEmailSub[email] = string(sub) + } + + if sub, found := memberEmailSub[asMember]; found { + loginSession.Email = asMember + loginSession.Sub = sub } } } diff --git a/internal/page/paths.go b/internal/page/paths.go index 26461cb9cf..0a49c83463 100644 --- a/internal/page/paths.go +++ b/internal/page/paths.go @@ -184,7 +184,10 @@ func (p SupporterPath) RedirectQuery(w http.ResponseWriter, r *http.Request, app } func (p SupporterPath) IsManageOrganisation() bool { - return p == Paths.Supporter.OrganisationDetails || p == Paths.Supporter.EditOrganisationName || p == Paths.Supporter.ManageTeamMembers + return p == Paths.Supporter.OrganisationDetails || + p == Paths.Supporter.EditOrganisationName || + p == Paths.Supporter.ManageTeamMembers || + p == Paths.Supporter.EditMember } type AttorneyPaths struct { diff --git a/internal/page/paths_test.go b/internal/page/paths_test.go index cc764a076b..ee270570a4 100644 --- a/internal/page/paths_test.go +++ b/internal/page/paths_test.go @@ -204,7 +204,11 @@ func TestSupporterPathRedirect(t *testing.T) { func TestSupporterPathIsManageOrganisation(t *testing.T) { assert.False(t, Paths.Supporter.Dashboard.IsManageOrganisation()) + assert.True(t, Paths.Supporter.OrganisationDetails.IsManageOrganisation()) + assert.True(t, Paths.Supporter.EditOrganisationName.IsManageOrganisation()) + assert.True(t, Paths.Supporter.ManageTeamMembers.IsManageOrganisation()) + assert.True(t, Paths.Supporter.EditMember.IsManageOrganisation()) } func TestCanGoTo(t *testing.T) { diff --git a/internal/page/supporter/dashboard.go b/internal/page/supporter/dashboard.go index 2c02873efe..9ab28ac62e 100644 --- a/internal/page/supporter/dashboard.go +++ b/internal/page/supporter/dashboard.go @@ -2,6 +2,7 @@ package supporter import ( "net/http" + "net/url" "github.com/ministryofjustice/opg-go-common/template" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -13,6 +14,7 @@ type dashboardData struct { App page.AppData Errors validation.List Donors []actor.DonorProvidedDetails + Query url.Values } func Dashboard(tmpl template.Template, organisationStore OrganisationStore) Handler { @@ -22,6 +24,6 @@ func Dashboard(tmpl template.Template, organisationStore OrganisationStore) Hand return err } - return tmpl(w, &dashboardData{App: appData, Donors: donors}) + return tmpl(w, &dashboardData{App: appData, Donors: donors, Query: r.URL.Query()}) } } diff --git a/internal/page/supporter/dashboard_test.go b/internal/page/supporter/dashboard_test.go index 5316f8c366..a53dce2a02 100644 --- a/internal/page/supporter/dashboard_test.go +++ b/internal/page/supporter/dashboard_test.go @@ -3,6 +3,7 @@ package supporter import ( "net/http" "net/http/httptest" + "net/url" "testing" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -11,7 +12,7 @@ import ( func TestGetDashboard(t *testing.T) { w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/", nil) + r, _ := http.NewRequest(http.MethodGet, "/?a=b", nil) donors := []actor.DonorProvidedDetails{{LpaID: "abc"}} @@ -25,6 +26,7 @@ func TestGetDashboard(t *testing.T) { Execute(w, &dashboardData{ App: testAppData, Donors: donors, + Query: url.Values{"a": {"b"}}, }). Return(expectedError) diff --git a/internal/page/supporter/edit_member.go b/internal/page/supporter/edit_member.go index e6310207b3..cbe398aaa7 100644 --- a/internal/page/supporter/edit_member.go +++ b/internal/page/supporter/edit_member.go @@ -54,7 +54,12 @@ func EditMember(tmpl template.Template, memberStore MemberStore) Handler { return err } - return page.Paths.Supporter.ManageTeamMembers.RedirectQuery(w, r, appData, query) + redirect := page.Paths.Supporter.ManageTeamMembers + if !appData.IsAdmin() { + redirect = page.Paths.Supporter.Dashboard + } + + return redirect.RedirectQuery(w, r, appData, query) } } diff --git a/internal/page/supporter/edit_member_test.go b/internal/page/supporter/edit_member_test.go index 4190913888..3e62106834 100644 --- a/internal/page/supporter/edit_member_test.go +++ b/internal/page/supporter/edit_member_test.go @@ -87,45 +87,87 @@ func TestGetEditMemberWhenTemplateError(t *testing.T) { func TestPostEditMember(t *testing.T) { testcases := map[string]struct { - form url.Values - expectedQuery string - expectedMember *actor.Member - memberEmail string + form url.Values + expectedRedirect string + expectedMember *actor.Member + memberEmail string + userPermission actor.Permission }{ - "Team member name updated": { + "As Admin: Team member name updated": { form: url.Values{ "first-names": {"c"}, "last-name": {"d"}, }, - expectedQuery: "?nameUpdated=c+d", + expectedRedirect: page.Paths.Supporter.ManageTeamMembers.Format() + "?nameUpdated=c+d", expectedMember: &actor.Member{ FirstNames: "c", LastName: "d", }, + userPermission: actor.Admin, }, - "Self name updated": { + "As Admin: Self name updated": { form: url.Values{ "first-names": {"c"}, "last-name": {"d"}, }, - expectedQuery: "?nameUpdated=c+d&selfUpdated=1", + expectedRedirect: page.Paths.Supporter.ManageTeamMembers.Format() + "?nameUpdated=c+d&selfUpdated=1", expectedMember: &actor.Member{ FirstNames: "c", LastName: "d", Email: "a@example.org", }, - memberEmail: "a@example.org", + userPermission: actor.Admin, + memberEmail: "a@example.org", }, - "no updates": { + "As Admin: no updates": { form: url.Values{ "first-names": {"a"}, "last-name": {"b"}, }, - expectedQuery: "?", + expectedRedirect: page.Paths.Supporter.ManageTeamMembers.Format() + "?", expectedMember: &actor.Member{ FirstNames: "a", LastName: "b", }, + userPermission: actor.Admin, + }, + "As Non-Admin: Team member name updated": { + form: url.Values{ + "first-names": {"c"}, + "last-name": {"d"}, + }, + expectedRedirect: page.Paths.Supporter.Dashboard.Format() + "?nameUpdated=c+d", + expectedMember: &actor.Member{ + FirstNames: "c", + LastName: "d", + }, + userPermission: actor.None, + }, + "As Non-Admin: Self name updated": { + form: url.Values{ + "first-names": {"c"}, + "last-name": {"d"}, + }, + expectedRedirect: page.Paths.Supporter.Dashboard.Format() + "?nameUpdated=c+d&selfUpdated=1", + expectedMember: &actor.Member{ + FirstNames: "c", + LastName: "d", + Email: "a@example.org", + }, + userPermission: actor.None, + memberEmail: "a@example.org", + }, + "As Non-Admin: no updates": { + form: url.Values{ + "first-names": {"a"}, + "last-name": {"b"}, + }, + expectedRedirect: page.Paths.Supporter.Dashboard.Format() + "?", + expectedMember: &actor.Member{ + FirstNames: "a", + LastName: "b", + }, + userPermission: actor.None, }, } @@ -149,12 +191,14 @@ func TestPostEditMember(t *testing.T) { Return(nil) testAppData.LoginSessionEmail = "a@example.org" + testAppData.Permission = tc.userPermission + err := EditMember(nil, memberStore)(testAppData, w, r, &actor.Organisation{}) resp := w.Result() assert.Nil(t, err) assert.Equal(t, http.StatusFound, resp.StatusCode) - assert.Equal(t, page.Paths.Supporter.ManageTeamMembers.Format()+tc.expectedQuery, resp.Header.Get("Location")) + assert.Equal(t, tc.expectedRedirect, resp.Header.Get("Location")) }) } } diff --git a/internal/page/supporter/organisation_details.go b/internal/page/supporter/organisation_details.go deleted file mode 100644 index 397aa89524..0000000000 --- a/internal/page/supporter/organisation_details.go +++ /dev/null @@ -1,35 +0,0 @@ -package supporter - -import ( - "net/http" - "net/url" - - "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 organisationDetailsData struct { - App page.AppData - Query url.Values - Errors validation.List - Organisation *actor.Organisation - InvitedMembers []*actor.MemberInvite -} - -func OrganisationDetails(tmpl template.Template, memberStore MemberStore) Handler { - return func(appData page.AppData, w http.ResponseWriter, r *http.Request, organisation *actor.Organisation) error { - invitedMembers, err := memberStore.InvitedMembers(r.Context()) - if err != nil { - return err - } - - return tmpl(w, &organisationDetailsData{ - App: appData, - Query: r.URL.Query(), - Organisation: organisation, - InvitedMembers: invitedMembers, - }) - } -} diff --git a/internal/page/supporter/organisation_details_test.go b/internal/page/supporter/organisation_details_test.go deleted file mode 100644 index 0bfdd32bb7..0000000000 --- a/internal/page/supporter/organisation_details_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package supporter - -import ( - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestGetOrganisationDetails(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/?a=b", nil) - - invitedMembers := []*actor.MemberInvite{ - { - PK: "PK", - SK: "SK", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - OrganisationID: "org-id", - Email: "a@example.com", - FirstNames: "a", - LastName: "b", - Permission: actor.None, - }, - } - - organisation := actor.Organisation{ - PK: "PK", - SK: "SK", - ID: "org-id", - Name: "Org Corp Ltd.", - } - - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, &organisationDetailsData{ - App: testAppData, - Organisation: &organisation, - InvitedMembers: invitedMembers, - Query: url.Values{"a": {"b"}}, - }). - Return(nil) - - memberStore := newMockMemberStore(t) - memberStore.EXPECT(). - InvitedMembers(r.Context()). - Return(invitedMembers, nil) - - err := OrganisationDetails(template.Execute, memberStore)(testAppData, w, r, &organisation) - resp := w.Result() - - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestGetOrganisationDetailsWhenOrganisationStoreError(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/", nil) - - memberStore := newMockMemberStore(t) - memberStore.EXPECT(). - InvitedMembers(r.Context()). - Return([]*actor.MemberInvite{}, expectedError) - - err := OrganisationDetails(nil, memberStore)(testAppData, w, r, &actor.Organisation{}) - resp := w.Result() - - assert.Equal(t, expectedError, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestGetOrganisationDetailsWhenTemplateError(t *testing.T) { - w := httptest.NewRecorder() - r, _ := http.NewRequest(http.MethodGet, "/", nil) - - template := newMockTemplate(t) - template.EXPECT(). - Execute(w, mock.Anything). - Return(expectedError) - - memberStore := newMockMemberStore(t) - memberStore.EXPECT(). - InvitedMembers(mock.Anything). - Return([]*actor.MemberInvite{}, nil) - - err := OrganisationDetails(template.Execute, memberStore)(testAppData, w, r, &actor.Organisation{}) - resp := w.Result() - - assert.Equal(t, expectedError, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) -} diff --git a/internal/page/supporter/register.go b/internal/page/supporter/register.go index 34edd8884f..84a3d2917d 100644 --- a/internal/page/supporter/register.go +++ b/internal/page/supporter/register.go @@ -84,7 +84,7 @@ func Register( handleRoot(paths.InviteExpired, page.RequireSession, page.Guidance(tmpls.Get("invite_expired.gohtml"))) - handleWithSupporter := makeSupporterHandle(rootMux, sessionStore, errorHandler, organisationStore) + handleWithSupporter := makeSupporterHandle(rootMux, sessionStore, errorHandler, organisationStore, memberStore) handleWithSupporter(paths.OrganisationCreated, page.None, Guidance(tmpls.Get("organisation_created.gohtml"))) @@ -135,7 +135,7 @@ 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, page.HandleOpt, Handler) { +func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page.ErrorHandler, organisationStore OrganisationStore, memberStore MemberStore) func(page.SupporterPath, page.HandleOpt, Handler) { return func(path page.SupporterPath, opt page.HandleOpt, h Handler) { mux.HandleFunc(path.String(), func(w http.ResponseWriter, r *http.Request) { loginSession, err := sesh.Login(store, r) @@ -144,17 +144,15 @@ func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page return } - ctx := r.Context() - - appData := page.AppDataFromContext(ctx) + appData := page.AppDataFromContext(r.Context()) appData.SessionID = loginSession.SessionID() appData.CanGoBack = opt&page.CanGoBack != 0 - sessionData, err := page.SessionDataFromContext(ctx) + sessionData, err := page.SessionDataFromContext(r.Context()) + if err == nil { sessionData.SessionID = appData.SessionID sessionData.OrganisationID = loginSession.OrganisationID - ctx = page.ContextWithSessionData(ctx, sessionData) } else { sessionData = &page.SessionData{ SessionID: appData.SessionID, @@ -164,11 +162,21 @@ func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page if loginSession.OrganisationID != "" { sessionData.OrganisationID = loginSession.OrganisationID } + } - ctx = page.ContextWithSessionData(ctx, sessionData) + organisation, err := organisationStore.Get(page.ContextWithSessionData(r.Context(), sessionData)) + if err != nil { + errorHandler(w, r, err) + return } - organisation, err := organisationStore.Get(ctx) + ctx := page.ContextWithSessionData(r.Context(), &page.SessionData{ + SessionID: appData.SessionID, + Email: loginSession.Email, + OrganisationID: organisation.ID, + }) + + member, err := memberStore.Get(ctx) if err != nil { errorHandler(w, r, err) return @@ -179,12 +187,10 @@ func makeSupporterHandle(mux *http.ServeMux, store sesh.Store, errorHandler page appData.OrganisationName = organisation.Name appData.IsManageOrganisation = path.IsManageOrganisation() appData.LoginSessionEmail = loginSession.Email + appData.Permission = member.Permission + appData.LoggedInSupporterID = member.ID - ctx = page.ContextWithAppData(page.ContextWithSessionData(ctx, &page.SessionData{ - SessionID: appData.SessionID, - Email: loginSession.Email, - OrganisationID: organisation.ID, - }), appData) + ctx = page.ContextWithAppData(ctx, appData) if err := h(appData, w, r.WithContext(ctx), organisation); err != nil { errorHandler(w, r, err) diff --git a/internal/page/supporter/register_test.go b/internal/page/supporter/register_test.go index a25a1defd5..8cb5679fed 100644 --- a/internal/page/supporter/register_test.go +++ b/internal/page/supporter/register_test.go @@ -133,22 +133,29 @@ func TestMakeSupporterHandle(t *testing.T) { organisationStore := newMockOrganisationStore(t) organisationStore.EXPECT(). Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: "cmFuZG9t", OrganisationID: "org-id", Email: "a@example.org"})). - Return(&actor.Organisation{}, nil) + Return(&actor.Organisation{ID: "org-id"}, nil) + + memberStore := newMockMemberStore(t) + memberStore.EXPECT(). + Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: "cmFuZG9t", OrganisationID: "org-id", Email: "a@example.org"})). + Return(&actor.Member{Permission: actor.Admin, ID: "member-id"}, nil) - handle := makeSupporterHandle(mux, sessionStore, nil, organisationStore) + handle := makeSupporterHandle(mux, sessionStore, nil, organisationStore, memberStore) handle("/path", page.CanGoBack, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, organisation *actor.Organisation) error { assert.Equal(t, page.AppData{ - Page: "/supporter/path", - SessionID: "cmFuZG9t", - IsSupporter: true, - CanGoBack: true, - LoginSessionEmail: "a@example.org", + Page: "/supporter/path", + SessionID: "cmFuZG9t", + IsSupporter: true, + CanGoBack: true, + LoginSessionEmail: "a@example.org", + Permission: actor.Admin, + LoggedInSupporterID: "member-id", }, appData) assert.Equal(t, w, hw) sessionData, _ := page.SessionDataFromContext(hr.Context()) - assert.Equal(t, &page.SessionData{SessionID: "cmFuZG9t", Email: "a@example.org"}, sessionData) + assert.Equal(t, &page.SessionData{SessionID: "cmFuZG9t", Email: "a@example.org", OrganisationID: "org-id"}, sessionData) hw.WriteHeader(http.StatusTeapot) return nil @@ -175,25 +182,33 @@ func TestMakeSupporterHandleWithSessionData(t *testing.T) { sessionStore := newMockSessionStore(t) sessionStore.EXPECT(). Get(r, "session"). - Return(&sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random", OrganisationID: "org-id"}}}, nil) + Return(&sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random", OrganisationID: "org-id", Email: "a@example.org"}}}, nil) organisationStore := newMockOrganisationStore(t) organisationStore.EXPECT(). Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: "cmFuZG9t", OrganisationID: "org-id"})). - Return(&actor.Organisation{}, nil) + Return(&actor.Organisation{ID: "org-id"}, nil) - handle := makeSupporterHandle(mux, sessionStore, nil, organisationStore) + memberStore := newMockMemberStore(t) + memberStore.EXPECT(). + Get(page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: "cmFuZG9t", OrganisationID: "org-id", Email: "a@example.org"})). + Return(&actor.Member{Permission: actor.Admin, ID: "member-id"}, nil) + + handle := makeSupporterHandle(mux, sessionStore, nil, organisationStore, memberStore) handle("/path", page.None, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, organisation *actor.Organisation) error { assert.Equal(t, page.AppData{ - Page: "/supporter/path", - SessionID: "cmFuZG9t", - IsSupporter: true, + Page: "/supporter/path", + SessionID: "cmFuZG9t", + IsSupporter: true, + Permission: actor.Admin, + LoggedInSupporterID: "member-id", + LoginSessionEmail: "a@example.org", }, appData) assert.Equal(t, w, hw) sessionData, _ := page.SessionDataFromContext(hr.Context()) - assert.Equal(t, &page.SessionData{SessionID: "cmFuZG9t"}, sessionData) + assert.Equal(t, &page.SessionData{SessionID: "cmFuZG9t", Email: "a@example.org", OrganisationID: "org-id"}, sessionData) hw.WriteHeader(http.StatusTeapot) return nil @@ -216,7 +231,7 @@ func TestMakeSupporterHandleWhenSessionStoreError(t *testing.T) { Get(r, "session"). Return(&sessions.Session{}, expectedError) - handle := makeSupporterHandle(mux, sessionStore, nil, nil) + handle := makeSupporterHandle(mux, sessionStore, nil, nil, nil) handle("/path", page.None, func(_ page.AppData, _ http.ResponseWriter, _ *http.Request, _ *actor.Organisation) error { return nil }) @@ -248,12 +263,48 @@ func TestMakeSupporterHandleWhenOrganisationStoreErrors(t *testing.T) { Get(mock.Anything). Return(nil, expectedError) - handle := makeSupporterHandle(mux, sessionStore, errorHandler.Execute, organisationStore) + handle := makeSupporterHandle(mux, sessionStore, errorHandler.Execute, organisationStore, nil) + handle("/path", page.None, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, organisation *actor.Organisation) error { + return nil + }) + + mux.ServeHTTP(w, r) +} + +func TestMakeSupporterHandleWhenMemberStoreError(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/supporter/path", nil) + + mux := http.NewServeMux() + + errorHandler := newMockErrorHandler(t) + errorHandler.EXPECT(). + Execute(w, r, expectedError) + + sessionStore := newMockSessionStore(t) + sessionStore.EXPECT(). + Get(mock.Anything, mock.Anything). + Return(&sessions.Session{Values: map[any]any{"session": &sesh.LoginSession{Sub: "random"}}}, nil) + + organisationStore := newMockOrganisationStore(t) + organisationStore.EXPECT(). + Get(mock.Anything). + Return(&actor.Organisation{}, nil) + + memberStore := newMockMemberStore(t) + memberStore.EXPECT(). + Get(mock.Anything). + Return(&actor.Member{}, expectedError) + + handle := makeSupporterHandle(mux, sessionStore, errorHandler.Execute, organisationStore, memberStore) handle("/path", page.None, func(appData page.AppData, hw http.ResponseWriter, hr *http.Request, organisation *actor.Organisation) error { return nil }) mux.ServeHTTP(w, r) + resp := w.Result() + + assert.Equal(t, http.StatusOK, resp.StatusCode) } func TestMakeSupporterHandleErrors(t *testing.T) { @@ -274,8 +325,13 @@ func TestMakeSupporterHandleErrors(t *testing.T) { Get(mock.Anything). Return(&actor.Organisation{}, nil) + memberStore := newMockMemberStore(t) + memberStore.EXPECT(). + Get(mock.Anything). + Return(&actor.Member{}, nil) + mux := http.NewServeMux() - handle := makeSupporterHandle(mux, sessionStore, errorHandler.Execute, organisationStore) + handle := makeSupporterHandle(mux, sessionStore, errorHandler.Execute, organisationStore, memberStore) handle("/path", page.None, func(_ page.AppData, _ http.ResponseWriter, _ *http.Request, _ *actor.Organisation) error { return expectedError }) diff --git a/lang/cy.json b/lang/cy.json index 039efe6452..37d16985b2 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -1083,5 +1083,6 @@ "nameUpdated": "Welsh", "teamMembersNameUpdatedToNewName": "Welsh {{ .NewName }}.", "yourNameHasBeenUpdatedToNewName": "Welsh {{ .NewName }}.", - "you": "Welsh" + "you": "Welsh", + "manageYourDetails": "Welsh" } diff --git a/lang/en.json b/lang/en.json index 727772cbf9..228f475a97 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1021,5 +1021,6 @@ "nameUpdated": "Name updated", "teamMembersNameUpdatedToNewName": "Team member’s name updated to {{ .NewName }}.", "yourNameHasBeenUpdatedToNewName": "Your name has been updated to {{ .NewName }}.", - "you": "you" + "you": "you", + "manageYourDetails": "Manage your details" } diff --git a/web/template/layout/login-header.gohtml b/web/template/layout/login-header.gohtml index 9522318b7f..5c5fac1df9 100644 --- a/web/template/layout/login-header.gohtml +++ b/web/template/layout/login-header.gohtml @@ -117,12 +117,18 @@