diff --git a/cypress/e2e/supporter/enter-organisation-name.cy.js b/cypress/e2e/supporter/enter-organisation-name.cy.js new file mode 100644 index 0000000000..1056c675f1 --- /dev/null +++ b/cypress/e2e/supporter/enter-organisation-name.cy.js @@ -0,0 +1,12 @@ +describe('Enter group name', () => { + beforeEach(() => { + cy.visit('/fixtures/supporter?redirect=/enter-the-name-of-your-organisation-or-company'); + }); + + it('can be started', () => { + cy.get('#f-name').type('My name' + Math.random()); + cy.contains('button', 'Continue').click(); + + cy.url().should('contain', '/organisation-or-company-created'); + }); +}); diff --git a/internal/actor/organisation.go b/internal/actor/organisation.go new file mode 100644 index 0000000000..ecfb0a2ac0 --- /dev/null +++ b/internal/actor/organisation.go @@ -0,0 +1,26 @@ +package actor + +import "time" + +// An Organisation contains users associated with a set of permissions that work on the +// same set of LPAs. +type Organisation struct { + PK, SK string + // CreatedAt is when the Organisation was created + CreatedAt time.Time + // UpdatedAt is when the Organisation was last updated + UpdatedAt time.Time + // ID is a unique identifier for the Organisation + ID string + // Name of the Organisation, this is unique across all Organisations + Name string +} + +// A Member is the association of a OneLogin user with an Organisation. +type Member struct { + PK, SK string + // CreatedAt is when the Member was created + CreatedAt time.Time + // UpdatedAt is when the Member was last updated + UpdatedAt time.Time +} diff --git a/internal/app/app.go b/internal/app/app.go index 28cca126eb..b84b3f1be1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -103,6 +103,7 @@ func App( shareCodeStore := &shareCodeStore{dynamoClient: lpaDynamoClient} dashboardStore := &dashboardStore{dynamoClient: lpaDynamoClient} evidenceReceivedStore := &evidenceReceivedStore{dynamoClient: lpaDynamoClient} + organisationStore := &organisationStore{dynamoClient: lpaDynamoClient, now: time.Now, uuidString: uuid.NewString} shareCodeSender := page.NewShareCodeSender(shareCodeStore, notifyClient, appPublicURL, random.String) witnessCodeSender := page.NewWitnessCodeSender(donorStore, notifyClient) @@ -123,6 +124,8 @@ func App( fixtures.CertificateProvider(tmpls.Get("certificate_provider_fixtures.gohtml"), sessionStore, shareCodeSender, donorStore, certificateProviderStore)) handleRoot(paths.AttorneyFixtures, None, fixtures.Attorney(tmpls.Get("attorney_fixtures.gohtml"), sessionStore, shareCodeSender, donorStore, certificateProviderStore, attorneyStore)) + handleRoot(paths.SupporterFixtures, None, + fixtures.Supporter(sessionStore)) handleRoot(paths.DashboardFixtures, None, fixtures.Dashboard(tmpls.Get("dashboard_fixtures.gohtml"), sessionStore, shareCodeSender, donorStore, certificateProviderStore, attorneyStore)) handleRoot(paths.YourLegalRightsAndResponsibilities, None, @@ -147,6 +150,7 @@ func App( supporterTmpls, oneLoginClient, sessionStore, + organisationStore, notFoundHandler, errorHandler, ) diff --git a/internal/app/organisation_store.go b/internal/app/organisation_store.go new file mode 100644 index 0000000000..53a6e18efd --- /dev/null +++ b/internal/app/organisation_store.go @@ -0,0 +1,58 @@ +package app + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" +) + +type organisationStore struct { + dynamoClient DynamoClient + uuidString func() string + now func() time.Time +} + +func (s *organisationStore) Create(ctx context.Context, name string) error { + data, err := page.SessionDataFromContext(ctx) + if err != nil { + return err + } + + if data.SessionID == "" { + return errors.New("organisationStore.Create requires SessionID") + } + + organisationID := s.uuidString() + + organisation := &actor.Organisation{ + PK: organisationKey(organisationID), + SK: organisationKey(organisationID), + ID: organisationID, + Name: name, + CreatedAt: s.now(), + } + + if err := s.dynamoClient.Create(ctx, organisation); err != nil { + return fmt.Errorf("error creating organisation: %w", err) + } + + member := &actor.Member{ + PK: organisationKey(organisationID), + SK: subKey(data.SessionID), + CreatedAt: s.now(), + } + + if err := s.dynamoClient.Create(ctx, member); err != nil { + return fmt.Errorf("error creating organisation member: %w", err) + } + + return nil +} + +func organisationKey(s string) string { + return "ORGANISATION#" + s +} diff --git a/internal/app/organisation_store_test.go b/internal/app/organisation_store_test.go new file mode 100644 index 0000000000..290082a76a --- /dev/null +++ b/internal/app/organisation_store_test.go @@ -0,0 +1,91 @@ +package app + +import ( + "context" + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" +) + +func TestOrganisationStoreCreate(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + Create(ctx, &actor.Organisation{ + PK: "ORGANISATION#a-uuid", + SK: "ORGANISATION#a-uuid", + ID: "a-uuid", + CreatedAt: testNow, + Name: "A name", + }). + Return(nil) + dynamoClient.EXPECT(). + Create(ctx, &actor.Member{ + PK: "ORGANISATION#a-uuid", + SK: "#SUB#an-id", + CreatedAt: testNow, + }). + Return(nil) + + organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + err := organisationStore.Create(ctx, "A name") + assert.Nil(t, err) +} + +func TestOrganisationStoreCreateWithSessionMissing(t *testing.T) { + ctx := context.Background() + organisationStore := &organisationStore{dynamoClient: nil, now: testNowFn} + + err := organisationStore.Create(ctx, "A name") + assert.Equal(t, page.SessionMissingError{}, err) +} + +func TestOrganisationStoreCreateWithMissingSessionID(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{}) + organisationStore := &organisationStore{dynamoClient: nil, now: testNowFn} + + err := organisationStore.Create(ctx, "A name") + assert.Error(t, err) +} + +func TestOrganisationStoreCreateWhenErrors(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + testcases := map[string]func(*testing.T) *mockDynamoClient{ + "organisation": func(t *testing.T) *mockDynamoClient { + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + Create(ctx, mock.Anything). + Return(expectedError) + + return dynamoClient + }, + "member": func(t *testing.T) *mockDynamoClient { + dynamoClient := newMockDynamoClient(t) + dynamoClient.EXPECT(). + Create(ctx, mock.Anything). + Return(nil). + Once() + dynamoClient.EXPECT(). + Create(ctx, mock.Anything). + Return(expectedError) + + return dynamoClient + }, + } + + for name, makeMockDynamoClient := range testcases { + t.Run(name, func(t *testing.T) { + dynamoClient := makeMockDynamoClient(t) + organisationStore := &organisationStore{dynamoClient: dynamoClient, now: testNowFn, uuidString: func() string { return "a-uuid" }} + + err := organisationStore.Create(ctx, "A name") + assert.ErrorIs(t, err, expectedError) + }) + } +} diff --git a/internal/page/fixtures/supporter.go b/internal/page/fixtures/supporter.go new file mode 100644 index 0000000000..54a2379b11 --- /dev/null +++ b/internal/page/fixtures/supporter.go @@ -0,0 +1,26 @@ +package fixtures + +import ( + "net/http" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/random" + "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" +) + +func Supporter(sessionStore sesh.Store) page.Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { + var ( + redirect = r.FormValue("redirect") + + supporterSub = random.String(16) + ) + + if err := sesh.SetLoginSession(sessionStore, r, w, &sesh.LoginSession{Sub: supporterSub, Email: testEmail}); err != nil { + return err + } + + http.Redirect(w, r, "/supporter/"+redirect, http.StatusFound) + return nil + } +} diff --git a/internal/page/paths.go b/internal/page/paths.go index a9666652d5..d63153df08 100644 --- a/internal/page/paths.go +++ b/internal/page/paths.go @@ -168,7 +168,8 @@ type SupporterPaths struct { Login Path LoginCallback Path - YourOrganisation SupporterPath + EnterOrganisationName SupporterPath + OrganisationCreated SupporterPath } type AppPaths struct { @@ -193,6 +194,7 @@ type AppPaths struct { Root Path SignOut Path Start Path + SupporterFixtures Path YourLegalRightsAndResponsibilities Path AboutPayment LpaPath @@ -334,7 +336,8 @@ var Paths = AppPaths{ Login: "/supporter-login", LoginCallback: "/supporter-login-callback", - YourOrganisation: "/your-organisation", + EnterOrganisationName: "/enter-the-name-of-your-organisation-or-company", + OrganisationCreated: "/organisation-or-company-created", }, HealthCheck: HealthCheckPaths{ @@ -425,6 +428,7 @@ var Paths = AppPaths{ SignTheLpaOnBehalf: "/sign-the-lpa-on-behalf", SignYourLpa: "/sign-your-lpa", Start: "/start", + SupporterFixtures: "/fixtures/supporter", TaskList: "/task-list", UploadEvidence: "/upload-evidence", UploadEvidenceSSE: "/upload-evidence-sse", @@ -438,12 +442,12 @@ var Paths = AppPaths{ WitnessingAsCertificateProvider: "/witnessing-as-certificate-provider", WitnessingAsIndependentWitness: "/witnessing-as-independent-witness", WitnessingYourSignature: "/witnessing-your-signature", - YouHaveSubmittedYourLpa: "/you-have-submitted-your-lpa", YouCannotSignYourLpaYet: "/you-cannot-sign-your-lpa-yet", + YouHaveSubmittedYourLpa: "/you-have-submitted-your-lpa", YourAddress: "/your-address", YourAuthorisedSignatory: "/your-authorised-signatory", - YourDetails: "/your-details", YourDateOfBirth: "/your-date-of-birth", + YourDetails: "/your-details", YourIndependentWitness: "/your-independent-witness", YourIndependentWitnessAddress: "/your-independent-witness-address", YourIndependentWitnessMobile: "/your-independent-witness-mobile", diff --git a/internal/page/supporter/enter_organisation_name.go b/internal/page/supporter/enter_organisation_name.go new file mode 100644 index 0000000000..50639b0798 --- /dev/null +++ b/internal/page/supporter/enter_organisation_name.go @@ -0,0 +1,59 @@ +package supporter + +import ( + "net/http" + + "github.com/ministryofjustice/opg-go-common/template" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" +) + +type enterOrganisationNameData struct { + App page.AppData + Errors validation.List + Form *enterOrganisationNameForm +} + +func EnterOrganisationName(tmpl template.Template, organisationStore OrganisationStore) Handler { + return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { + data := &enterOrganisationNameData{ + App: appData, + Form: &enterOrganisationNameForm{}, + } + + if r.Method == http.MethodPost { + data.Form = readEnterOrganisationNameForm(r) + data.Errors = data.Form.Validate() + + if !data.Errors.Any() { + if err := organisationStore.Create(r.Context(), data.Form.Name); err != nil { + return err + } + + return page.Paths.Supporter.OrganisationCreated.Redirect(w, r, appData) + } + } + + return tmpl(w, data) + } +} + +type enterOrganisationNameForm struct { + Name string +} + +func readEnterOrganisationNameForm(r *http.Request) *enterOrganisationNameForm { + return &enterOrganisationNameForm{ + Name: page.PostFormString(r, "name"), + } +} + +func (f *enterOrganisationNameForm) Validate() validation.List { + var errors validation.List + + errors.String("name", "fullOrganisationOrCompanyName", f.Name, + validation.Empty(), + validation.StringTooLong(100)) + + return errors +} diff --git a/internal/page/supporter/enter_organisation_name_test.go b/internal/page/supporter/enter_organisation_name_test.go new file mode 100644 index 0000000000..c39c6d4974 --- /dev/null +++ b/internal/page/supporter/enter_organisation_name_test.go @@ -0,0 +1,158 @@ +package supporter + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/validation" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEnterOrganisationName(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, &enterOrganisationNameData{ + App: testAppData, + Form: &enterOrganisationNameForm{}, + }). + Return(nil) + + err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestGetEnterOrganisationNameWhenTemplateErrors(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, mock.Anything). + Return(expectedError) + + err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostEnterOrganisationName(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(), "My organisation"). + Return(nil) + + err := EnterOrganisationName(nil, organisationStore)(testAppData, w, r) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Equal(t, page.Paths.Supporter.OrganisationCreated.Format(), resp.Header.Get("Location")) +} + +func TestPostEnterOrganisationNameWhenValidationError(t *testing.T) { + w := httptest.NewRecorder() + form := url.Values{} + + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + dataMatcher := func(t *testing.T, data *enterOrganisationNameData) bool { + return assert.Equal(t, validation.With("name", validation.EnterError{Label: "fullOrganisationOrCompanyName"}), data.Errors) + } + + template := newMockTemplate(t) + template.EXPECT(). + Execute(w, mock.MatchedBy(func(data *enterOrganisationNameData) bool { + return dataMatcher(t, data) + })). + Return(nil) + + err := EnterOrganisationName(template.Execute, nil)(testAppData, w, r) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestPostEnterOrganisationNameWhenOrganisationStoreErrors(t *testing.T) { + form := url.Values{ + "name": {"My name"}, + } + + 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(expectedError) + + err := EnterOrganisationName(nil, organisationStore)(testAppData, w, r) + resp := w.Result() + + assert.Equal(t, expectedError, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestReadEnterOrganisationNameForm(t *testing.T) { + form := url.Values{ + "name": {"My name"}, + } + + r, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode())) + r.Header.Add("Content-Type", page.FormUrlEncoded) + + result := readEnterOrganisationNameForm(r) + + assert.Equal(t, "My name", result.Name) +} + +func TestEnterOrganisationNameFormValidate(t *testing.T) { + testCases := map[string]struct { + form *enterOrganisationNameForm + errors validation.List + }{ + "valid": { + form: &enterOrganisationNameForm{ + Name: "My name", + }, + }, + "missing": { + form: &enterOrganisationNameForm{}, + errors: validation.With("name", validation.EnterError{Label: "fullOrganisationOrCompanyName"}), + }, + "too long": { + form: &enterOrganisationNameForm{ + Name: strings.Repeat("a", 101), + }, + errors: validation.With("name", validation.StringTooLongError{Label: "fullOrganisationOrCompanyName", Length: 100}), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.errors, tc.form.Validate()) + }) + } +} diff --git a/internal/page/supporter/login_callback.go b/internal/page/supporter/login_callback.go index 5902272b96..220410761a 100644 --- a/internal/page/supporter/login_callback.go +++ b/internal/page/supporter/login_callback.go @@ -39,6 +39,6 @@ func LoginCallback(oneLoginClient LoginCallbackOneLoginClient, sessionStore sesh return err } - return page.Paths.Supporter.YourOrganisation.Redirect(w, r, appData) + return page.Paths.Supporter.EnterOrganisationName.Redirect(w, r, appData) } } diff --git a/internal/page/supporter/login_callback_test.go b/internal/page/supporter/login_callback_test.go index 1604789ed0..b3951edee3 100644 --- a/internal/page/supporter/login_callback_test.go +++ b/internal/page/supporter/login_callback_test.go @@ -64,7 +64,7 @@ func TestLoginCallback(t *testing.T) { resp := w.Result() assert.Equal(t, http.StatusFound, resp.StatusCode) - assert.Equal(t, page.Paths.Supporter.YourOrganisation.Format(), resp.Header.Get("Location")) + assert.Equal(t, page.Paths.Supporter.EnterOrganisationName.Format(), resp.Header.Get("Location")) } func TestLoginCallbackSessionMissing(t *testing.T) { diff --git a/internal/page/supporter/mock_OrganisationStore_test.go b/internal/page/supporter/mock_OrganisationStore_test.go new file mode 100644 index 0000000000..a7e320b947 --- /dev/null +++ b/internal/page/supporter/mock_OrganisationStore_test.go @@ -0,0 +1,83 @@ +// Code generated by mockery v2.39.1. DO NOT EDIT. + +package supporter + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// mockOrganisationStore is an autogenerated mock type for the OrganisationStore type +type mockOrganisationStore struct { + mock.Mock +} + +type mockOrganisationStore_Expecter struct { + mock *mock.Mock +} + +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) + + 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) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockOrganisationStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type mockOrganisationStore_Create_Call struct { + *mock.Call +} + +// 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)} +} + +func (_c *mockOrganisationStore_Create_Call) Run(run func(_a0 context.Context, _a1 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) + return _c +} + +func (_c *mockOrganisationStore_Create_Call) RunAndReturn(run func(context.Context, string) error) *mockOrganisationStore_Create_Call { + _c.Call.Return(run) + return _c +} + +// newMockOrganisationStore creates a new instance of mockOrganisationStore. 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 newMockOrganisationStore(t interface { + mock.TestingT + Cleanup(func()) +}) *mockOrganisationStore { + mock := &mockOrganisationStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/page/supporter/mock_Template_test.go b/internal/page/supporter/mock_Template_test.go new file mode 100644 index 0000000000..2c0374cb67 --- /dev/null +++ b/internal/page/supporter/mock_Template_test.go @@ -0,0 +1,83 @@ +// Code generated by mockery v2.39.1. DO NOT EDIT. + +package supporter + +import ( + io "io" + + mock "github.com/stretchr/testify/mock" +) + +// mockTemplate is an autogenerated mock type for the Template type +type mockTemplate struct { + mock.Mock +} + +type mockTemplate_Expecter struct { + mock *mock.Mock +} + +func (_m *mockTemplate) EXPECT() *mockTemplate_Expecter { + return &mockTemplate_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0, _a1 +func (_m *mockTemplate) Execute(_a0 io.Writer, _a1 interface{}) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 error + if rf, ok := ret.Get(0).(func(io.Writer, interface{}) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockTemplate_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type mockTemplate_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 io.Writer +// - _a1 interface{} +func (_e *mockTemplate_Expecter) Execute(_a0 interface{}, _a1 interface{}) *mockTemplate_Execute_Call { + return &mockTemplate_Execute_Call{Call: _e.mock.On("Execute", _a0, _a1)} +} + +func (_c *mockTemplate_Execute_Call) Run(run func(_a0 io.Writer, _a1 interface{})) *mockTemplate_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(io.Writer), args[1].(interface{})) + }) + return _c +} + +func (_c *mockTemplate_Execute_Call) Return(_a0 error) *mockTemplate_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockTemplate_Execute_Call) RunAndReturn(run func(io.Writer, interface{}) error) *mockTemplate_Execute_Call { + _c.Call.Return(run) + return _c +} + +// newMockTemplate creates a new instance of mockTemplate. 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 newMockTemplate(t interface { + mock.TestingT + Cleanup(func()) +}) *mockTemplate { + mock := &mockTemplate{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/page/supporter/register.go b/internal/page/supporter/register.go index 9c23d996aa..ac0058615a 100644 --- a/internal/page/supporter/register.go +++ b/internal/page/supporter/register.go @@ -3,6 +3,7 @@ package supporter import ( "context" "encoding/base64" + "io" "net/http" "github.com/gorilla/sessions" @@ -13,6 +14,10 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" ) +type OrganisationStore interface { + Create(context.Context, string) error +} + type OneLoginClient interface { AuthCodeURL(state, nonce, locale string, identity bool) (string, error) Exchange(ctx context.Context, code, nonce string) (idToken, accessToken string, err error) @@ -25,6 +30,8 @@ type SessionStore interface { Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error } +type Template func(io.Writer, interface{}) error + type Handler func(data page.AppData, w http.ResponseWriter, r *http.Request) error type ErrorHandler func(http.ResponseWriter, *http.Request, error) @@ -34,6 +41,7 @@ func Register( tmpls template.Templates, oneLoginClient OneLoginClient, sessionStore SessionStore, + organisationStore OrganisationStore, notFoundHandler page.Handler, errorHandler page.ErrorHandler, ) { @@ -53,7 +61,9 @@ func Register( handleSupporter(page.Paths.Root, notFoundHandler) - handleWithSupporter(paths.YourOrganisation, + handleWithSupporter(paths.EnterOrganisationName, + EnterOrganisationName(tmpls.Get("enter_organisation_name.gohtml"), organisationStore)) + handleWithSupporter(paths.OrganisationCreated, TODO()) } diff --git a/internal/page/supporter/register_test.go b/internal/page/supporter/register_test.go index a883ddc075..b3b0dd858e 100644 --- a/internal/page/supporter/register_test.go +++ b/internal/page/supporter/register_test.go @@ -15,10 +15,11 @@ import ( ) var expectedError = errors.New("err") +var testAppData = page.AppData{} func TestRegister(t *testing.T) { mux := http.NewServeMux() - Register(mux, template.Templates{}, &onelogin.Client{}, nil, nil, nil) + Register(mux, template.Templates{}, &onelogin.Client{}, nil, nil, nil, nil) assert.Implements(t, (*http.Handler)(nil), mux) } diff --git a/internal/page/supporter/todo.go b/internal/page/supporter/todo.go index 7246f9b0a9..8ba01786ca 100644 --- a/internal/page/supporter/todo.go +++ b/internal/page/supporter/todo.go @@ -8,7 +8,7 @@ import ( func TODO() Handler { return func(appData page.AppData, w http.ResponseWriter, r *http.Request) error { - _, err := w.Write([]byte("TODO")) + _, err := w.Write([]byte("
TODO
")) return err } } diff --git a/lang/cy.json b/lang/cy.json index 8bb23a527d..e922df9f4f 100644 --- a/lang/cy.json +++ b/lang/cy.json @@ -1021,5 +1021,8 @@ "howeverItWillNotApplyToAnySignedOrRegisteredLPAs": "Welsh Welsh Welsh {{ .Detail }} Welsh
", "ifYoureInProcessOfMakingAnotherLPACheckIfDetailIsCorrect": "Welsh {{ .Detail }} Welsh", "helpSomeoneMakeLastingPowerOfAttorney": "Welsh", - "helpSomeoneMakeLastingPowerOfAttorneyContent": "Welsh" + "helpSomeoneMakeLastingPowerOfAttorneyContent": "Welsh", + "enterTheNameOfYourOrganisationOrCompany": "Welsh", + "fullOrganisationOrCompanyName": "Welsh", + "forExampleCharityOrSolicitor": "Welsh" } diff --git a/lang/en.json b/lang/en.json index bc77438a11..0358b6df6c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -959,5 +959,8 @@ "howeverItWillNotApplyToAnySignedOrRegisteredLPAs": "However, it will not apply to any signed or registered LPAs you already have. You should contact OPG to correct your {{ .Detail }} on an existing LPA.
", "ifYoureInProcessOfMakingAnotherLPACheckIfDetailIsCorrect": "If you’re in the process of making another LPA, you should check if your {{ .Detail }} is correct and update it if necessary.", "helpSomeoneMakeLastingPowerOfAttorney": "Help someone to make a lasting power of attorney (LPA)", - "helpSomeoneMakeLastingPowerOfAttorneyContent": "This online service is for professionals who help people to make LPAs.
You can use this service if you or your organisation help people to make LPAs.
For example, if you are a:
To create your organisation, you’ll need:
If you are the first person from your organisation to use this service, you will be an admin by default. This means you can invite team members to create accounts and manage permissions.
An organisation can have more than one admin.
If someone else in your organisation has already used this service, you do not need to create the organisation again.
Instead, you should ask them to invite you to the existing organisation rather than creating a new one.
This online service is for professionals who help people to make LPAs.
You can use this service if you or your organisation help people to make LPAs.
For example, if you are a:
To create your organisation, you’ll need:
If you are the first person from your organisation to use this service, you will be an admin by default. This means you can invite team members to create accounts and manage permissions.
An organisation can have more than one admin.
If someone else in your organisation has already used this service, you do not need to create the organisation again.
Instead, you should ask them to invite you to the existing organisation rather than creating a new one.