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 }