From a5382ee3d9a0f98ac1148096cc0794c2ea1a2714 Mon Sep 17 00:00:00 2001 From: Joshua Hawxwell Date: Thu, 21 Sep 2023 08:58:29 +0100 Subject: [PATCH] Use details from the latest LPA to populate your-details page This changes the ActorIndex to be (SK, UpdatedAt) so we can query for LPAs by actor sorted by UpdatedAt. By not setting UpdatedAt until a UID is set we can filter out LPAs that aren't shown on the dashboard page. Also renames the dynamo client methods to be more consistent, now they are prefixed with One or All depending on how many results are returned. --- cmd/event-received/main.go | 20 +- cmd/event-received/main_test.go | 64 ++-- .../mock_dynamodbClient_test.go | 20 +- ...choose-replacement-attorneys-summary.cy.js | 32 +- cypress/e2e/donor/dashboard.cy.js | 9 +- .../localstack/dynamodb-lpa-gsi-schema.json | 4 +- docker/localstack/localstack-init.sh | 8 +- internal/app/app.go | 9 +- internal/app/attorney_store.go | 19 +- internal/app/attorney_store_test.go | 40 +-- internal/app/certificate_provider_store.go | 21 +- .../app/certificate_provider_store_test.go | 44 +-- internal/app/dashboard_store.go | 7 +- internal/app/dashboard_store_test.go | 35 +- internal/app/donor_store.go | 56 ++-- internal/app/donor_store_test.go | 132 +++++--- internal/app/evidence_received_store.go | 2 +- internal/app/evidence_received_store_test.go | 6 +- internal/app/mock_DynamoClient_test.go | 88 ++--- internal/app/share_code_store.go | 2 +- internal/app/share_code_store_test.go | 4 +- internal/dynamo/client.go | 96 +++--- internal/dynamo/client_test.go | 306 ++++++++++-------- .../page/attorney/mock_AttorneyStore_test.go | 26 -- .../mock_CertificateProviderStore_test.go | 66 ---- internal/page/attorney/register.go | 4 - .../mock_CertificateProviderStore_test.go | 26 -- internal/page/certificateprovider/register.go | 1 - internal/page/common.go | 4 - internal/page/donor/mock_DonorStore_test.go | 34 +- internal/page/donor/register.go | 3 +- internal/page/donor/your_details.go | 12 + internal/page/donor/your_details_test.go | 48 ++- internal/page/mock_AttorneyStore_test.go | 26 -- .../mock_CertificateProviderStore_test.go | 26 -- internal/page/mock_DonorStore_test.go | 52 --- terraform/environment/dynamodb.tf | 10 +- 37 files changed, 593 insertions(+), 769 deletions(-) diff --git a/cmd/event-received/main.go b/cmd/event-received/main.go index 4bcdeff788..eaa69ea178 100644 --- a/cmd/event-received/main.go +++ b/cmd/event-received/main.go @@ -29,9 +29,9 @@ type uidEvent struct { //go:generate mockery --testonly --inpackage --name dynamodbClient --structname mockDynamodbClient type dynamodbClient interface { - Put(context.Context, interface{}) error - GetOneByUID(context.Context, string, interface{}) error - Get(ctx context.Context, pk, sk string, v interface{}) error + One(ctx context.Context, pk, sk string, v interface{}) error + OneByUID(ctx context.Context, uid string, v interface{}) error + Put(ctx context.Context, v interface{}) error } //go:generate mockery --testonly --inpackage --name shareCodeSender --structname mockShareCodeSender @@ -106,7 +106,7 @@ func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event ev } var key dynamo.Key - if err := client.GetOneByUID(ctx, v.UID, &key); err != nil { + if err := client.OneByUID(ctx, v.UID, &key); err != nil { return fmt.Errorf("failed to resolve uid for 'evidence-received': %w", err) } @@ -128,12 +128,12 @@ func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event e } var key dynamo.Key - if err := dynamoClient.GetOneByUID(ctx, v.UID, &key); err != nil { + if err := dynamoClient.OneByUID(ctx, v.UID, &key); err != nil { return fmt.Errorf("failed to resolve uid for 'fee-approved': %w", err) } var lpa page.Lpa - if err := dynamoClient.Get(ctx, key.PK, key.SK, &lpa); err != nil { + if err := dynamoClient.One(ctx, key.PK, key.SK, &lpa); err != nil { return fmt.Errorf("failed to get LPA for 'fee-approved': %w", err) } @@ -157,7 +157,7 @@ func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, even } var key dynamo.Key - if err := client.GetOneByUID(ctx, v.UID, &key); err != nil { + if err := client.OneByUID(ctx, v.UID, &key); err != nil { return fmt.Errorf("failed to resolve uid for 'more-evidence-required': %w", err) } @@ -166,7 +166,7 @@ func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, even } var lpa page.Lpa - if err := client.Get(ctx, key.PK, key.SK, &lpa); err != nil { + if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil { return fmt.Errorf("failed to get LPA for 'more-evidence-required': %w", err) } @@ -186,7 +186,7 @@ func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.Cl } var key dynamo.Key - if err := client.GetOneByUID(ctx, v.UID, &key); err != nil { + if err := client.OneByUID(ctx, v.UID, &key); err != nil { return fmt.Errorf("failed to resolve uid for 'fee-denied': %w", err) } @@ -195,7 +195,7 @@ func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.Cl } var lpa page.Lpa - if err := client.Get(ctx, key.PK, key.SK, &lpa); err != nil { + if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil { return fmt.Errorf("failed to get LPA for 'fee-denied': %w", err) } diff --git a/cmd/event-received/main_test.go b/cmd/event-received/main_test.go index a6b2da8b3a..d75fdcaaa1 100644 --- a/cmd/event-received/main_test.go +++ b/cmd/event-received/main_test.go @@ -27,7 +27,7 @@ func TestHandleEvidenceReceived(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123"}) json.Unmarshal(b, v) @@ -53,7 +53,7 @@ func TestHandleEvidenceReceivedWhenClientGetError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(expectedError) err := handleEvidenceReceived(ctx, client, event) @@ -69,7 +69,7 @@ func TestHandleEvidenceReceivedWhenLpaMissingPK(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{}) json.Unmarshal(b, v) @@ -89,7 +89,7 @@ func TestHandleEvidenceReceivedWhenClientPutError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123"}) json.Unmarshal(b, v) @@ -115,14 +115,14 @@ func TestHandleFeeApproved(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -141,7 +141,7 @@ func TestHandleFeeApproved(t *testing.T) { assert.Nil(t, err) } -func TestHandleFeeApprovedWhenDynamoClientGetOneByUIDError(t *testing.T) { +func TestHandleFeeApprovedWhenDynamoClientOneByUIDError(t *testing.T) { ctx := context.Background() event := events.CloudWatchEvent{ DetailType: "fee-approved", @@ -150,7 +150,7 @@ func TestHandleFeeApprovedWhenDynamoClientGetOneByUIDError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(expectedError) err := handleFeeApproved(ctx, client, event, nil, page.AppData{}) @@ -166,14 +166,14 @@ func TestHandleFeeApprovedWhenDynamoClientGetError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(expectedError) err := handleFeeApproved(ctx, client, event, nil, page.AppData{}) @@ -189,14 +189,14 @@ func TestHandleFeeApprovedWhenDynamoClientPutError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -219,14 +219,14 @@ func TestHandleFeeApprovedWhenShareCodeSenderError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -254,14 +254,14 @@ func TestHandleMoreEvidenceRequired(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -275,7 +275,7 @@ func TestHandleMoreEvidenceRequired(t *testing.T) { assert.Nil(t, err) } -func TestHandleMoreEvidenceRequiredWhenGetOneByUIDError(t *testing.T) { +func TestHandleMoreEvidenceRequiredWhenOneByUIDError(t *testing.T) { ctx := context.Background() event := events.CloudWatchEvent{ DetailType: "more-evidence-required", @@ -284,7 +284,7 @@ func TestHandleMoreEvidenceRequiredWhenGetOneByUIDError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(expectedError) err := handleMoreEvidenceRequired(ctx, client, event) @@ -300,7 +300,7 @@ func TestHandleMoreEvidenceRequiredWhenPKMissing(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{}) json.Unmarshal(b, v) @@ -321,14 +321,14 @@ func TestHandleMoreEvidenceRequiredWhenGetError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(expectedError) err := handleMoreEvidenceRequired(ctx, client, event) @@ -344,14 +344,14 @@ func TestHandleMoreEvidenceRequiredWhenPutError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -374,14 +374,14 @@ func TestHandleFeeDenied(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) @@ -395,7 +395,7 @@ func TestHandleFeeDenied(t *testing.T) { assert.Nil(t, err) } -func TestHandleFeeDeniedWhenGetOneByUIDError(t *testing.T) { +func TestHandleFeeDeniedWhenOneByUIDError(t *testing.T) { ctx := context.Background() event := events.CloudWatchEvent{ DetailType: "fee-denied", @@ -404,7 +404,7 @@ func TestHandleFeeDeniedWhenGetOneByUIDError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(expectedError) err := handleFeeDenied(ctx, client, event) @@ -420,7 +420,7 @@ func TestHandleFeeDeniedWhenPKMissing(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{}) json.Unmarshal(b, v) @@ -441,14 +441,14 @@ func TestHandleFeeDeniedWhenGetError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(expectedError) err := handleFeeDenied(ctx, client, event) @@ -464,14 +464,14 @@ func TestHandleFeeDeniedWhenPutError(t *testing.T) { client := newMockDynamodbClient(t) client. - On("GetOneByUID", ctx, "M-1111-2222-3333", mock.Anything). + On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(func(ctx context.Context, uid string, v interface{}) error { b, _ := json.Marshal(dynamo.Key{PK: "LPA#123", SK: "#DONOR#456"}) json.Unmarshal(b, v) return nil }) client. - On("Get", ctx, "LPA#123", "#DONOR#456", mock.Anything). + On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(func(ctx context.Context, pk, sk string, v interface{}) error { b, _ := json.Marshal(page.Lpa{PK: "LPA#123", SK: "#DONOR#456", Tasks: page.Tasks{PayForLpa: actor.PaymentTaskPending}}) json.Unmarshal(b, v) diff --git a/cmd/event-received/mock_dynamodbClient_test.go b/cmd/event-received/mock_dynamodbClient_test.go index 2ae76ea441..726e121708 100644 --- a/cmd/event-received/mock_dynamodbClient_test.go +++ b/cmd/event-received/mock_dynamodbClient_test.go @@ -13,8 +13,8 @@ type mockDynamodbClient struct { mock.Mock } -// Get provides a mock function with given fields: ctx, pk, sk, v -func (_m *mockDynamodbClient) Get(ctx context.Context, pk string, sk string, v interface{}) error { +// One provides a mock function with given fields: ctx, pk, sk, v +func (_m *mockDynamodbClient) One(ctx context.Context, pk string, sk string, v interface{}) error { ret := _m.Called(ctx, pk, sk, v) var r0 error @@ -27,13 +27,13 @@ func (_m *mockDynamodbClient) Get(ctx context.Context, pk string, sk string, v i return r0 } -// GetOneByUID provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockDynamodbClient) GetOneByUID(_a0 context.Context, _a1 string, _a2 interface{}) error { - ret := _m.Called(_a0, _a1, _a2) +// OneByUID provides a mock function with given fields: ctx, uid, v +func (_m *mockDynamodbClient) OneByUID(ctx context.Context, uid string, v interface{}) error { + ret := _m.Called(ctx, uid, v) var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok { - r0 = rf(_a0, _a1, _a2) + r0 = rf(ctx, uid, v) } else { r0 = ret.Error(0) } @@ -41,13 +41,13 @@ func (_m *mockDynamodbClient) GetOneByUID(_a0 context.Context, _a1 string, _a2 i return r0 } -// Put provides a mock function with given fields: _a0, _a1 -func (_m *mockDynamodbClient) Put(_a0 context.Context, _a1 interface{}) error { - ret := _m.Called(_a0, _a1) +// Put provides a mock function with given fields: ctx, v +func (_m *mockDynamodbClient) Put(ctx context.Context, v interface{}) error { + ret := _m.Called(ctx, v) var r0 error if rf, ok := ret.Get(0).(func(context.Context, interface{}) error); ok { - r0 = rf(_a0, _a1) + r0 = rf(ctx, v) } else { r0 = ret.Error(0) } diff --git a/cypress/e2e/donor/choose-replacement-attorneys-summary.cy.js b/cypress/e2e/donor/choose-replacement-attorneys-summary.cy.js index de33c2ec0f..56b637afea 100644 --- a/cypress/e2e/donor/choose-replacement-attorneys-summary.cy.js +++ b/cypress/e2e/donor/choose-replacement-attorneys-summary.cy.js @@ -15,7 +15,7 @@ describe('Choose replacement attorneys summary', () => { cy.contains('2 RICHMOND PLACE'); cy.contains('B14 7ED'); - cy.contains('Joan Smith'); + cy.contains('Robin Redcar'); cy.contains('2 January 2000'); cy.visitLpa('/task-list') @@ -26,18 +26,12 @@ describe('Choose replacement attorneys summary', () => { cy.checkA11yApp(); cy.get('#replacement-name-1').contains('a', 'Change').click(); - - cy.url().should('contain', '/choose-replacement-attorneys'); - cy.url().should('contain', 'from=/choose-replacement-attorneys-summary'); - cy.url().should('match', /id=\w*/); - cy.get('#f-first-names').clear().type('Mark'); cy.contains('button', 'Save and continue').click(); cy.url().should('contain', '/choose-replacement-attorneys-summary'); - - cy.contains('Mark Smith'); + cy.contains('Mark Jones'); }); it('can amend attorney address', () => { @@ -45,29 +39,11 @@ describe('Choose replacement attorneys summary', () => { cy.get('#replacement-address-2').contains('a', 'Change').click(); - cy.url().should('contain', '/choose-replacement-attorneys-address'); - cy.url().should('contain', 'from=/choose-replacement-attorneys-summary'); - cy.url().should('match', /id=\w*/); - - cy.contains('label', 'Enter a new address').click(); - cy.contains('button', 'Continue').click(); - - cy.get('#f-lookup-postcode').type('B14 7ED'); - cy.contains('button', 'Find address').click(); - - cy.get('#f-select-address').select('4 RICHMOND PLACE, BIRMINGHAM, B14 7ED'); - cy.contains('button', 'Continue').click(); - - cy.url().should('contain', '/choose-replacement-attorneys-address'); - cy.get('#f-address-line-1').should('have.value', '4 RICHMOND PLACE'); + cy.get('#f-address-line-1').clear().type('4 RICHMOND PLACE'); cy.contains('button', 'Continue').click(); cy.url().should('contain', '/choose-replacement-attorneys-summary'); - cy.contains('dd', '4 RICHMOND PLACE'); - - cy.visitLpa('/task-list') - cy.contains('a', 'Choose your replacement attorneys').parent().parent().contains('Completed (2)') }); it('can add another attorney from summary page', () => { @@ -109,7 +85,7 @@ describe('Choose replacement attorneys summary', () => { cy.contains('B14 7ED'); }); - it.only('can remove an attorney', () => { + it('can remove an attorney', () => { cy.checkA11yApp(); cy.get('#remove-replacement-1').contains('a', 'Remove').click(); diff --git a/cypress/e2e/donor/dashboard.cy.js b/cypress/e2e/donor/dashboard.cy.js index b94c7e198f..a76526f17b 100644 --- a/cypress/e2e/donor/dashboard.cy.js +++ b/cypress/e2e/donor/dashboard.cy.js @@ -13,15 +13,10 @@ describe('Dashboard', () => { cy.url().should('contain', '/task-list'); }); - it('can create another', () => { + it('can create another reusing some previous details', () => { cy.contains('button', 'Create another LPA').click(); - cy.get('#f-first-names').type('Jane'); - cy.get('#f-last-name').type('Smith'); - cy.get('#f-date-of-birth').type('2'); - cy.get('#f-date-of-birth-month').type('3'); - cy.get('#f-date-of-birth-year').type('1990'); - cy.get('#f-can-sign').check(); + cy.get('#f-first-names').clear().type('Jane'); cy.contains('button', 'Continue').click(); cy.get('#f-lookup-postcode').type('B14 7ED'); diff --git a/docker/localstack/dynamodb-lpa-gsi-schema.json b/docker/localstack/dynamodb-lpa-gsi-schema.json index 626f5978a9..ff5079f56b 100644 --- a/docker/localstack/dynamodb-lpa-gsi-schema.json +++ b/docker/localstack/dynamodb-lpa-gsi-schema.json @@ -1,7 +1,7 @@ [ { - "IndexName": "ActorIndex", - "KeySchema": [{"AttributeName":"SK","KeyType":"HASH"}], + "IndexName": "ActorUpdatedAtIndex", + "KeySchema": [{"AttributeName":"SK","KeyType":"HASH"},{"AttributeName":"UpdatedAt","KeyType":"Range"}], "Projection": { "ProjectionType":"ALL" }, diff --git a/docker/localstack/localstack-init.sh b/docker/localstack/localstack-init.sh index 504d465aff..85a6b83443 100644 --- a/docker/localstack/localstack-init.sh +++ b/docker/localstack/localstack-init.sh @@ -16,8 +16,12 @@ awslocal secretsmanager create-secret --name "yoti-private-key" --secret-string awslocal secretsmanager create-secret --name "gov-uk-notify-api-key" --secret-string "extremely_fake-a-b-c-d-e-f-g-h-i-j" echo 'creating tables' -awslocal dynamodb create-table --table-name lpas --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S AttributeName=UID,AttributeType=S --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=1000,WriteCapacityUnits=1000 --global-secondary-indexes file://dynamodb-lpa-gsi-schema.json -awslocal dynamodb create-table --table-name reduced-fees --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=1000,WriteCapacityUnits=1000 +awslocal dynamodb create-table \ + --table-name lpas \ + --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S AttributeName=UID,AttributeType=S AttributeName=UpdatedAt,AttributeType=S \ + --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \ + --provisioned-throughput ReadCapacityUnits=1000,WriteCapacityUnits=1000 \ + --global-secondary-indexes file://dynamodb-lpa-gsi-schema.json echo 'creating bucket' awslocal s3api create-bucket --bucket evidence --create-bucket-configuration LocationConstraint=eu-west-1 diff --git a/internal/app/app.go b/internal/app/app.go index e9e90739a1..cd7b33e789 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -39,11 +39,12 @@ type Logger interface { //go:generate mockery --testonly --inpackage --name DynamoClient --structname mockDynamoClient type DynamoClient interface { - Get(ctx context.Context, pk, sk string, v interface{}) error + One(ctx context.Context, pk, sk string, v interface{}) error + OneByPartialSk(ctx context.Context, pk, partialSk string, v interface{}) error + LatestForActor(ctx context.Context, sk string, v interface{}) error + AllForActor(ctx context.Context, sk string, v interface{}) error + AllByKeys(ctx context.Context, pks []dynamo.Key) ([]map[string]types.AttributeValue, error) Put(ctx context.Context, v interface{}) error - GetOneByPartialSk(ctx context.Context, pk, partialSk string, v interface{}) error - GetAllByGsi(ctx context.Context, gsi, sk string, v interface{}) error - GetAllByKeys(ctx context.Context, pks []dynamo.Key) ([]map[string]types.AttributeValue, error) Create(ctx context.Context, v interface{}) error } diff --git a/internal/app/attorney_store.go b/internal/app/attorney_store.go index e606e918c2..b5162d1be3 100644 --- a/internal/app/attorney_store.go +++ b/internal/app/attorney_store.go @@ -41,6 +41,7 @@ func (s *attorneyStore) Create(ctx context.Context, donorSessionID, attorneyID s SK: subKey(data.SessionID), DonorKey: donorKey(donorSessionID), ActorType: actor.TypeAttorney, + UpdatedAt: s.now(), }); err != nil { return nil, err } @@ -48,22 +49,6 @@ func (s *attorneyStore) Create(ctx context.Context, donorSessionID, attorneyID s return attorney, err } -func (s *attorneyStore) GetAll(ctx context.Context) ([]*actor.AttorneyProvidedDetails, error) { - data, err := page.SessionDataFromContext(ctx) - if err != nil { - return nil, err - } - - if data.SessionID == "" { - return nil, errors.New("attorneyStore.GetAll requires SessionID") - } - - var items []*actor.AttorneyProvidedDetails - err = s.dynamoClient.GetAllByGsi(ctx, "ActorIndex", attorneyKey(data.SessionID), &items) - - return items, err -} - func (s *attorneyStore) Get(ctx context.Context) (*actor.AttorneyProvidedDetails, error) { data, err := page.SessionDataFromContext(ctx) if err != nil { @@ -75,7 +60,7 @@ func (s *attorneyStore) Get(ctx context.Context) (*actor.AttorneyProvidedDetails } var attorney actor.AttorneyProvidedDetails - err = s.dynamoClient.Get(ctx, lpaKey(data.LpaID), attorneyKey(data.SessionID), &attorney) + err = s.dynamoClient.One(ctx, lpaKey(data.LpaID), attorneyKey(data.SessionID), &attorney) return &attorney, err } diff --git a/internal/app/attorney_store_test.go b/internal/app/attorney_store_test.go index d7a3d513a2..a01661102f 100644 --- a/internal/app/attorney_store_test.go +++ b/internal/app/attorney_store_test.go @@ -24,7 +24,7 @@ func TestAttorneyStoreCreate(t *testing.T) { On("Create", ctx, details). Return(nil) dynamoClient. - On("Create", ctx, lpaLink{PK: "LPA#123", SK: "#SUB#456", DonorKey: "#DONOR#session-id", ActorType: actor.TypeAttorney}). + On("Create", ctx, lpaLink{PK: "LPA#123", SK: "#SUB#456", DonorKey: "#DONOR#session-id", ActorType: actor.TypeAttorney, UpdatedAt: now}). Return(nil) attorneyStore := &attorneyStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} @@ -102,46 +102,12 @@ func TestAttorneyStoreCreateWhenCreateError(t *testing.T) { } } -func TestAttorneyStoreGetAll(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "session-id"}) - attorney := &actor.AttorneyProvidedDetails{LpaID: "123"} - - dynamoClient := newMockDynamoClient(t) - dynamoClient. - ExpectGetAllByGsi(ctx, "ActorIndex", "#ATTORNEY#session-id", - []any{attorney}, nil) - - attorneyStore := &attorneyStore{dynamoClient: dynamoClient, now: nil} - - attorneys, err := attorneyStore.GetAll(ctx) - assert.Nil(t, err) - assert.Equal(t, []*actor.AttorneyProvidedDetails{attorney}, attorneys) -} - -func TestAttorneyStoreGetAllWhenSessionMissing(t *testing.T) { - ctx := context.Background() - - attorneyStore := &attorneyStore{} - - _, err := attorneyStore.GetAll(ctx) - assert.Equal(t, page.SessionMissingError{}, err) -} - -func TestAttorneyStoreGetAllWhenMissingSessionID(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{}) - - attorneyStore := &attorneyStore{} - - _, err := attorneyStore.GetAll(ctx) - assert.NotNil(t, err) -} - func TestAttorneyStoreGet(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, "LPA#123", "#ATTORNEY#456", + ExpectOne(ctx, "LPA#123", "#ATTORNEY#456", &actor.AttorneyProvidedDetails{LpaID: "123"}, nil) attorneyStore := &attorneyStore{dynamoClient: dynamoClient, now: nil} @@ -183,7 +149,7 @@ func TestAttorneyStoreGetOnError(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, "LPA#123", "#ATTORNEY#456", + ExpectOne(ctx, "LPA#123", "#ATTORNEY#456", &actor.AttorneyProvidedDetails{LpaID: "123"}, expectedError) attorneyStore := &attorneyStore{dynamoClient: dynamoClient, now: nil} diff --git a/internal/app/certificate_provider_store.go b/internal/app/certificate_provider_store.go index 458b3d8679..ece8416f41 100644 --- a/internal/app/certificate_provider_store.go +++ b/internal/app/certificate_provider_store.go @@ -39,6 +39,7 @@ func (s *certificateProviderStore) Create(ctx context.Context, donorSessionID st SK: subKey(data.SessionID), DonorKey: donorKey(donorSessionID), ActorType: actor.TypeCertificateProvider, + UpdatedAt: s.now(), }); err != nil { return nil, err } @@ -46,22 +47,6 @@ func (s *certificateProviderStore) Create(ctx context.Context, donorSessionID st return cp, err } -func (s *certificateProviderStore) GetAll(ctx context.Context) ([]*actor.CertificateProviderProvidedDetails, error) { - data, err := page.SessionDataFromContext(ctx) - if err != nil { - return nil, err - } - - if data.SessionID == "" { - return nil, errors.New("certificateProviderStore.GetAll requires SessionID") - } - - var items []*actor.CertificateProviderProvidedDetails - err = s.dynamoClient.GetAllByGsi(ctx, "ActorIndex", certificateProviderKey(data.SessionID), &items) - - return items, err -} - func (s *certificateProviderStore) GetAny(ctx context.Context) (*actor.CertificateProviderProvidedDetails, error) { data, err := page.SessionDataFromContext(ctx) if err != nil { @@ -73,7 +58,7 @@ func (s *certificateProviderStore) GetAny(ctx context.Context) (*actor.Certifica } var certificateProvider actor.CertificateProviderProvidedDetails - err = s.dynamoClient.GetOneByPartialSk(ctx, lpaKey(data.LpaID), "#CERTIFICATE_PROVIDER#", &certificateProvider) + err = s.dynamoClient.OneByPartialSk(ctx, lpaKey(data.LpaID), "#CERTIFICATE_PROVIDER#", &certificateProvider) return &certificateProvider, err } @@ -89,7 +74,7 @@ func (s *certificateProviderStore) Get(ctx context.Context) (*actor.CertificateP } var certificateProvider actor.CertificateProviderProvidedDetails - err = s.dynamoClient.Get(ctx, lpaKey(data.LpaID), certificateProviderKey(data.SessionID), &certificateProvider) + err = s.dynamoClient.One(ctx, lpaKey(data.LpaID), certificateProviderKey(data.SessionID), &certificateProvider) return &certificateProvider, err } diff --git a/internal/app/certificate_provider_store_test.go b/internal/app/certificate_provider_store_test.go index dc3154edec..55140238c3 100644 --- a/internal/app/certificate_provider_store_test.go +++ b/internal/app/certificate_provider_store_test.go @@ -22,7 +22,7 @@ func TestCertificateProviderStoreCreate(t *testing.T) { On("Create", ctx, details). Return(nil) dynamoClient. - On("Create", ctx, lpaLink{PK: "LPA#123", SK: "#SUB#456", DonorKey: "#DONOR#session-id", ActorType: actor.TypeCertificateProvider}). + On("Create", ctx, lpaLink{PK: "LPA#123", SK: "#SUB#456", DonorKey: "#DONOR#session-id", ActorType: actor.TypeCertificateProvider, UpdatedAt: now}). Return(nil) certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} @@ -98,46 +98,12 @@ func TestCertificateProviderStoreCreateWhenCreateError(t *testing.T) { } } -func TestCertificateProviderStoreGetAll(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "session-id"}) - certificateProvider := &actor.CertificateProviderProvidedDetails{LpaID: "123"} - - dynamoClient := newMockDynamoClient(t) - dynamoClient. - ExpectGetAllByGsi(ctx, "ActorIndex", "#CERTIFICATE_PROVIDER#session-id", - []any{certificateProvider}, nil) - - certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: nil} - - certificateProviders, err := certificateProviderStore.GetAll(ctx) - assert.Nil(t, err) - assert.Equal(t, []*actor.CertificateProviderProvidedDetails{certificateProvider}, certificateProviders) -} - -func TestCertificateProviderStoreGetAllWhenSessionMissing(t *testing.T) { - ctx := context.Background() - - certificateProviderStore := &certificateProviderStore{} - - _, err := certificateProviderStore.GetAll(ctx) - assert.Equal(t, page.SessionMissingError{}, err) -} - -func TestCertificateProviderStoreGetAllWhenMissingSessionID(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{}) - - certificateProviderStore := &certificateProviderStore{} - - _, err := certificateProviderStore.GetAll(ctx) - assert.NotNil(t, err) -} - func TestCertificateProviderStoreGetAny(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "123"}) dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGetOneByPartialSk(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, nil) + ExpectOneByPartialSk(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, nil) certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: nil} @@ -169,7 +135,7 @@ func TestCertificateProviderStoreGetAnyOnError(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGetOneByPartialSk(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, expectedError) + ExpectOneByPartialSk(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, expectedError) certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: nil} @@ -182,7 +148,7 @@ func TestCertificateProviderStoreGet(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#456", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, nil) + ExpectOne(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#456", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, nil) certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: nil} @@ -223,7 +189,7 @@ func TestCertificateProviderStoreGetOnError(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#456", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, expectedError) + ExpectOne(ctx, "LPA#123", "#CERTIFICATE_PROVIDER#456", &actor.CertificateProviderProvidedDetails{LpaID: "123"}, expectedError) certificateProviderStore := &certificateProviderStore{dynamoClient: dynamoClient, now: nil} diff --git a/internal/app/dashboard_store.go b/internal/app/dashboard_store.go index 1222d8a11f..b9b27483a1 100644 --- a/internal/app/dashboard_store.go +++ b/internal/app/dashboard_store.go @@ -5,6 +5,7 @@ import ( "errors" "slices" "strings" + "time" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" @@ -22,6 +23,8 @@ type lpaLink struct { DonorKey string // ActorType is the type for the current user ActorType actor.Type + // UpdatedAt is set to allow this data to be queried from ActorUpdatedAtIndex + UpdatedAt time.Time } type dashboardStore struct { @@ -55,7 +58,7 @@ func (s *dashboardStore) GetAll(ctx context.Context) (donor, attorney, certifica } var links []lpaLink - if err := s.dynamoClient.GetAllByGsi(ctx, "ActorIndex", subKey(data.SessionID), &links); err != nil { + if err := s.dynamoClient.AllForActor(ctx, subKey(data.SessionID), &links); err != nil { return nil, nil, nil, err } @@ -80,7 +83,7 @@ func (s *dashboardStore) GetAll(ctx context.Context) (donor, attorney, certifica return nil, nil, nil, nil } - lpasOrProvidedDetails, err := s.dynamoClient.GetAllByKeys(ctx, searchKeys) + lpasOrProvidedDetails, err := s.dynamoClient.AllByKeys(ctx, searchKeys) if err != nil { return nil, nil, nil, err } diff --git a/internal/app/dashboard_store_test.go b/internal/app/dashboard_store_test.go index 2e9a01bcd1..ce95f2f6a6 100644 --- a/internal/app/dashboard_store_test.go +++ b/internal/app/dashboard_store_test.go @@ -51,7 +51,7 @@ func TestDashboardStoreGetAll(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGetAllByGsi(ctx, "ActorIndex", "#SUB#an-id", + dynamoClient.ExpectAllForActor(ctx, "#SUB#an-id", []lpaLink{ {PK: "LPA#123", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor}, {PK: "LPA#456", SK: "#SUB#an-id", DonorKey: "#DONOR#another-id", ActorType: actor.TypeCertificateProvider}, @@ -59,7 +59,7 @@ func TestDashboardStoreGetAll(t *testing.T) { {PK: "LPA#0", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor}, {PK: "LPA#999", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor}, }, nil) - dynamoClient.ExpectGetAllByKeys(ctx, []dynamo.Key{ + dynamoClient.ExpectAllByKeys(ctx, []dynamo.Key{ {PK: "LPA#123", SK: "#DONOR#an-id"}, {PK: "LPA#456", SK: "#DONOR#another-id"}, {PK: "LPA#456", SK: "#CERTIFICATE_PROVIDER#an-id"}, @@ -90,7 +90,7 @@ func TestDashboardStoreGetAllWhenNone(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGetAllByGsi(ctx, "ActorIndex", "#SUB#an-id", + dynamoClient.ExpectAllForActor(ctx, "#SUB#an-id", []map[string]any{}, nil) dashboardStore := &dashboardStore{dynamoClient: dynamoClient} @@ -101,3 +101,32 @@ func TestDashboardStoreGetAllWhenNone(t *testing.T) { assert.Nil(t, attorney) assert.Nil(t, certificateProvider) } + +func TestDashboardStoreGetAllWhenAllForActorErrors(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.ExpectAllForActor(ctx, "#SUB#an-id", + []lpaLink{}, expectedError) + + dashboardStore := &dashboardStore{dynamoClient: dynamoClient} + + _, _, _, err := dashboardStore.GetAll(ctx) + assert.Equal(t, err, expectedError) +} + +func TestDashboardStoreGetAllWhenAllByKeysErrors(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + dynamoClient := newMockDynamoClient(t) + dynamoClient.ExpectAllForActor(ctx, "#SUB#an-id", + []lpaLink{{PK: "LPA#123", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor}}, nil) + dynamoClient.ExpectAllByKeys(ctx, []dynamo.Key{ + {PK: "LPA#123", SK: "#DONOR#an-id"}, + }, nil, expectedError) + + dashboardStore := &dashboardStore{dynamoClient: dynamoClient} + + _, _, _, err := dashboardStore.GetAll(ctx) + assert.Equal(t, expectedError, err) +} diff --git a/internal/app/donor_store.go b/internal/app/donor_store.go index 416c6c1786..d0021b37d8 100644 --- a/internal/app/donor_store.go +++ b/internal/app/donor_store.go @@ -3,7 +3,6 @@ package app import ( "context" "errors" - "slices" "time" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -50,7 +49,6 @@ func (s *donorStore) Create(ctx context.Context) (*page.Lpa, error) { SK: donorKey(data.SessionID), ID: lpaID, CreatedAt: s.now(), - UpdatedAt: s.now(), } if err := s.dynamoClient.Create(ctx, lpa); err != nil { @@ -61,6 +59,7 @@ func (s *donorStore) Create(ctx context.Context) (*page.Lpa, error) { SK: subKey(data.SessionID), DonorKey: donorKey(data.SessionID), ActorType: actor.TypeDonor, + UpdatedAt: s.now(), }); err != nil { return nil, err } @@ -68,29 +67,6 @@ func (s *donorStore) Create(ctx context.Context) (*page.Lpa, error) { return lpa, err } -func (s *donorStore) GetAll(ctx context.Context) ([]*page.Lpa, error) { - data, err := page.SessionDataFromContext(ctx) - if err != nil { - return nil, err - } - - if data.SessionID == "" { - return nil, errors.New("donorStore.GetAll requires SessionID") - } - - var items []*page.Lpa - err = s.dynamoClient.GetAllByGsi(ctx, "ActorIndex", donorKey(data.SessionID), &items) - - slices.SortFunc(items, func(a, b *page.Lpa) int { - if a.UpdatedAt.After(b.UpdatedAt) { - return -1 - } - return 1 - }) - - return items, err -} - func (s *donorStore) GetAny(ctx context.Context) (*page.Lpa, error) { data, err := page.SessionDataFromContext(ctx) if err != nil { @@ -102,7 +78,7 @@ func (s *donorStore) GetAny(ctx context.Context) (*page.Lpa, error) { } var lpa *page.Lpa - if err := s.dynamoClient.GetOneByPartialSk(ctx, lpaKey(data.LpaID), "#DONOR#", &lpa); err != nil { + if err := s.dynamoClient.OneByPartialSk(ctx, lpaKey(data.LpaID), "#DONOR#", &lpa); err != nil { return nil, err } @@ -120,13 +96,29 @@ func (s *donorStore) Get(ctx context.Context) (*page.Lpa, error) { } var lpa *page.Lpa - err = s.dynamoClient.Get(ctx, lpaKey(data.LpaID), donorKey(data.SessionID), &lpa) + err = s.dynamoClient.One(ctx, lpaKey(data.LpaID), donorKey(data.SessionID), &lpa) return lpa, err } -func (s *donorStore) Put(ctx context.Context, lpa *page.Lpa) error { - lpa.UpdatedAt = s.now() +func (s *donorStore) Latest(ctx context.Context) (*page.Lpa, error) { + data, err := page.SessionDataFromContext(ctx) + if err != nil { + return nil, err + } + + if data.SessionID == "" { + return nil, errors.New("donorStore.Get requires SessionID") + } + + var lpa *page.Lpa + if err := s.dynamoClient.LatestForActor(ctx, donorKey(data.SessionID), &lpa); err != nil { + return nil, err + } + + return lpa, nil +} +func (s *donorStore) Put(ctx context.Context, lpa *page.Lpa) error { if lpa.UID == "" && !lpa.Type.Empty() { resp, err := s.uidClient.CreateCase(ctx, &uid.CreateCaseRequestBody{ Type: lpa.Type.String(), @@ -143,6 +135,12 @@ func (s *donorStore) Put(ctx context.Context, lpa *page.Lpa) error { } } + // By not setting UpdatedAt until a UID exists, queries for SK=#DONOR#xyz on + // ActorUpdatedAtIndex will not return UID-less LPAs. + if lpa.UID != "" { + lpa.UpdatedAt = s.now() + } + if lpa.UID != "" && !lpa.HasSentApplicationUpdatedEvent { if err := s.eventClient.Send(ctx, "application-updated", applicationUpdatedEvent{ UID: lpa.UID, diff --git a/internal/app/donor_store_test.go b/internal/app/donor_store_test.go index b75a2c43ca..dbb4cfc233 100644 --- a/internal/app/donor_store_test.go +++ b/internal/app/donor_store_test.go @@ -19,9 +19,9 @@ import ( var expectedError = errors.New("err") -func (m *mockDynamoClient) ExpectGet(ctx, pk, sk, data interface{}, err error) { +func (m *mockDynamoClient) ExpectOne(ctx, pk, sk, data interface{}, err error) { m. - On("Get", ctx, pk, sk, mock.Anything). + On("One", ctx, pk, sk, mock.Anything). Return(func(ctx context.Context, pk, partialSk string, v interface{}) error { b, _ := json.Marshal(data) json.Unmarshal(b, v) @@ -29,9 +29,9 @@ func (m *mockDynamoClient) ExpectGet(ctx, pk, sk, data interface{}, err error) { }) } -func (m *mockDynamoClient) ExpectGetOneByPartialSk(ctx, pk, partialSk, data interface{}, err error) { +func (m *mockDynamoClient) ExpectOneByPartialSk(ctx, pk, partialSk, data interface{}, err error) { m. - On("GetOneByPartialSk", ctx, pk, partialSk, mock.Anything). + On("OneByPartialSk", ctx, pk, partialSk, mock.Anything). Return(func(ctx context.Context, pk, partialSk string, v interface{}) error { b, _ := json.Marshal(data) json.Unmarshal(b, v) @@ -39,122 +39,131 @@ func (m *mockDynamoClient) ExpectGetOneByPartialSk(ctx, pk, partialSk, data inte }) } -func (m *mockDynamoClient) ExpectGetAllByGsi(ctx, gsi, sk, data interface{}, err error) { +func (m *mockDynamoClient) ExpectAllForActor(ctx, sk, data interface{}, err error) { m. - On("GetAllByGsi", ctx, gsi, sk, mock.Anything). - Return(func(ctx context.Context, gsi, pk string, v interface{}) error { + On("AllForActor", ctx, sk, mock.Anything). + Return(func(ctx context.Context, pk string, v interface{}) error { b, _ := json.Marshal(data) json.Unmarshal(b, v) return err }) } -func (m *mockDynamoClient) ExpectGetAllByKeys(ctx context.Context, keys []dynamo.Key, data interface{}, err error) { +func (m *mockDynamoClient) ExpectLatestForActor(ctx, sk, data interface{}, err error) { m. - On("GetAllByKeys", ctx, keys, mock.Anything). + On("LatestForActor", ctx, sk, mock.Anything). + Return(func(ctx context.Context, sk string, v interface{}) error { + b, _ := json.Marshal(data) + json.Unmarshal(b, v) + return err + }) +} + +func (m *mockDynamoClient) ExpectAllByKeys(ctx context.Context, keys []dynamo.Key, data interface{}, err error) { + m. + On("AllByKeys", ctx, keys, mock.Anything). Return(data, err) } -func TestDonorStoreGetAll(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) - lpa123 := &page.Lpa{ID: "123", UpdatedAt: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)} - lpa456 := &page.Lpa{ID: "456", UpdatedAt: time.Date(2002, time.January, 1, 0, 0, 0, 0, time.UTC)} - lpa789 := &page.Lpa{ID: "789", UpdatedAt: time.Date(2001, time.January, 1, 0, 0, 0, 0, time.UTC)} +func TestDonorStoreGetAny(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGetAllByGsi(ctx, "ActorIndex", "#DONOR#an-id", - []any{lpa123, lpa456, lpa789}, nil) + dynamoClient.ExpectOneByPartialSk(ctx, "LPA#an-id", "#DONOR#", &page.Lpa{ID: "an-id"}, nil) - donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: nil} + donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} - result, err := donorStore.GetAll(ctx) + lpa, err := donorStore.GetAny(ctx) assert.Nil(t, err) - assert.Equal(t, []*page.Lpa{lpa456, lpa789, lpa123}, result) + assert.Equal(t, &page.Lpa{ID: "an-id"}, lpa) } -func TestDonorStoreGetAllWithSessionMissing(t *testing.T) { +func TestDonorStoreGetAnyWithSessionMissing(t *testing.T) { ctx := context.Background() donorStore := &donorStore{dynamoClient: nil, uuidString: func() string { return "10100000" }} - _, err := donorStore.GetAll(ctx) + _, err := donorStore.GetAny(ctx) assert.Equal(t, page.SessionMissingError{}, err) } -func TestDonorStoreGetAllWhenMissingSessionID(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{}) +func TestDonorStoreGetAnyWhenDataStoreError(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id"}) - donorStore := &donorStore{dynamoClient: nil, uuidString: func() string { return "10100000" }} + dynamoClient := newMockDynamoClient(t) + dynamoClient.ExpectOneByPartialSk(ctx, "LPA#an-id", "#DONOR#", &page.Lpa{ID: "an-id"}, expectedError) - _, err := donorStore.GetAll(ctx) - assert.NotNil(t, err) + donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} + + _, err := donorStore.GetAny(ctx) + assert.Equal(t, expectedError, err) } -func TestDonorStoreGetAny(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id"}) +func TestDonorStoreGet(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGetOneByPartialSk(ctx, "LPA#an-id", "#DONOR#", &page.Lpa{ID: "an-id"}, nil) + dynamoClient.ExpectOne(ctx, "LPA#an-id", "#DONOR#456", &page.Lpa{ID: "an-id"}, nil) donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} - lpa, err := donorStore.GetAny(ctx) + lpa, err := donorStore.Get(ctx) assert.Nil(t, err) assert.Equal(t, &page.Lpa{ID: "an-id"}, lpa) } -func TestDonorStoreGetAnyWithSessionMissing(t *testing.T) { +func TestDonorStoreGetWithSessionMissing(t *testing.T) { ctx := context.Background() donorStore := &donorStore{dynamoClient: nil, uuidString: func() string { return "10100000" }} - _, err := donorStore.GetAny(ctx) + _, err := donorStore.Get(ctx) assert.Equal(t, page.SessionMissingError{}, err) } -func TestDonorStoreGetAnyWhenDataStoreError(t *testing.T) { - ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id"}) +func TestDonorStoreGetWhenDataStoreError(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGetOneByPartialSk(ctx, "LPA#an-id", "#DONOR#", &page.Lpa{ID: "an-id"}, expectedError) + dynamoClient.ExpectOne(ctx, "LPA#an-id", "#DONOR#456", &page.Lpa{ID: "an-id"}, expectedError) donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} - _, err := donorStore.GetAny(ctx) + _, err := donorStore.Get(ctx) assert.Equal(t, expectedError, err) } -func TestDonorStoreGet(t *testing.T) { +func TestDonorStoreLatest(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGet(ctx, "LPA#an-id", "#DONOR#456", &page.Lpa{ID: "an-id"}, nil) + dynamoClient.ExpectLatestForActor(ctx, "#DONOR#456", &page.Lpa{ID: "an-id"}, nil) donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} - lpa, err := donorStore.Get(ctx) + lpa, err := donorStore.Latest(ctx) assert.Nil(t, err) assert.Equal(t, &page.Lpa{ID: "an-id"}, lpa) } -func TestDonorStoreGetWithSessionMissing(t *testing.T) { +func TestDonorStoreLatestWithSessionMissing(t *testing.T) { ctx := context.Background() donorStore := &donorStore{dynamoClient: nil, uuidString: func() string { return "10100000" }} - _, err := donorStore.Get(ctx) + _, err := donorStore.Latest(ctx) assert.Equal(t, page.SessionMissingError{}, err) } -func TestDonorStoreGetWhenDataStoreError(t *testing.T) { +func TestDonorStoreLatestWhenDataStoreError(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGet(ctx, "LPA#an-id", "#DONOR#456", &page.Lpa{ID: "an-id"}, expectedError) + dynamoClient.ExpectLatestForActor(ctx, "#DONOR#456", &page.Lpa{ID: "an-id"}, expectedError) donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }} - _, err := donorStore.Get(ctx) + _, err := donorStore.Latest(ctx) assert.Equal(t, expectedError, err) } @@ -162,15 +171,32 @@ func TestDonorStorePut(t *testing.T) { ctx := context.Background() now := time.Now() - dynamoClient := newMockDynamoClient(t) - dynamoClient. - On("Put", ctx, &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", UpdatedAt: now}). - Return(nil) + testcases := map[string]struct { + input, saved *page.Lpa + }{ + "no uid": { + input: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true}, + saved: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true}, + }, + "with uid": { + input: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M"}, + saved: &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5", HasSentApplicationUpdatedEvent: true, UID: "M", UpdatedAt: now}, + }, + } - donorStore := &donorStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + dynamoClient := newMockDynamoClient(t) + dynamoClient. + On("Put", ctx, tc.saved). + Return(nil) - err := donorStore.Put(ctx, &page.Lpa{PK: "LPA#5", SK: "#DONOR#an-id", ID: "5"}) - assert.Nil(t, err) + donorStore := &donorStore{dynamoClient: dynamoClient, now: func() time.Time { return now }} + + err := donorStore.Put(ctx, tc.input) + assert.Nil(t, err) + }) + } } func TestDonorStorePutWhenError(t *testing.T) { @@ -709,14 +735,14 @@ func TestDonorStorePutWhenReducedFeeRequestedWhenError(t *testing.T) { func TestDonorStoreCreate(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) now := time.Now() - lpa := &page.Lpa{PK: "LPA#10100000", SK: "#DONOR#an-id", ID: "10100000", CreatedAt: now, UpdatedAt: now} + lpa := &page.Lpa{PK: "LPA#10100000", SK: "#DONOR#an-id", ID: "10100000", CreatedAt: now} dynamoClient := newMockDynamoClient(t) dynamoClient. On("Create", ctx, lpa). Return(nil) dynamoClient. - On("Create", ctx, lpaLink{PK: "LPA#10100000", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor}). + On("Create", ctx, lpaLink{PK: "LPA#10100000", SK: "#SUB#an-id", DonorKey: "#DONOR#an-id", ActorType: actor.TypeDonor, UpdatedAt: now}). Return(nil) donorStore := &donorStore{dynamoClient: dynamoClient, uuidString: func() string { return "10100000" }, now: func() time.Time { return now }} diff --git a/internal/app/evidence_received_store.go b/internal/app/evidence_received_store.go index bc701831bb..58f5248d19 100644 --- a/internal/app/evidence_received_store.go +++ b/internal/app/evidence_received_store.go @@ -23,7 +23,7 @@ func (s *evidenceReceivedStore) Get(ctx context.Context) (bool, error) { } var v any - if err := s.dynamoClient.Get(ctx, lpaKey(data.LpaID), "#EVIDENCE_RECEIVED", &v); err != nil { + if err := s.dynamoClient.One(ctx, lpaKey(data.LpaID), "#EVIDENCE_RECEIVED", &v); err != nil { if errors.Is(err, dynamo.NotFoundError{}) { return false, nil } diff --git a/internal/app/evidence_received_store_test.go b/internal/app/evidence_received_store_test.go index 5d08cb78c9..5cc798d216 100644 --- a/internal/app/evidence_received_store_test.go +++ b/internal/app/evidence_received_store_test.go @@ -13,7 +13,7 @@ func TestEvidenceReceivedStoreGet(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGet(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", nil, nil) + dynamoClient.ExpectOne(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", nil, nil) evidenceReceivedStore := &evidenceReceivedStore{dynamoClient: dynamoClient} @@ -26,7 +26,7 @@ func TestEvidenceReceivedStoreGetWhenFalse(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGet(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", nil, dynamo.NotFoundError{}) + dynamoClient.ExpectOne(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", nil, dynamo.NotFoundError{}) evidenceReceivedStore := &evidenceReceivedStore{dynamoClient: dynamoClient} @@ -48,7 +48,7 @@ func TestEvidenceReceivedStoreGetWhenDataStoreError(t *testing.T) { ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{LpaID: "an-id", SessionID: "456"}) dynamoClient := newMockDynamoClient(t) - dynamoClient.ExpectGet(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", &page.Lpa{ID: "an-id"}, expectedError) + dynamoClient.ExpectOne(ctx, "LPA#an-id", "#EVIDENCE_RECEIVED", &page.Lpa{ID: "an-id"}, expectedError) evidenceReceivedStore := &evidenceReceivedStore{dynamoClient: dynamoClient} diff --git a/internal/app/mock_DynamoClient_test.go b/internal/app/mock_DynamoClient_test.go index 40403a3fb0..7e4e969eb9 100644 --- a/internal/app/mock_DynamoClient_test.go +++ b/internal/app/mock_DynamoClient_test.go @@ -16,27 +16,39 @@ type mockDynamoClient struct { mock.Mock } -// Create provides a mock function with given fields: ctx, v -func (_m *mockDynamoClient) Create(ctx context.Context, v interface{}) error { - ret := _m.Called(ctx, v) +// AllByKeys provides a mock function with given fields: ctx, pks +func (_m *mockDynamoClient) AllByKeys(ctx context.Context, pks []dynamo.Key) ([]map[string]types.AttributeValue, error) { + ret := _m.Called(ctx, pks) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, interface{}) error); ok { - r0 = rf(ctx, v) + var r0 []map[string]types.AttributeValue + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []dynamo.Key) ([]map[string]types.AttributeValue, error)); ok { + return rf(ctx, pks) + } + if rf, ok := ret.Get(0).(func(context.Context, []dynamo.Key) []map[string]types.AttributeValue); ok { + r0 = rf(ctx, pks) } else { - r0 = ret.Error(0) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]map[string]types.AttributeValue) + } } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, []dynamo.Key) error); ok { + r1 = rf(ctx, pks) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } -// Get provides a mock function with given fields: ctx, pk, sk, v -func (_m *mockDynamoClient) Get(ctx context.Context, pk string, sk string, v interface{}) error { - ret := _m.Called(ctx, pk, sk, v) +// AllForActor provides a mock function with given fields: ctx, sk, v +func (_m *mockDynamoClient) AllForActor(ctx context.Context, sk string, v interface{}) error { + ret := _m.Called(ctx, sk, v) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}) error); ok { - r0 = rf(ctx, pk, sk, v) + if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok { + r0 = rf(ctx, sk, v) } else { r0 = ret.Error(0) } @@ -44,13 +56,13 @@ func (_m *mockDynamoClient) Get(ctx context.Context, pk string, sk string, v int return r0 } -// GetAllByGsi provides a mock function with given fields: ctx, gsi, sk, v -func (_m *mockDynamoClient) GetAllByGsi(ctx context.Context, gsi string, sk string, v interface{}) error { - ret := _m.Called(ctx, gsi, sk, v) +// Create provides a mock function with given fields: ctx, v +func (_m *mockDynamoClient) Create(ctx context.Context, v interface{}) error { + ret := _m.Called(ctx, v) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}) error); ok { - r0 = rf(ctx, gsi, sk, v) + if rf, ok := ret.Get(0).(func(context.Context, interface{}) error); ok { + r0 = rf(ctx, v) } else { r0 = ret.Error(0) } @@ -58,34 +70,36 @@ func (_m *mockDynamoClient) GetAllByGsi(ctx context.Context, gsi string, sk stri return r0 } -// GetAllByKeys provides a mock function with given fields: ctx, pks -func (_m *mockDynamoClient) GetAllByKeys(ctx context.Context, pks []dynamo.Key) ([]map[string]types.AttributeValue, error) { - ret := _m.Called(ctx, pks) +// LatestForActor provides a mock function with given fields: ctx, sk, v +func (_m *mockDynamoClient) LatestForActor(ctx context.Context, sk string, v interface{}) error { + ret := _m.Called(ctx, sk, v) - var r0 []map[string]types.AttributeValue - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []dynamo.Key) ([]map[string]types.AttributeValue, error)); ok { - return rf(ctx, pks) - } - if rf, ok := ret.Get(0).(func(context.Context, []dynamo.Key) []map[string]types.AttributeValue); ok { - r0 = rf(ctx, pks) + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok { + r0 = rf(ctx, sk, v) } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]map[string]types.AttributeValue) - } + r0 = ret.Error(0) } - if rf, ok := ret.Get(1).(func(context.Context, []dynamo.Key) error); ok { - r1 = rf(ctx, pks) + return r0 +} + +// One provides a mock function with given fields: ctx, pk, sk, v +func (_m *mockDynamoClient) One(ctx context.Context, pk string, sk string, v interface{}) error { + ret := _m.Called(ctx, pk, sk, v) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}) error); ok { + r0 = rf(ctx, pk, sk, v) } else { - r1 = ret.Error(1) + r0 = ret.Error(0) } - return r0, r1 + return r0 } -// GetOneByPartialSk provides a mock function with given fields: ctx, pk, partialSk, v -func (_m *mockDynamoClient) GetOneByPartialSk(ctx context.Context, pk string, partialSk string, v interface{}) error { +// OneByPartialSk provides a mock function with given fields: ctx, pk, partialSk, v +func (_m *mockDynamoClient) OneByPartialSk(ctx context.Context, pk string, partialSk string, v interface{}) error { ret := _m.Called(ctx, pk, partialSk, v) var r0 error diff --git a/internal/app/share_code_store.go b/internal/app/share_code_store.go index 3fced8d72c..9bc44e9376 100644 --- a/internal/app/share_code_store.go +++ b/internal/app/share_code_store.go @@ -23,7 +23,7 @@ func (s *shareCodeStore) Get(ctx context.Context, actorType actor.Type, shareCod return data, err } - err = s.dynamoClient.Get(ctx, pk, sk, &data) + err = s.dynamoClient.One(ctx, pk, sk, &data) return data, err } diff --git a/internal/app/share_code_store_test.go b/internal/app/share_code_store_test.go index d392233c8e..587cb19bd7 100644 --- a/internal/app/share_code_store_test.go +++ b/internal/app/share_code_store_test.go @@ -35,7 +35,7 @@ func TestShareCodeStoreGet(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, tc.pk, "#METADATA#123", + ExpectOne(ctx, tc.pk, "#METADATA#123", data, nil) shareCodeStore := &shareCodeStore{dynamoClient: dynamoClient} @@ -61,7 +61,7 @@ func TestShareCodeStoreGetOnError(t *testing.T) { dynamoClient := newMockDynamoClient(t) dynamoClient. - ExpectGet(ctx, "ATTORNEYSHARE#123", "#METADATA#123", + ExpectOne(ctx, "ATTORNEYSHARE#123", "#METADATA#123", data, expectedError) shareCodeStore := &shareCodeStore{dynamoClient: dynamoClient} diff --git a/internal/dynamo/client.go b/internal/dynamo/client.go index 475c32d09c..335d957153 100644 --- a/internal/dynamo/client.go +++ b/internal/dynamo/client.go @@ -10,6 +10,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" ) +const ( + uidIndex = "UidIndex" + actorUpdatedAtIndex = "ActorUpdatedAtIndex" +) + //go:generate mockery --testonly --inpackage --name dynamoDB --structname mockDynamoDB type dynamoDB interface { Query(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) @@ -40,7 +45,7 @@ func NewClient(cfg aws.Config, tableName string) (*Client, error) { return &Client{table: tableName, svc: dynamodb.NewFromConfig(cfg)}, nil } -func (c *Client) Get(ctx context.Context, pk, sk string, v interface{}) error { +func (c *Client) One(ctx context.Context, pk, sk string, v interface{}) error { result, err := c.svc.GetItem(ctx, &dynamodb.GetItemInput{ TableName: aws.String(c.table), Key: map[string]types.AttributeValue{ @@ -58,18 +63,37 @@ func (c *Client) Get(ctx context.Context, pk, sk string, v interface{}) error { return attributevalue.UnmarshalMap(result.Item, v) } -func (c *Client) GetAllByGsi(ctx context.Context, gsi, sk string, v interface{}) error { - skey, err := attributevalue.Marshal(sk) +func (c *Client) OneByUID(ctx context.Context, uid string, v interface{}) error { + response, err := c.svc.Query(ctx, &dynamodb.QueryInput{ + TableName: aws.String(c.table), + IndexName: aws.String(uidIndex), + ExpressionAttributeNames: map[string]string{"#UID": "UID"}, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":UID": &types.AttributeValueMemberS{Value: uid}, + }, + KeyConditionExpression: aws.String("#UID = :UID"), + }) + if err != nil { - return err + return fmt.Errorf("failed to query UID: %w", err) } + if len(response.Items) != 1 { + return fmt.Errorf("expected to resolve UID but got %d items", len(response.Items)) + } + + return attributevalue.UnmarshalMap(response.Items[0], v) +} + +func (c *Client) AllForActor(ctx context.Context, sk string, v interface{}) error { response, err := c.svc.Query(ctx, &dynamodb.QueryInput{ - TableName: aws.String(c.table), - IndexName: aws.String(gsi), - ExpressionAttributeNames: map[string]string{"#SK": "SK"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":SK": skey}, - KeyConditionExpression: aws.String("#SK = :SK"), + TableName: aws.String(c.table), + IndexName: aws.String(actorUpdatedAtIndex), + ExpressionAttributeNames: map[string]string{"#SK": "SK"}, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":SK": &types.AttributeValueMemberS{Value: sk}, + }, + KeyConditionExpression: aws.String("#SK = :SK"), }) if err != nil { return err @@ -78,26 +102,27 @@ func (c *Client) GetAllByGsi(ctx context.Context, gsi, sk string, v interface{}) return attributevalue.UnmarshalListOfMaps(response.Items, v) } -func (c *Client) GetOneByUID(ctx context.Context, uid string, v interface{}) error { - skey, err := attributevalue.Marshal(uid) - if err != nil { - return fmt.Errorf("failed to marshal UID: %w", err) - } - +func (c *Client) LatestForActor(ctx context.Context, sk string, v interface{}) error { response, err := c.svc.Query(ctx, &dynamodb.QueryInput{ - TableName: aws.String(c.table), - IndexName: aws.String("UidIndex"), - ExpressionAttributeNames: map[string]string{"#UID": "UID"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":UID": skey}, - KeyConditionExpression: aws.String("#UID = :UID"), + TableName: aws.String(c.table), + IndexName: aws.String(actorUpdatedAtIndex), + ExpressionAttributeNames: map[string]string{"#SK": "SK", "#UpdatedAt": "UpdatedAt"}, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":SK": &types.AttributeValueMemberS{Value: sk}, + // Specifying the condition UpdatedAt>2 filters out zero-value timestamps + ":UpdatedAt": &types.AttributeValueMemberS{Value: "2"}, + }, + KeyConditionExpression: aws.String("#SK = :SK and #UpdatedAt > :UpdatedAt"), + ScanIndexForward: aws.Bool(false), + Limit: aws.Int32(1), }) if err != nil { - return fmt.Errorf("failed to query UID: %w", err) + return err } - if len(response.Items) != 1 { - return fmt.Errorf("expected to resolve UID but got %d items", len(response.Items)) + if len(response.Items) == 0 { + return nil } return attributevalue.UnmarshalMap(response.Items[0], v) @@ -108,7 +133,7 @@ type Key struct { SK string } -func (c *Client) GetAllByKeys(ctx context.Context, keys []Key) ([]map[string]types.AttributeValue, error) { +func (c *Client) AllByKeys(ctx context.Context, keys []Key) ([]map[string]types.AttributeValue, error) { var keyAttrs []map[string]types.AttributeValue for _, key := range keys { keyAttrs = append(keyAttrs, map[string]types.AttributeValue{ @@ -131,22 +156,15 @@ func (c *Client) GetAllByKeys(ctx context.Context, keys []Key) ([]map[string]typ return result.Responses[c.table], nil } -func (c *Client) GetOneByPartialSk(ctx context.Context, pk, partialSk string, v interface{}) error { - pkey, err := attributevalue.Marshal(pk) - if err != nil { - return err - } - - partialSkey, err := attributevalue.Marshal(partialSk) - if err != nil { - return err - } - +func (c *Client) OneByPartialSk(ctx context.Context, pk, partialSk string, v interface{}) error { response, err := c.svc.Query(ctx, &dynamodb.QueryInput{ - TableName: aws.String(c.table), - ExpressionAttributeNames: map[string]string{"#PK": "PK", "#SK": "SK"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":PK": pkey, ":SK": partialSkey}, - KeyConditionExpression: aws.String("#PK = :PK and begins_with(#SK, :SK)"), + TableName: aws.String(c.table), + ExpressionAttributeNames: map[string]string{"#PK": "PK", "#SK": "SK"}, + ExpressionAttributeValues: map[string]types.AttributeValue{ + ":PK": &types.AttributeValueMemberS{Value: pk}, + ":SK": &types.AttributeValueMemberS{Value: partialSk}, + }, + KeyConditionExpression: aws.String("#PK = :PK and begins_with(#SK, :SK)"), }) if err != nil { diff --git a/internal/dynamo/client_test.go b/internal/dynamo/client_test.go index 1a9238d958..17e826e05a 100644 --- a/internal/dynamo/client_test.go +++ b/internal/dynamo/client_test.go @@ -17,7 +17,7 @@ import ( var expectedError = errors.New("err") -func TestGet(t *testing.T) { +func TestOne(t *testing.T) { ctx := context.Background() expected := map[string]string{"Col": "Val"} @@ -36,12 +36,12 @@ func TestGet(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var actual map[string]string - err := c.Get(ctx, "a-pk", "a-sk", &actual) + err := c.One(ctx, "a-pk", "a-sk", &actual) assert.Nil(t, err) assert.Equal(t, expected, actual) } -func TestGetWhenError(t *testing.T) { +func TestOneWhenError(t *testing.T) { ctx := context.Background() pkey, _ := attributevalue.Marshal("a-pk") skey, _ := attributevalue.Marshal("a-sk") @@ -57,12 +57,12 @@ func TestGetWhenError(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v string - err := c.Get(ctx, "a-pk", "a-sk", &v) + err := c.One(ctx, "a-pk", "a-sk", &v) assert.Equal(t, expectedError, err) assert.Equal(t, "", v) } -func TestGetWhenNotFound(t *testing.T) { +func TestOneWhenNotFound(t *testing.T) { ctx := context.Background() pkey, _ := attributevalue.Marshal("a-pk") skey, _ := attributevalue.Marshal("a-sk") @@ -78,77 +78,121 @@ func TestGetWhenNotFound(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v string - err := c.Get(ctx, "a-pk", "a-sk", &v) + err := c.One(ctx, "a-pk", "a-sk", &v) assert.Equal(t, NotFoundError{}, err) assert.Equal(t, "", v) } -func TestPut(t *testing.T) { +func TestOneByUID(t *testing.T) { ctx := context.Background() - data, _ := attributevalue.MarshalMap(map[string]string{"Col": "Val"}) dynamoDB := newMockDynamoDB(t) dynamoDB. - On("PutItem", ctx, &dynamodb.PutItemInput{ - TableName: aws.String("this"), - Item: data, + On("Query", ctx, &dynamodb.QueryInput{ + TableName: aws.String("this"), + IndexName: aws.String(uidIndex), + ExpressionAttributeNames: map[string]string{"#UID": "UID"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, + KeyConditionExpression: aws.String("#UID = :UID"), }). - Return(&dynamodb.PutItemOutput{}, nil) + Return(&dynamodb.QueryOutput{ + Items: []map[string]types.AttributeValue{{ + "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, + "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, + }}, + }, nil) c := &Client{table: "this", svc: dynamoDB} - err := c.Put(ctx, map[string]string{"Col": "Val"}) + var v page.Lpa + err := c.OneByUID(ctx, "M-1111-2222-3333", &v) + assert.Nil(t, err) + assert.Equal(t, page.Lpa{PK: "LPA#123", UID: "M-1111-2222-3333"}, v) } -func TestPutWhenError(t *testing.T) { +func TestOneByUIDWhenQueryError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) dynamoDB. - On("PutItem", ctx, mock.Anything). - Return(&dynamodb.PutItemOutput{}, expectedError) + On("Query", ctx, &dynamodb.QueryInput{ + TableName: aws.String("this"), + IndexName: aws.String(uidIndex), + ExpressionAttributeNames: map[string]string{"#UID": "UID"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, + KeyConditionExpression: aws.String("#UID = :UID"), + }). + Return(&dynamodb.QueryOutput{}, expectedError) c := &Client{table: "this", svc: dynamoDB} - err := c.Put(ctx, "hello") - assert.Equal(t, expectedError, err) + err := c.OneByUID(ctx, "M-1111-2222-3333", mock.Anything) + + assert.Equal(t, fmt.Errorf("failed to query UID: %w", expectedError), err) } -func TestCreate(t *testing.T) { +func TestOneByUIDWhenNot1Item(t *testing.T) { ctx := context.Background() - data, _ := attributevalue.MarshalMap(map[string]string{"Col": "Val"}) dynamoDB := newMockDynamoDB(t) dynamoDB. - On("PutItem", ctx, &dynamodb.PutItemInput{ - TableName: aws.String("this"), - Item: data, - ConditionExpression: aws.String("attribute_not_exists(PK) AND attribute_not_exists(SK)"), + On("Query", ctx, &dynamodb.QueryInput{ + TableName: aws.String("this"), + IndexName: aws.String(uidIndex), + ExpressionAttributeNames: map[string]string{"#UID": "UID"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, + KeyConditionExpression: aws.String("#UID = :UID"), }). - Return(&dynamodb.PutItemOutput{}, nil) + Return(&dynamodb.QueryOutput{ + Items: []map[string]types.AttributeValue{ + { + "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, + "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, + }, + { + "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, + "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, + }, + }, + }, nil) c := &Client{table: "this", svc: dynamoDB} - err := c.Create(ctx, map[string]string{"Col": "Val"}) - assert.Nil(t, err) + err := c.OneByUID(ctx, "M-1111-2222-3333", mock.Anything) + + assert.Equal(t, errors.New("expected to resolve UID but got 2 items"), err) } -func TestCreateWhenError(t *testing.T) { +func TestOneByUIDWhenUnmarshalError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) dynamoDB. - On("PutItem", ctx, mock.Anything). - Return(&dynamodb.PutItemOutput{}, expectedError) + On("Query", ctx, &dynamodb.QueryInput{ + TableName: aws.String("this"), + IndexName: aws.String(uidIndex), + ExpressionAttributeNames: map[string]string{"#UID": "UID"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, + KeyConditionExpression: aws.String("#UID = :UID"), + }). + Return(&dynamodb.QueryOutput{ + Items: []map[string]types.AttributeValue{ + { + "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, + "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, + }, + }, + }, nil) c := &Client{table: "this", svc: dynamoDB} - err := c.Create(ctx, map[string]string{"Col": "Val"}) - assert.Equal(t, expectedError, err) + err := c.OneByUID(ctx, "M-1111-2222-3333", "not an lpa") + + assert.IsType(t, &attributevalue.InvalidUnmarshalError{}, err) } -func TestGetOneByPartialSk(t *testing.T) { +func TestOneByPartialSk(t *testing.T) { ctx := context.Background() expected := map[string]string{"Col": "Val"} @@ -169,12 +213,12 @@ func TestGetOneByPartialSk(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v map[string]string - err := c.GetOneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) + err := c.OneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) assert.Nil(t, err) assert.Equal(t, expected, v) } -func TestGetOneByPartialSkOnQueryError(t *testing.T) { +func TestOneByPartialSkOnQueryError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) @@ -185,11 +229,11 @@ func TestGetOneByPartialSkOnQueryError(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v map[string]string - err := c.GetOneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) + err := c.OneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) assert.Equal(t, expectedError, err) } -func TestGetOneByPartialSkWhenNotFound(t *testing.T) { +func TestOneByPartialSkWhenNotFound(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) @@ -200,11 +244,11 @@ func TestGetOneByPartialSkWhenNotFound(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v map[string]string - err := c.GetOneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) + err := c.OneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) assert.Equal(t, NotFoundError{}, err) } -func TestGetOneByPartialSkWhenMultipleResults(t *testing.T) { +func TestOneByPartialSkWhenMultipleResults(t *testing.T) { ctx := context.Background() data, _ := attributevalue.MarshalMap(map[string]string{"Col": "Val"}) @@ -217,11 +261,11 @@ func TestGetOneByPartialSkWhenMultipleResults(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v map[string]string - err := c.GetOneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) + err := c.OneByPartialSk(ctx, "a-pk", "a-partial-sk", &v) assert.Equal(t, MultipleResultsError{}, err) } -func TestGetAllByGsi(t *testing.T) { +func TestAllForActor(t *testing.T) { ctx := context.Background() expected := map[string]string{"Col": "Val"} @@ -232,7 +276,7 @@ func TestGetAllByGsi(t *testing.T) { dynamoDB. On("Query", ctx, &dynamodb.QueryInput{ TableName: aws.String("this"), - IndexName: aws.String("index-name"), + IndexName: aws.String(actorUpdatedAtIndex), ExpressionAttributeNames: map[string]string{"#SK": "SK"}, ExpressionAttributeValues: map[string]types.AttributeValue{":SK": skey}, KeyConditionExpression: aws.String("#SK = :SK"), @@ -242,34 +286,88 @@ func TestGetAllByGsi(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v []map[string]string - err := c.GetAllByGsi(ctx, "index-name", "a-partial-sk", &v) + err := c.AllForActor(ctx, "a-partial-sk", &v) assert.Nil(t, err) assert.Equal(t, []map[string]string{expected, expected}, v) } -func TestGetAllByGsiWhenNotFound(t *testing.T) { +func TestAllForActorWhenNotFound(t *testing.T) { ctx := context.Background() + + dynamoDB := newMockDynamoDB(t) + dynamoDB. + On("Query", ctx, mock.Anything). + Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{}}, nil) + + c := &Client{table: "this", svc: dynamoDB} + + var v []string + err := c.AllForActor(ctx, "a-partial-sk", &v) + assert.Nil(t, err) + assert.Empty(t, v) +} + +func TestAllForActorOnQueryError(t *testing.T) { + ctx := context.Background() + + dynamoDB := newMockDynamoDB(t) + dynamoDB. + On("Query", ctx, mock.Anything). + Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{}}, expectedError) + + c := &Client{table: "this", svc: dynamoDB} + + var v []string + err := c.AllForActor(ctx, "a-partial-sk", &v) + assert.Equal(t, expectedError, err) +} + +func TestLatestForActor(t *testing.T) { + ctx := context.Background() + + expected := map[string]string{"Col": "Val"} skey, _ := attributevalue.Marshal("a-partial-sk") + updated, _ := attributevalue.Marshal("2") + data, _ := attributevalue.MarshalMap(expected) dynamoDB := newMockDynamoDB(t) dynamoDB. On("Query", ctx, &dynamodb.QueryInput{ TableName: aws.String("this"), - IndexName: aws.String("index-name"), - ExpressionAttributeNames: map[string]string{"#SK": "SK"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":SK": skey}, - KeyConditionExpression: aws.String("#SK = :SK"), + IndexName: aws.String(actorUpdatedAtIndex), + ExpressionAttributeNames: map[string]string{"#SK": "SK", "#UpdatedAt": "UpdatedAt"}, + ExpressionAttributeValues: map[string]types.AttributeValue{":SK": skey, ":UpdatedAt": updated}, + KeyConditionExpression: aws.String("#SK = :SK and #UpdatedAt > :UpdatedAt"), + ScanIndexForward: aws.Bool(false), + Limit: aws.Int32(1), }). + Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{data}}, nil) + + c := &Client{table: "this", svc: dynamoDB} + + var v map[string]string + err := c.LatestForActor(ctx, "a-partial-sk", &v) + assert.Nil(t, err) + assert.Equal(t, expected, v) +} + +func TestLatestForActorWhenNotFound(t *testing.T) { + ctx := context.Background() + + dynamoDB := newMockDynamoDB(t) + dynamoDB. + On("Query", ctx, mock.Anything). Return(&dynamodb.QueryOutput{Items: []map[string]types.AttributeValue{}}, nil) c := &Client{table: "this", svc: dynamoDB} - var v []string - err := c.GetAllByGsi(ctx, "index-name", "a-partial-sk", &v) + var v interface{} + err := c.LatestForActor(ctx, "a-partial-sk", &v) assert.Nil(t, err) + assert.Nil(t, v) } -func TestGetAllByGsiOnQueryError(t *testing.T) { +func TestLatestForActorOnQueryError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) @@ -280,11 +378,11 @@ func TestGetAllByGsiOnQueryError(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} var v []string - err := c.GetAllByGsi(ctx, "index-name", "a-partial-sk", &v) + err := c.LatestForActor(ctx, "a-partial-sk", &v) assert.Equal(t, expectedError, err) } -func TestGetAllByKeys(t *testing.T) { +func TestAllByKeys(t *testing.T) { ctx := context.Background() expected := map[string]string{"Col": "Val"} @@ -310,12 +408,12 @@ func TestGetAllByKeys(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} - v, err := c.GetAllByKeys(ctx, []Key{{PK: "pk", SK: "sk"}}) + v, err := c.AllByKeys(ctx, []Key{{PK: "pk", SK: "sk"}}) assert.Nil(t, err) assert.Equal(t, []map[string]types.AttributeValue{data}, v) } -func TestGetAllByKeysWhenQueryErrors(t *testing.T) { +func TestAllByKeysWhenQueryErrors(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) @@ -325,115 +423,71 @@ func TestGetAllByKeysWhenQueryErrors(t *testing.T) { c := &Client{table: "this", svc: dynamoDB} - _, err := c.GetAllByKeys(ctx, []Key{{PK: "pk", SK: "sk"}}) + _, err := c.AllByKeys(ctx, []Key{{PK: "pk", SK: "sk"}}) assert.Equal(t, expectedError, err) } -func TestGetOneByUID(t *testing.T) { +func TestPut(t *testing.T) { ctx := context.Background() + data, _ := attributevalue.MarshalMap(map[string]string{"Col": "Val"}) dynamoDB := newMockDynamoDB(t) dynamoDB. - On("Query", ctx, &dynamodb.QueryInput{ - TableName: aws.String("this"), - IndexName: aws.String("UidIndex"), - ExpressionAttributeNames: map[string]string{"#UID": "UID"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, - KeyConditionExpression: aws.String("#UID = :UID"), + On("PutItem", ctx, &dynamodb.PutItemInput{ + TableName: aws.String("this"), + Item: data, }). - Return(&dynamodb.QueryOutput{ - Items: []map[string]types.AttributeValue{{ - "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, - "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, - }}, - }, nil) + Return(&dynamodb.PutItemOutput{}, nil) c := &Client{table: "this", svc: dynamoDB} - var v page.Lpa - err := c.GetOneByUID(ctx, "M-1111-2222-3333", &v) - + err := c.Put(ctx, map[string]string{"Col": "Val"}) assert.Nil(t, err) - assert.Equal(t, page.Lpa{PK: "LPA#123", UID: "M-1111-2222-3333"}, v) } -func TestGetOneByUIDWhenQueryError(t *testing.T) { +func TestPutWhenError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) dynamoDB. - On("Query", ctx, &dynamodb.QueryInput{ - TableName: aws.String("this"), - IndexName: aws.String("UidIndex"), - ExpressionAttributeNames: map[string]string{"#UID": "UID"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, - KeyConditionExpression: aws.String("#UID = :UID"), - }). - Return(&dynamodb.QueryOutput{}, expectedError) + On("PutItem", ctx, mock.Anything). + Return(&dynamodb.PutItemOutput{}, expectedError) c := &Client{table: "this", svc: dynamoDB} - err := c.GetOneByUID(ctx, "M-1111-2222-3333", mock.Anything) - - assert.Equal(t, fmt.Errorf("failed to query UID: %w", expectedError), err) + err := c.Put(ctx, "hello") + assert.Equal(t, expectedError, err) } -func TestGetOneByUIDWhenNot1Item(t *testing.T) { +func TestCreate(t *testing.T) { ctx := context.Background() + data, _ := attributevalue.MarshalMap(map[string]string{"Col": "Val"}) dynamoDB := newMockDynamoDB(t) dynamoDB. - On("Query", ctx, &dynamodb.QueryInput{ - TableName: aws.String("this"), - IndexName: aws.String("UidIndex"), - ExpressionAttributeNames: map[string]string{"#UID": "UID"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, - KeyConditionExpression: aws.String("#UID = :UID"), + On("PutItem", ctx, &dynamodb.PutItemInput{ + TableName: aws.String("this"), + Item: data, + ConditionExpression: aws.String("attribute_not_exists(PK) AND attribute_not_exists(SK)"), }). - Return(&dynamodb.QueryOutput{ - Items: []map[string]types.AttributeValue{ - { - "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, - "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, - }, - { - "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, - "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, - }, - }, - }, nil) + Return(&dynamodb.PutItemOutput{}, nil) c := &Client{table: "this", svc: dynamoDB} - err := c.GetOneByUID(ctx, "M-1111-2222-3333", mock.Anything) - - assert.Equal(t, errors.New("expected to resolve UID but got 2 items"), err) + err := c.Create(ctx, map[string]string{"Col": "Val"}) + assert.Nil(t, err) } -func TestGetOneByUIDWhenUnmarshalError(t *testing.T) { +func TestCreateWhenError(t *testing.T) { ctx := context.Background() dynamoDB := newMockDynamoDB(t) dynamoDB. - On("Query", ctx, &dynamodb.QueryInput{ - TableName: aws.String("this"), - IndexName: aws.String("UidIndex"), - ExpressionAttributeNames: map[string]string{"#UID": "UID"}, - ExpressionAttributeValues: map[string]types.AttributeValue{":UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}}, - KeyConditionExpression: aws.String("#UID = :UID"), - }). - Return(&dynamodb.QueryOutput{ - Items: []map[string]types.AttributeValue{ - { - "PK": &types.AttributeValueMemberS{Value: "LPA#123"}, - "UID": &types.AttributeValueMemberS{Value: "M-1111-2222-3333"}, - }, - }, - }, nil) + On("PutItem", ctx, mock.Anything). + Return(&dynamodb.PutItemOutput{}, expectedError) c := &Client{table: "this", svc: dynamoDB} - err := c.GetOneByUID(ctx, "M-1111-2222-3333", "not an lpa") - - assert.IsType(t, &attributevalue.InvalidUnmarshalError{}, err) + err := c.Create(ctx, map[string]string{"Col": "Val"}) + assert.Equal(t, expectedError, err) } diff --git a/internal/page/attorney/mock_AttorneyStore_test.go b/internal/page/attorney/mock_AttorneyStore_test.go index b777e12d5b..f86a071d27 100644 --- a/internal/page/attorney/mock_AttorneyStore_test.go +++ b/internal/page/attorney/mock_AttorneyStore_test.go @@ -67,32 +67,6 @@ func (_m *mockAttorneyStore) Get(_a0 context.Context) (*actor.AttorneyProvidedDe return r0, r1 } -// GetAll provides a mock function with given fields: _a0 -func (_m *mockAttorneyStore) GetAll(_a0 context.Context) ([]*actor.AttorneyProvidedDetails, error) { - ret := _m.Called(_a0) - - var r0 []*actor.AttorneyProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*actor.AttorneyProvidedDetails, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*actor.AttorneyProvidedDetails); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*actor.AttorneyProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Put provides a mock function with given fields: _a0, _a1 func (_m *mockAttorneyStore) Put(_a0 context.Context, _a1 *actor.AttorneyProvidedDetails) error { ret := _m.Called(_a0, _a1) diff --git a/internal/page/attorney/mock_CertificateProviderStore_test.go b/internal/page/attorney/mock_CertificateProviderStore_test.go index 9697285cdc..01c840fdae 100644 --- a/internal/page/attorney/mock_CertificateProviderStore_test.go +++ b/internal/page/attorney/mock_CertificateProviderStore_test.go @@ -15,58 +15,6 @@ type mockCertificateProviderStore struct { mock.Mock } -// Create provides a mock function with given fields: _a0, _a1 -func (_m *mockCertificateProviderStore) Create(_a0 context.Context, _a1 string) (*actor.CertificateProviderProvidedDetails, error) { - ret := _m.Called(_a0, _a1) - - var r0 *actor.CertificateProviderProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*actor.CertificateProviderProvidedDetails, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *actor.CertificateProviderProvidedDetails); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*actor.CertificateProviderProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetAll provides a mock function with given fields: _a0 -func (_m *mockCertificateProviderStore) GetAll(_a0 context.Context) ([]*actor.CertificateProviderProvidedDetails, error) { - ret := _m.Called(_a0) - - var r0 []*actor.CertificateProviderProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*actor.CertificateProviderProvidedDetails, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*actor.CertificateProviderProvidedDetails); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*actor.CertificateProviderProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetAny provides a mock function with given fields: ctx func (_m *mockCertificateProviderStore) GetAny(ctx context.Context) (*actor.CertificateProviderProvidedDetails, error) { ret := _m.Called(ctx) @@ -93,20 +41,6 @@ func (_m *mockCertificateProviderStore) GetAny(ctx context.Context) (*actor.Cert return r0, r1 } -// Put provides a mock function with given fields: _a0, _a1 -func (_m *mockCertificateProviderStore) Put(_a0 context.Context, _a1 *actor.CertificateProviderProvidedDetails) error { - ret := _m.Called(_a0, _a1) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *actor.CertificateProviderProvidedDetails) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - type mockConstructorTestingTnewMockCertificateProviderStore interface { mock.TestingT Cleanup(func()) diff --git a/internal/page/attorney/register.go b/internal/page/attorney/register.go index 06fc8801c8..79bf7984c0 100644 --- a/internal/page/attorney/register.go +++ b/internal/page/attorney/register.go @@ -65,9 +65,6 @@ type NotifyClient interface { //go:generate mockery --testonly --inpackage --name CertificateProviderStore --structname mockCertificateProviderStore type CertificateProviderStore interface { GetAny(ctx context.Context) (*actor.CertificateProviderProvidedDetails, error) - GetAll(context.Context) ([]*actor.CertificateProviderProvidedDetails, error) - Create(context.Context, string) (*actor.CertificateProviderProvidedDetails, error) - Put(context.Context, *actor.CertificateProviderProvidedDetails) error } //go:generate mockery --testonly --inpackage --name AttorneyStore --structname mockAttorneyStore @@ -75,7 +72,6 @@ type AttorneyStore interface { Create(context.Context, string, string, bool) (*actor.AttorneyProvidedDetails, error) Get(context.Context) (*actor.AttorneyProvidedDetails, error) Put(context.Context, *actor.AttorneyProvidedDetails) error - GetAll(context.Context) ([]*actor.AttorneyProvidedDetails, error) } //go:generate mockery --testonly --inpackage --name AddressClient --structname mockAddressClient diff --git a/internal/page/certificateprovider/mock_CertificateProviderStore_test.go b/internal/page/certificateprovider/mock_CertificateProviderStore_test.go index db98a414b7..5944ae5e3f 100644 --- a/internal/page/certificateprovider/mock_CertificateProviderStore_test.go +++ b/internal/page/certificateprovider/mock_CertificateProviderStore_test.go @@ -67,32 +67,6 @@ func (_m *mockCertificateProviderStore) Get(ctx context.Context) (*actor.Certifi return r0, r1 } -// GetAll provides a mock function with given fields: ctx -func (_m *mockCertificateProviderStore) GetAll(ctx context.Context) ([]*actor.CertificateProviderProvidedDetails, error) { - ret := _m.Called(ctx) - - var r0 []*actor.CertificateProviderProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*actor.CertificateProviderProvidedDetails, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []*actor.CertificateProviderProvidedDetails); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*actor.CertificateProviderProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Put provides a mock function with given fields: ctx, certificateProvider func (_m *mockCertificateProviderStore) Put(ctx context.Context, certificateProvider *actor.CertificateProviderProvidedDetails) error { ret := _m.Called(ctx, certificateProvider) diff --git a/internal/page/certificateprovider/register.go b/internal/page/certificateprovider/register.go index 5dbd742234..cb8e417d72 100644 --- a/internal/page/certificateprovider/register.go +++ b/internal/page/certificateprovider/register.go @@ -33,7 +33,6 @@ type CertificateProviderStore interface { Create(ctx context.Context, sessionID string) (*actor.CertificateProviderProvidedDetails, error) Get(ctx context.Context) (*actor.CertificateProviderProvidedDetails, error) Put(ctx context.Context, certificateProvider *actor.CertificateProviderProvidedDetails) error - GetAll(ctx context.Context) ([]*actor.CertificateProviderProvidedDetails, error) } //go:generate mockery --testonly --inpackage --name OneLoginClient --structname mockOneLoginClient diff --git a/internal/page/common.go b/internal/page/common.go index d17e5b04f3..1a7b3d92a7 100644 --- a/internal/page/common.go +++ b/internal/page/common.go @@ -50,13 +50,10 @@ type OneLoginClient interface { type DonorStore interface { Create(context.Context) (*Lpa, error) Put(context.Context, *Lpa) error - GetAll(context.Context) ([]*Lpa, error) - GetAny(context.Context) (*Lpa, error) } //go:generate mockery --testonly --inpackage --name CertificateProviderStore --structname mockCertificateProviderStore type CertificateProviderStore interface { - GetAll(context.Context) ([]*actor.CertificateProviderProvidedDetails, error) Create(context.Context, string) (*actor.CertificateProviderProvidedDetails, error) Put(context.Context, *actor.CertificateProviderProvidedDetails) error } @@ -64,7 +61,6 @@ type CertificateProviderStore interface { //go:generate mockery --testonly --inpackage --name AttorneyStore --structname mockAttorneyStore type AttorneyStore interface { Create(context.Context, string, string, bool) (*actor.AttorneyProvidedDetails, error) - GetAll(context.Context) ([]*actor.AttorneyProvidedDetails, error) } //go:generate mockery --testonly --inpackage --name SessionStore --structname mockSessionStore diff --git a/internal/page/donor/mock_DonorStore_test.go b/internal/page/donor/mock_DonorStore_test.go index e44c855573..a17d8fdc1f 100644 --- a/internal/page/donor/mock_DonorStore_test.go +++ b/internal/page/donor/mock_DonorStore_test.go @@ -14,8 +14,8 @@ type mockDonorStore struct { mock.Mock } -// Create provides a mock function with given fields: _a0 -func (_m *mockDonorStore) Create(_a0 context.Context) (*page.Lpa, error) { +// Get provides a mock function with given fields: _a0 +func (_m *mockDonorStore) Get(_a0 context.Context) (*page.Lpa, error) { ret := _m.Called(_a0) var r0 *page.Lpa @@ -40,8 +40,8 @@ func (_m *mockDonorStore) Create(_a0 context.Context) (*page.Lpa, error) { return r0, r1 } -// Get provides a mock function with given fields: _a0 -func (_m *mockDonorStore) Get(_a0 context.Context) (*page.Lpa, error) { +// Latest provides a mock function with given fields: _a0 +func (_m *mockDonorStore) Latest(_a0 context.Context) (*page.Lpa, error) { ret := _m.Called(_a0) var r0 *page.Lpa @@ -66,32 +66,6 @@ func (_m *mockDonorStore) Get(_a0 context.Context) (*page.Lpa, error) { return r0, r1 } -// GetAll provides a mock function with given fields: _a0 -func (_m *mockDonorStore) GetAll(_a0 context.Context) ([]*page.Lpa, error) { - ret := _m.Called(_a0) - - var r0 []*page.Lpa - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*page.Lpa, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*page.Lpa); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*page.Lpa) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Put provides a mock function with given fields: _a0, _a1 func (_m *mockDonorStore) Put(_a0 context.Context, _a1 *page.Lpa) error { ret := _m.Called(_a0, _a1) diff --git a/internal/page/donor/register.go b/internal/page/donor/register.go index 59a7f1434b..2a1997b2c7 100644 --- a/internal/page/donor/register.go +++ b/internal/page/donor/register.go @@ -36,9 +36,8 @@ type Logger interface { //go:generate mockery --testonly --inpackage --name DonorStore --structname mockDonorStore type DonorStore interface { - Create(context.Context) (*page.Lpa, error) - GetAll(context.Context) ([]*page.Lpa, error) Get(context.Context) (*page.Lpa, error) + Latest(context.Context) (*page.Lpa, error) Put(context.Context, *page.Lpa) error } diff --git a/internal/page/donor/your_details.go b/internal/page/donor/your_details.go index 7521408dce..972348ff01 100644 --- a/internal/page/donor/your_details.go +++ b/internal/page/donor/your_details.go @@ -38,6 +38,18 @@ func YourDetails(tmpl template.Template, donorStore DonorStore, sessionStore ses YesNoMaybeOptions: actor.YesNoMaybeValues, } + if r.Method == http.MethodGet && data.Form.FirstNames == "" { + if latestLpa, _ := donorStore.Latest(r.Context()); latestLpa != nil { + data.Form = &yourDetailsForm{ + FirstNames: latestLpa.Donor.FirstNames, + LastName: latestLpa.Donor.LastName, + OtherNames: latestLpa.Donor.OtherNames, + Dob: latestLpa.Donor.DateOfBirth, + CanSign: latestLpa.Donor.ThinksCanSign, + } + } + } + if r.Method == http.MethodPost { loginSession, err := sesh.Login(sessionStore, r) if err != nil { diff --git a/internal/page/donor/your_details_test.go b/internal/page/donor/your_details_test.go index d089dd3f44..e67abf7778 100644 --- a/internal/page/donor/your_details_test.go +++ b/internal/page/donor/your_details_test.go @@ -25,6 +25,11 @@ func TestGetYourDetails(t *testing.T) { w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodGet, "/", nil) + donorStore := newMockDonorStore(t) + donorStore. + On("Latest", r.Context()). + Return(nil, expectedError) + template := newMockTemplate(t) template. On("Execute", w, &yourDetailsData{ @@ -34,7 +39,7 @@ func TestGetYourDetails(t *testing.T) { }). Return(nil) - err := YourDetails(template.Execute, nil, nil)(testAppData, w, r, &page.Lpa{}) + err := YourDetails(template.Execute, donorStore, nil)(testAppData, w, r, &page.Lpa{}) resp := w.Result() assert.Nil(t, err) @@ -67,6 +72,45 @@ func TestGetYourDetailsFromStore(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) } +func TestGetYourDetailsFromLatest(t *testing.T) { + w := httptest.NewRecorder() + r, _ := http.NewRequest(http.MethodGet, "/", nil) + + donorStore := newMockDonorStore(t) + donorStore. + On("Latest", r.Context()). + Return(&page.Lpa{ + Donor: actor.Donor{ + FirstNames: "John", + LastName: "Doe", + OtherNames: "J", + DateOfBirth: date.New("2000", "01", "02"), + ThinksCanSign: actor.Yes, + }, + }, nil) + + template := newMockTemplate(t) + template. + On("Execute", w, &yourDetailsData{ + App: testAppData, + Form: &yourDetailsForm{ + FirstNames: "John", + LastName: "Doe", + OtherNames: "J", + Dob: date.New("2000", "01", "02"), + CanSign: actor.Yes, + }, + YesNoMaybeOptions: actor.YesNoMaybeValues, + }). + Return(nil) + + err := YourDetails(template.Execute, donorStore, nil)(testAppData, w, r, &page.Lpa{}) + resp := w.Result() + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + func TestGetYourDetailsWhenTemplateErrors(t *testing.T) { w := httptest.NewRecorder() r, _ := http.NewRequest(http.MethodGet, "/", nil) @@ -76,7 +120,7 @@ func TestGetYourDetailsWhenTemplateErrors(t *testing.T) { On("Execute", w, mock.Anything). Return(expectedError) - err := YourDetails(template.Execute, nil, nil)(testAppData, w, r, &page.Lpa{}) + err := YourDetails(template.Execute, nil, nil)(testAppData, w, r, &page.Lpa{Donor: actor.Donor{FirstNames: "John"}}) resp := w.Result() assert.Equal(t, expectedError, err) diff --git a/internal/page/mock_AttorneyStore_test.go b/internal/page/mock_AttorneyStore_test.go index badb761fd9..817dbb0293 100644 --- a/internal/page/mock_AttorneyStore_test.go +++ b/internal/page/mock_AttorneyStore_test.go @@ -41,32 +41,6 @@ func (_m *mockAttorneyStore) Create(_a0 context.Context, _a1 string, _a2 string, return r0, r1 } -// GetAll provides a mock function with given fields: _a0 -func (_m *mockAttorneyStore) GetAll(_a0 context.Context) ([]*actor.AttorneyProvidedDetails, error) { - ret := _m.Called(_a0) - - var r0 []*actor.AttorneyProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*actor.AttorneyProvidedDetails, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*actor.AttorneyProvidedDetails); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*actor.AttorneyProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - type mockConstructorTestingTnewMockAttorneyStore interface { mock.TestingT Cleanup(func()) diff --git a/internal/page/mock_CertificateProviderStore_test.go b/internal/page/mock_CertificateProviderStore_test.go index 4e7879c746..526b9c6d18 100644 --- a/internal/page/mock_CertificateProviderStore_test.go +++ b/internal/page/mock_CertificateProviderStore_test.go @@ -41,32 +41,6 @@ func (_m *mockCertificateProviderStore) Create(_a0 context.Context, _a1 string) return r0, r1 } -// GetAll provides a mock function with given fields: _a0 -func (_m *mockCertificateProviderStore) GetAll(_a0 context.Context) ([]*actor.CertificateProviderProvidedDetails, error) { - ret := _m.Called(_a0) - - var r0 []*actor.CertificateProviderProvidedDetails - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*actor.CertificateProviderProvidedDetails, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*actor.CertificateProviderProvidedDetails); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*actor.CertificateProviderProvidedDetails) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Put provides a mock function with given fields: _a0, _a1 func (_m *mockCertificateProviderStore) Put(_a0 context.Context, _a1 *actor.CertificateProviderProvidedDetails) error { ret := _m.Called(_a0, _a1) diff --git a/internal/page/mock_DonorStore_test.go b/internal/page/mock_DonorStore_test.go index a43b76539c..53d8a0cc23 100644 --- a/internal/page/mock_DonorStore_test.go +++ b/internal/page/mock_DonorStore_test.go @@ -39,58 +39,6 @@ func (_m *mockDonorStore) Create(_a0 context.Context) (*Lpa, error) { return r0, r1 } -// GetAll provides a mock function with given fields: _a0 -func (_m *mockDonorStore) GetAll(_a0 context.Context) ([]*Lpa, error) { - ret := _m.Called(_a0) - - var r0 []*Lpa - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]*Lpa, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) []*Lpa); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*Lpa) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetAny provides a mock function with given fields: _a0 -func (_m *mockDonorStore) GetAny(_a0 context.Context) (*Lpa, error) { - ret := _m.Called(_a0) - - var r0 *Lpa - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*Lpa, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *Lpa); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*Lpa) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // Put provides a mock function with given fields: _a0, _a1 func (_m *mockDonorStore) Put(_a0 context.Context, _a1 *Lpa) error { ret := _m.Called(_a0, _a1) diff --git a/terraform/environment/dynamodb.tf b/terraform/environment/dynamodb.tf index 896c71ef4f..25c89dd1e9 100644 --- a/terraform/environment/dynamodb.tf +++ b/terraform/environment/dynamodb.tf @@ -9,7 +9,7 @@ data "aws_kms_alias" "dynamodb_encryption_key_eu_west_2" { } resource "aws_dynamodb_table" "lpas_table" { - name = "${local.environment_name}-Lpas2" + name = "${local.environment_name}-Lpas" billing_mode = "PAY_PER_REQUEST" deletion_protection_enabled = local.default_tags.environment-name == "production" ? true : false # see docs/runbooks/disabling_dynamodb_global_tables.md when Global Tables needs to be disabled @@ -19,8 +19,9 @@ resource "aws_dynamodb_table" "lpas_table" { range_key = "SK" global_secondary_index { - name = "ActorIndex" + name = "ActorUpdatedAtIndex" hash_key = "SK" + range_key = "UpdatedAt" projection_type = "ALL" } @@ -50,6 +51,11 @@ resource "aws_dynamodb_table" "lpas_table" { type = "S" } + attribute { + name = "UpdatedAt" + type = "S" + } + point_in_time_recovery { enabled = true }