diff --git a/.codecov.yml b/.codecov.yml index 6e822a3029..2bca24df7e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,6 +9,7 @@ coverage: - "./internal/identity/yoti*" - "./internal/telemetry" - "./internal/page/fixtures" + - "./cmd/event-received/main.go" - "./cmd/mlpa/main.go" - "./cmd/mock-notify/main.go" - "./cmd/mock-onelogin/main.go" diff --git a/Makefile b/Makefile index 98bcc627cf..9fb2552911 100644 --- a/Makefile +++ b/Makefile @@ -88,6 +88,10 @@ get-lpa: ##@app dumps all entries in the lpas dynamodb table that are related t docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \ query --table-name lpas --key-condition-expression 'PK = :pk' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}}' +get-donor-session-id: + docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \ + query --table-name lpas --key-condition-expression 'PK = :pk and begins_with(SK, :sk)' --expression-attribute-values '{":pk": {"S": "LPA#$(id)"}, ":sk": {"S": "#DONOR#"}}' | jq -r .Items[0].SK.S | sed 's/#DONOR#//g' + get-documents: ##@app dumps all documents in the lpas dynamodb table that are related to the LPA id supplied e.g. get-documents lpaId=abc-123 docker compose -f docker/docker-compose.yml exec localstack awslocal dynamodb \ query --table-name lpas --key-condition-expression 'PK = :pk and begins_with(SK, :sk)' --expression-attribute-values '{":pk": {"S": "LPA#$(lpaId)"}, ":sk": {"S": "#DOCUMENT#"}}' @@ -116,5 +120,8 @@ emit-object-tags-added-without-virus: ##@app emits a ObjectTagging:Put event wit curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"Records":[{"eventSource":"aws:s3","eventTime":"2023-10-23T15:58:33.081Z","eventName":"ObjectTagging:Put","s3":{"bucket":{"name":"uploads-opg-modernising-lpa-eu-west-1"},"object":{"key":"$(key)"}}}]}' +emit-uid-requested: ##@app emits a uid-requested event with the given detail e.g. emit-uid-requested lpaID=abc sessionID=xyz + curl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"version":"0","id":"63eb7e5f-1f10-4744-bba9-e16d327c3b98","detail-type":"uid-requested","source":"opg.poas.makeregister","account":"653761790766","time":"2023-08-30T13:40:30Z","region":"eu-west-1","resources":[],"detail":{"LpaID":"$(lpaID)","DonorSessionID":"$(sessionID)","Type":"pfa","Donor":{"Name":"abc","Dob":"2000-01-01","Postcode":"F1 1FF"}}}' + logs: ##@app tails logs for all containers running docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml logs -f diff --git a/cmd/event-received/handlers.go b/cmd/event-received/handlers.go new file mode 100644 index 0000000000..0c83f5213c --- /dev/null +++ b/cmd/event-received/handlers.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/aws/aws-lambda-go/events" + "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/event" + "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" + "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" +) + +func handleUidRequested(ctx context.Context, uidStore UidStore, uidClient UidClient, e events.CloudWatchEvent) error { + var v event.UidRequested + if err := json.Unmarshal(e.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + uid, err := uidClient.CreateCase(ctx, &uid.CreateCaseRequestBody{Type: v.Type, Donor: v.Donor}) + if err != nil { + return fmt.Errorf("failed to create case: %w", err) + } + + if err := uidStore.Set(ctx, v.LpaID, v.DonorSessionID, uid); err != nil { + return fmt.Errorf("failed to set uid: %w", err) + } + + return nil +} + +func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent) error { + var v uidEvent + if err := json.Unmarshal(event.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + var key dynamo.Key + if err := client.OneByUID(ctx, v.UID, &key); err != nil { + return fmt.Errorf("failed to resolve uid: %w", err) + } + + if key.PK == "" { + return fmt.Errorf("PK missing from LPA in response") + } + + if err := client.Put(ctx, map[string]string{"PK": key.PK, "SK": "#EVIDENCE_RECEIVED"}); err != nil { + return fmt.Errorf("failed to persist evidence received: %w", err) + } + + return nil +} + +func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event events.CloudWatchEvent, shareCodeSender shareCodeSender, appData page.AppData, now func() time.Time) error { + var v uidEvent + if err := json.Unmarshal(event.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + lpa, err := getLpaByUID(ctx, dynamoClient, v.UID) + if err != nil { + return err + } + + lpa.Tasks.PayForLpa = actor.PaymentTaskCompleted + lpa.UpdatedAt = now() + + if err := dynamoClient.Put(ctx, lpa); err != nil { + return fmt.Errorf("failed to update LPA task status: %w", err) + } + + if err := shareCodeSender.SendCertificateProvider(ctx, notify.CertificateProviderInviteEmail, appData, false, &lpa); err != nil { + return fmt.Errorf("failed to send share code to certificate provider: %w", err) + } + + return nil +} + +func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error { + var v uidEvent + if err := json.Unmarshal(event.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + lpa, err := getLpaByUID(ctx, client, v.UID) + if err != nil { + return err + } + + lpa.Tasks.PayForLpa = actor.PaymentTaskMoreEvidenceRequired + lpa.UpdatedAt = now() + + if err := client.Put(ctx, lpa); err != nil { + return fmt.Errorf("failed to update LPA task status: %w", err) + } + + return nil +} + +func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error { + var v uidEvent + if err := json.Unmarshal(event.Detail, &v); err != nil { + return fmt.Errorf("failed to unmarshal detail: %w", err) + } + + lpa, err := getLpaByUID(ctx, client, v.UID) + if err != nil { + return err + } + + lpa.Tasks.PayForLpa = actor.PaymentTaskDenied + lpa.UpdatedAt = now() + + if err := client.Put(ctx, lpa); err != nil { + return fmt.Errorf("failed to update LPA task status: %w", err) + } + + return nil +} + +func handleObjectTagsAdded(ctx context.Context, dynamodbClient dynamodbClient, event events.S3Event, s3Client s3Client, documentStore DocumentStore) error { + objectKey := event.Records[0].S3.Object.Key + if objectKey == "" { + return fmt.Errorf("object key missing") + } + + tags, err := s3Client.GetObjectTags(ctx, objectKey) + if err != nil { + return fmt.Errorf("failed to get tags for object: %w", err) + } + + hasScannedTag := false + hasVirus := false + + for _, tag := range tags { + if *tag.Key == "virus-scan-status" { + hasScannedTag = true + hasVirus = *tag.Value == virusFound + break + } + } + + if !hasScannedTag { + return nil + } + + parts := strings.Split(objectKey, "/") + + lpa, err := getLpaByUID(ctx, dynamodbClient, parts[0]) + if err != nil { + return err + } + + err = documentStore.UpdateScanResults(ctx, lpa.ID, objectKey, hasVirus) + if err != nil { + return fmt.Errorf("failed to update scan results: %w", err) + } + + return nil +} + +func getLpaByUID(ctx context.Context, client dynamodbClient, uid string) (page.Lpa, error) { + var key dynamo.Key + if err := client.OneByUID(ctx, uid, &key); err != nil { + return page.Lpa{}, fmt.Errorf("failed to resolve uid: %w", err) + } + + if key.PK == "" { + return page.Lpa{}, fmt.Errorf("PK missing from LPA in response") + } + + var lpa page.Lpa + if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil { + return page.Lpa{}, fmt.Errorf("failed to get LPA: %w", err) + } + + return lpa, nil +} diff --git a/cmd/event-received/main_test.go b/cmd/event-received/handlers_test.go similarity index 84% rename from cmd/event-received/main_test.go rename to cmd/event-received/handlers_test.go index 1538f6027b..f1be4ccce5 100644 --- a/cmd/event-received/main_test.go +++ b/cmd/event-received/handlers_test.go @@ -12,9 +12,11 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" + "github.com/ministryofjustice/opg-modernising-lpa/internal/date" "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" + "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -22,6 +24,68 @@ import ( var expectedError = errors.New("err") var ctx = context.Background() +func TestHandleUidRequested(t *testing.T) { + event := events.CloudWatchEvent{ + DetailType: "uid-requested", + Detail: json.RawMessage(`{"lpaID":"an-id","donorSessionID":"donor-id","type":"hw","donor":{"name":"a donor","dob":"2000-01-02","postcode":"F1 1FF"}}`), + } + + uidClient := newMockUidClient(t) + uidClient. + On("CreateCase", ctx, &uid.CreateCaseRequestBody{ + Type: "hw", + Donor: uid.DonorDetails{ + Name: "a donor", + Dob: date.New("2000", "01", "02"), + Postcode: "F1 1FF", + }, + }). + Return("M-1111-2222-3333", nil) + + uidStore := newMockUidStore(t) + uidStore. + On("Set", ctx, "an-id", "donor-id", "M-1111-2222-3333"). + Return(nil) + + err := handleUidRequested(ctx, uidStore, uidClient, event) + assert.Nil(t, err) +} + +func TestHandleUidRequestedWhenUidClientErrors(t *testing.T) { + event := events.CloudWatchEvent{ + DetailType: "uid-requested", + Detail: json.RawMessage(`{"lpaID":"an-id","donorSessionID":"donor-id","type":"hw","donor":{"name":"a donor","dob":"2000-01-02","postcode":"F1 1FF"}}`), + } + + uidClient := newMockUidClient(t) + uidClient. + On("CreateCase", ctx, mock.Anything). + Return("", expectedError) + + err := handleUidRequested(ctx, nil, uidClient, event) + assert.Equal(t, fmt.Errorf("failed to create case: %w", expectedError), err) +} + +func TestHandleUidRequestedWhenUidStoreErrors(t *testing.T) { + event := events.CloudWatchEvent{ + DetailType: "uid-requested", + Detail: json.RawMessage(`{"lpaID":"an-id","donorSessionID":"donor-id","type":"hw","donor":{"name":"a donor","dob":"2000-01-02","postcode":"F1 1FF"}}`), + } + + uidClient := newMockUidClient(t) + uidClient. + On("CreateCase", ctx, mock.Anything). + Return("M-1111-2222-3333", nil) + + uidStore := newMockUidStore(t) + uidStore. + On("Set", ctx, "an-id", "donor-id", "M-1111-2222-3333"). + Return(expectedError) + + err := handleUidRequested(ctx, uidStore, uidClient, event) + assert.Equal(t, fmt.Errorf("failed to set uid: %w", expectedError), err) +} + func TestHandleEvidenceReceived(t *testing.T) { event := events.CloudWatchEvent{ DetailType: "evidence-required", @@ -59,7 +123,7 @@ func TestHandleEvidenceReceivedWhenClientGetError(t *testing.T) { Return(expectedError) err := handleEvidenceReceived(ctx, client, event) - assert.Equal(t, fmt.Errorf("failed to resolve uid for 'evidence-received': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to resolve uid: %w", expectedError), err) } func TestHandleEvidenceReceivedWhenLpaMissingPK(t *testing.T) { @@ -78,7 +142,7 @@ func TestHandleEvidenceReceivedWhenLpaMissingPK(t *testing.T) { }) err := handleEvidenceReceived(ctx, client, event) - assert.Equal(t, errors.New("PK missing from LPA in response to 'evidence-received'"), err) + assert.Equal(t, errors.New("PK missing from LPA in response"), err) } func TestHandleEvidenceReceivedWhenClientPutError(t *testing.T) { @@ -103,7 +167,7 @@ func TestHandleEvidenceReceivedWhenClientPutError(t *testing.T) { Return(expectedError) err := handleEvidenceReceived(ctx, client, event) - assert.Equal(t, fmt.Errorf("failed to persist evidence received for 'evidence-received': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to persist evidence received: %w", expectedError), err) } func TestHandleFeeApproved(t *testing.T) { @@ -170,7 +234,7 @@ func TestHandleFeeApprovedWhenDynamoClientPutError(t *testing.T) { Return(expectedError) err := handleFeeApproved(ctx, client, event, nil, page.AppData{}, func() time.Time { return now }) - assert.Equal(t, fmt.Errorf("failed to update LPA task status for 'fee-approved': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to update LPA task status: %w", expectedError), err) } func TestHandleFeeApprovedWhenShareCodeSenderError(t *testing.T) { @@ -206,7 +270,7 @@ func TestHandleFeeApprovedWhenShareCodeSenderError(t *testing.T) { Return(expectedError) err := handleFeeApproved(ctx, client, event, shareCodeSender, page.AppData{}, func() time.Time { return now }) - assert.Equal(t, fmt.Errorf("failed to send share code to certificate provider for 'fee-approved': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to send share code to certificate provider: %w", expectedError), err) } func TestHandleMoreEvidenceRequired(t *testing.T) { @@ -268,7 +332,7 @@ func TestHandleMoreEvidenceRequiredWhenPutError(t *testing.T) { Return(expectedError) err := handleMoreEvidenceRequired(ctx, client, event, func() time.Time { return now }) - assert.Equal(t, fmt.Errorf("failed to update LPA task status for 'more-evidence-required': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to update LPA task status: %w", expectedError), err) } func TestHandleFeeDenied(t *testing.T) { @@ -330,7 +394,7 @@ func TestHandleFeeDeniedWhenPutError(t *testing.T) { Return(expectedError) err := handleFeeDenied(ctx, client, event, func() time.Time { return now }) - assert.Equal(t, fmt.Errorf("failed to update LPA task status for 'fee-denied': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to update LPA task status: %w", expectedError), err) } func TestHandleObjectTagsAdded(t *testing.T) { @@ -407,7 +471,7 @@ func TestHandleObjectTagsAddedWhenObjectKeyMissing(t *testing.T) { } err := handleObjectTagsAdded(ctx, nil, event.S3Event, nil, nil) - assert.Equal(t, fmt.Errorf("object key missing in event in '%s'", objectTagsAddedEventName), err) + assert.Equal(t, fmt.Errorf("object key missing"), err) } func TestHandleObjectTagsAddedWhenS3ClientGetObjectTagsError(t *testing.T) { @@ -423,7 +487,7 @@ func TestHandleObjectTagsAddedWhenS3ClientGetObjectTagsError(t *testing.T) { Return([]types.Tag{}, expectedError) err := handleObjectTagsAdded(ctx, nil, event.S3Event, s3Client, nil) - assert.Equal(t, fmt.Errorf("failed to get tags for object in '%s': %w", objectTagsAddedEventName, expectedError), err) + assert.Equal(t, fmt.Errorf("failed to get tags for object: %w", expectedError), err) } func TestHandleObjectTagsAddedWhenDynamoClientOneByUIDError(t *testing.T) { @@ -457,7 +521,7 @@ func TestHandleObjectTagsAddedWhenDynamoClientOneByUIDError(t *testing.T) { }) err := handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, s3Client, nil) - assert.Equal(t, fmt.Errorf("failed to get LPA for '%s': %w", objectTagsAddedEventName, expectedError), err) + assert.Equal(t, fmt.Errorf("failed to get LPA: %w", expectedError), err) } func TestHandleObjectTagsAddedWhenDocumentStoreUpdateScanResultsError(t *testing.T) { @@ -496,7 +560,7 @@ func TestHandleObjectTagsAddedWhenDocumentStoreUpdateScanResultsError(t *testing Return(expectedError) err := handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, s3Client, documentStore) - assert.Equal(t, fmt.Errorf("failed to update scan results for '%s': %w", objectTagsAddedEventName, expectedError), err) + assert.Equal(t, fmt.Errorf("failed to update scan results: %w", expectedError), err) } func TestGetLpaByUID(t *testing.T) { @@ -518,7 +582,7 @@ func TestGetLpaByUID(t *testing.T) { return nil }) - lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333", "an-event") + lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333") assert.Equal(t, expectedLpa, lpa) assert.Nil(t, err) @@ -530,10 +594,10 @@ func TestGetLpaByUIDWhenClientOneByUidError(t *testing.T) { On("OneByUID", ctx, "M-1111-2222-3333", mock.Anything). Return(expectedError) - lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333", "an-event") + lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333") assert.Equal(t, page.Lpa{}, lpa) - assert.Equal(t, fmt.Errorf("failed to resolve uid for 'an-event': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to resolve uid: %w", expectedError), err) } func TestGetLpaByUIDWhenPKMissing(t *testing.T) { @@ -546,10 +610,10 @@ func TestGetLpaByUIDWhenPKMissing(t *testing.T) { return nil }) - lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333", "an-event") + lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333") assert.Equal(t, page.Lpa{}, lpa) - assert.Equal(t, errors.New("PK missing from LPA in response to 'an-event'"), err) + assert.Equal(t, errors.New("PK missing from LPA in response"), err) } func TestGetLpaByUIDWhenClientOneError(t *testing.T) { @@ -565,8 +629,8 @@ func TestGetLpaByUIDWhenClientOneError(t *testing.T) { On("One", ctx, "LPA#123", "#DONOR#456", mock.Anything). Return(expectedError) - lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333", "an-event") + lpa, err := getLpaByUID(ctx, client, "M-1111-2222-3333") assert.Equal(t, page.Lpa{}, lpa) - assert.Equal(t, fmt.Errorf("failed to get LPA for 'an-event': %w", expectedError), err) + assert.Equal(t, fmt.Errorf("failed to get LPA: %w", expectedError), err) } diff --git a/cmd/event-received/main.go b/cmd/event-received/main.go index 403b37ae82..c125a8babe 100644 --- a/cmd/event-received/main.go +++ b/cmd/event-received/main.go @@ -6,15 +6,15 @@ import ( "fmt" "net/http" "os" - "strings" "time" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/config" + dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/app" "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" @@ -23,15 +23,12 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/random" "github.com/ministryofjustice/opg-modernising-lpa/internal/s3" "github.com/ministryofjustice/opg-modernising-lpa/internal/secrets" + "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" ) const ( - virusFound = "infected" - evidenceReceivedEventName = "evidence-received" - feeApprovedEventName = "fee-approved" - feeDeniedEventName = "fee-denied" - moreEvidenceRequiredEventName = "more-evidence-required" - objectTagsAddedEventName = "ObjectTagging:Put" + virusFound = "infected" + objectTagsAddedEventName = "ObjectTagging:Put" ) type uidEvent struct { @@ -43,6 +40,7 @@ type dynamodbClient interface { 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 + Update(ctx context.Context, pk, sk string, values map[string]dynamodbtypes.AttributeValue, expression string) error } //go:generate mockery --testonly --inpackage --name s3Client --structname mockS3Client @@ -60,6 +58,16 @@ type DocumentStore interface { UpdateScanResults(ctx context.Context, lpaID, objectKey string, virusDetected bool) error } +//go:generate mockery --testonly --inpackage --name UidStore --structname mockUidStore +type UidStore interface { + Set(ctx context.Context, lpaID, sessionID, uid string) error +} + +//go:generate mockery --testonly --inpackage --name UidClient --structname mockUidClient +type UidClient interface { + CreateCase(context.Context, *uid.CreateCaseRequestBody) (string, error) +} + type Event struct { events.S3Event events.CloudWatchEvent @@ -70,16 +78,19 @@ func (e Event) isS3Event() bool { } func (e Event) isCloudWatchEvent() bool { - return e.Source == "aws.cloudwatch" + return e.Source == "aws.cloudwatch" || e.Source == "opg.poas.makeregister" } -func Handler(ctx context.Context, event Event) error { - tableName := os.Getenv("LPAS_TABLE") - notifyIsProduction := os.Getenv("GOVUK_NOTIFY_IS_PRODUCTION") == "1" - appPublicURL := os.Getenv("APP_PUBLIC_URL") - awsBaseURL := os.Getenv("AWS_BASE_URL") - notifyBaseURL := os.Getenv("GOVUK_NOTIFY_BASE_URL") - evidenceBucketName := os.Getenv("UPLOADS_S3_BUCKET_NAME") +func handler(ctx context.Context, event Event) error { + var ( + tableName = os.Getenv("LPAS_TABLE") + notifyIsProduction = os.Getenv("GOVUK_NOTIFY_IS_PRODUCTION") == "1" + appPublicURL = os.Getenv("APP_PUBLIC_URL") + awsBaseURL = os.Getenv("AWS_BASE_URL") + notifyBaseURL = os.Getenv("GOVUK_NOTIFY_BASE_URL") + evidenceBucketName = os.Getenv("UPLOADS_S3_BUCKET_NAME") + uidBaseURL = os.Getenv("UID_BASE_URL") + ) cfg, err := config.LoadDefaultConfig(ctx) if err != nil { @@ -96,206 +107,80 @@ func Handler(ctx context.Context, event Event) error { }) } - s3Client := s3.NewClient(cfg, evidenceBucketName) - dynamoClient, err := dynamo.NewClient(cfg, tableName) if err != nil { return fmt.Errorf("failed to create dynamodb client: %w", err) } - secretsClient, err := secrets.NewClient(cfg, time.Hour) - if err != nil { - return fmt.Errorf("failed to create secrets client: %w", err) - } - - notifyApiKey, err := secretsClient.Secret(ctx, secrets.GovUkNotify) - if err != nil { - return fmt.Errorf("failed to get notify API secret: %w", err) - } - - notifyClient, err := notify.New(notifyIsProduction, notifyBaseURL, notifyApiKey, http.DefaultClient) - - documentStore := app.NewDocumentStore(dynamoClient, s3Client, random.UuidString) - - bundle := localize.NewBundle("./lang/en.json", "./lang/cy.json") - - //TODO do this in handleFeeApproved when/if we save lang preference in LPA - appData := page.AppData{Localizer: bundle.For(localize.En)} - - shareCodeSender := page.NewShareCodeSender(app.NewShareCodeStore(dynamoClient), notifyClient, appPublicURL, random.String) now := time.Now if event.isS3Event() { - return handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, s3Client, documentStore) - } + s3Client := s3.NewClient(cfg, evidenceBucketName) + documentStore := app.NewDocumentStore(dynamoClient, s3Client, random.UuidString) - if event.isCloudWatchEvent() { - switch event.DetailType { - case evidenceReceivedEventName: - return handleEvidenceReceived(ctx, dynamoClient, event.CloudWatchEvent) - case feeApprovedEventName: - return handleFeeApproved(ctx, dynamoClient, event.CloudWatchEvent, shareCodeSender, appData, now) - case moreEvidenceRequiredEventName: - return handleMoreEvidenceRequired(ctx, dynamoClient, event.CloudWatchEvent, now) - case feeDeniedEventName: - return handleFeeDenied(ctx, dynamoClient, event.CloudWatchEvent, now) - default: - return fmt.Errorf("unknown event received: %s", event.DetailType) + if err := handleObjectTagsAdded(ctx, dynamoClient, event.S3Event, s3Client, documentStore); err != nil { + return fmt.Errorf("ObjectTagging:Put: %w", err) } - } - - eJson, _ := json.Marshal(event) - return fmt.Errorf("unknown event type received: %s", string(eJson)) -} - -func handleEvidenceReceived(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent) error { - var v uidEvent - if err := json.Unmarshal(event.Detail, &v); err != nil { - return fmt.Errorf("failed to unmarshal '%s' detail: %w", evidenceReceivedEventName, err) - } - - var key dynamo.Key - if err := client.OneByUID(ctx, v.UID, &key); err != nil { - return fmt.Errorf("failed to resolve uid for '%s': %w", evidenceReceivedEventName, err) - } - - if key.PK == "" { - return fmt.Errorf("PK missing from LPA in response to '%s'", evidenceReceivedEventName) - } - - if err := client.Put(ctx, map[string]string{"PK": key.PK, "SK": "#EVIDENCE_RECEIVED"}); err != nil { - return fmt.Errorf("failed to persist evidence received for '%s': %w", evidenceReceivedEventName, err) - } - - return nil -} - -func handleFeeApproved(ctx context.Context, dynamoClient dynamodbClient, event events.CloudWatchEvent, shareCodeSender shareCodeSender, appData page.AppData, now func() time.Time) error { - var v uidEvent - if err := json.Unmarshal(event.Detail, &v); err != nil { - return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeApprovedEventName, err) - } - - lpa, err := getLpaByUID(ctx, dynamoClient, v.UID, feeApprovedEventName) - if err != nil { - return err - } - - lpa.Tasks.PayForLpa = actor.PaymentTaskCompleted - lpa.UpdatedAt = now() - if err := dynamoClient.Put(ctx, lpa); err != nil { - return fmt.Errorf("failed to update LPA task status for '%s': %w", feeApprovedEventName, err) - } - - if err := shareCodeSender.SendCertificateProvider(ctx, notify.CertificateProviderInviteEmail, appData, false, &lpa); err != nil { - return fmt.Errorf("failed to send share code to certificate provider for '%s': %w", feeApprovedEventName, err) + return nil } - return nil -} - -func handleMoreEvidenceRequired(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error { - var v uidEvent - if err := json.Unmarshal(event.Detail, &v); err != nil { - return fmt.Errorf("failed to unmarshal '%s' detail: %w", moreEvidenceRequiredEventName, err) - } + if event.isCloudWatchEvent() { + err := fmt.Errorf("unknown cloudwatch event") - lpa, err := getLpaByUID(ctx, client, v.UID, moreEvidenceRequiredEventName) - if err != nil { - return err - } + switch event.DetailType { + case "uid-requested": + uidStore := app.NewUidStore(dynamoClient, now) + uidClient := uid.New(uidBaseURL, &http.Client{Timeout: 10 * time.Second}, cfg, v4.NewSigner(), time.Now) - lpa.Tasks.PayForLpa = actor.PaymentTaskMoreEvidenceRequired - lpa.UpdatedAt = now() + err = handleUidRequested(ctx, uidStore, uidClient, event.CloudWatchEvent) - if err := client.Put(ctx, lpa); err != nil { - return fmt.Errorf("failed to update LPA task status for '%s': %w", moreEvidenceRequiredEventName, err) - } + case "evidence-received": + err = handleEvidenceReceived(ctx, dynamoClient, event.CloudWatchEvent) - return nil -} + case "fee-approved": + bundle := localize.NewBundle("./lang/en.json", "./lang/cy.json") -func handleFeeDenied(ctx context.Context, client dynamodbClient, event events.CloudWatchEvent, now func() time.Time) error { - var v uidEvent - if err := json.Unmarshal(event.Detail, &v); err != nil { - return fmt.Errorf("failed to unmarshal '%s' detail: %w", feeDeniedEventName, err) - } + //TODO do this in handleFeeApproved when/if we save lang preference in LPA + appData := page.AppData{Localizer: bundle.For(localize.En)} - lpa, err := getLpaByUID(ctx, client, v.UID, feeDeniedEventName) - if err != nil { - return err - } + secretsClient, err := secrets.NewClient(cfg, time.Hour) + if err != nil { + return fmt.Errorf("failed to create secrets client: %w", err) + } - lpa.Tasks.PayForLpa = actor.PaymentTaskDenied - lpa.UpdatedAt = now() + notifyApiKey, err := secretsClient.Secret(ctx, secrets.GovUkNotify) + if err != nil { + return fmt.Errorf("failed to get notify API secret: %w", err) + } - if err := client.Put(ctx, lpa); err != nil { - return fmt.Errorf("failed to update LPA task status for '%s': %w", feeDeniedEventName, err) - } + notifyClient, err := notify.New(notifyIsProduction, notifyBaseURL, notifyApiKey, http.DefaultClient) + if err != nil { + return err + } - return nil -} + shareCodeSender := page.NewShareCodeSender(app.NewShareCodeStore(dynamoClient), notifyClient, appPublicURL, random.String) -func handleObjectTagsAdded(ctx context.Context, dynamodbClient dynamodbClient, event events.S3Event, s3Client s3Client, documentStore DocumentStore) error { - objectKey := event.Records[0].S3.Object.Key - if objectKey == "" { - return fmt.Errorf("object key missing in event in '%s'", objectTagsAddedEventName) - } + err = handleFeeApproved(ctx, dynamoClient, event.CloudWatchEvent, shareCodeSender, appData, now) - tags, err := s3Client.GetObjectTags(ctx, objectKey) - if err != nil { - return fmt.Errorf("failed to get tags for object in '%s': %w", objectTagsAddedEventName, err) - } + case "fee-denined": + err = handleFeeDenied(ctx, dynamoClient, event.CloudWatchEvent, now) - hasScannedTag := false - hasVirus := false + case "move-evidence-required": + err = handleMoreEvidenceRequired(ctx, dynamoClient, event.CloudWatchEvent, now) + } - for _, tag := range tags { - if *tag.Key == "virus-scan-status" { - hasScannedTag = true - hasVirus = *tag.Value == virusFound - break + if err != nil { + return fmt.Errorf("%s: %w", event.DetailType, err) } - } - if !hasScannedTag { return nil } - parts := strings.Split(objectKey, "/") - - lpa, err := getLpaByUID(ctx, dynamodbClient, parts[0], objectTagsAddedEventName) - if err != nil { - return err - } - - err = documentStore.UpdateScanResults(ctx, lpa.ID, objectKey, hasVirus) - if err != nil { - return fmt.Errorf("failed to update scan results for '%s': %w", objectTagsAddedEventName, err) - } - - return nil -} - -func getLpaByUID(ctx context.Context, client dynamodbClient, uid, eventName string) (page.Lpa, error) { - var key dynamo.Key - if err := client.OneByUID(ctx, uid, &key); err != nil { - return page.Lpa{}, fmt.Errorf("failed to resolve uid for '%s': %w", eventName, err) - } - - if key.PK == "" { - return page.Lpa{}, fmt.Errorf("PK missing from LPA in response to '%s'", eventName) - } - - var lpa page.Lpa - if err := client.One(ctx, key.PK, key.SK, &lpa); err != nil { - return page.Lpa{}, fmt.Errorf("failed to get LPA for '%s': %w", eventName, err) - } - - return lpa, nil + eJson, _ := json.Marshal(event) + return fmt.Errorf("unknown event type received: %s", string(eJson)) } func main() { - lambda.Start(Handler) + lambda.Start(handler) } diff --git a/cmd/event-received/mock_DocumentStore_test.go b/cmd/event-received/mock_DocumentStore_test.go index b62160b663..8fccebba72 100644 --- a/cmd/event-received/mock_DocumentStore_test.go +++ b/cmd/event-received/mock_DocumentStore_test.go @@ -13,13 +13,13 @@ type mockDocumentStore struct { mock.Mock } -// UpdateScanResults provides a mock function with given fields: ctx, pk, sk, virusDetected -func (_m *mockDocumentStore) UpdateScanResults(ctx context.Context, pk string, sk string, virusDetected bool) error { - ret := _m.Called(ctx, pk, sk, virusDetected) +// UpdateScanResults provides a mock function with given fields: ctx, lpaID, objectKey, virusDetected +func (_m *mockDocumentStore) UpdateScanResults(ctx context.Context, lpaID string, objectKey string, virusDetected bool) error { + ret := _m.Called(ctx, lpaID, objectKey, virusDetected) var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) error); ok { - r0 = rf(ctx, pk, sk, virusDetected) + r0 = rf(ctx, lpaID, objectKey, virusDetected) } else { r0 = ret.Error(0) } diff --git a/internal/page/mock_UidClient_test.go b/cmd/event-received/mock_UidClient_test.go similarity index 60% rename from internal/page/mock_UidClient_test.go rename to cmd/event-received/mock_UidClient_test.go index a75ba96486..23db3d21f7 100644 --- a/internal/page/mock_UidClient_test.go +++ b/cmd/event-received/mock_UidClient_test.go @@ -1,14 +1,12 @@ // Code generated by mockery v2.20.0. DO NOT EDIT. -package page +package main import ( context "context" - http "net/http" - - mock "github.com/stretchr/testify/mock" uid "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" + mock "github.com/stretchr/testify/mock" ) // mockUidClient is an autogenerated mock type for the UidClient type @@ -17,18 +15,18 @@ type mockUidClient struct { } // CreateCase provides a mock function with given fields: _a0, _a1 -func (_m *mockUidClient) CreateCase(_a0 context.Context, _a1 *uid.CreateCaseRequestBody) (uid.CreateCaseResponse, error) { +func (_m *mockUidClient) CreateCase(_a0 context.Context, _a1 *uid.CreateCaseRequestBody) (string, error) { ret := _m.Called(_a0, _a1) - var r0 uid.CreateCaseResponse + var r0 string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *uid.CreateCaseRequestBody) (uid.CreateCaseResponse, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *uid.CreateCaseRequestBody) (string, error)); ok { return rf(_a0, _a1) } - if rf, ok := ret.Get(0).(func(context.Context, *uid.CreateCaseRequestBody) uid.CreateCaseResponse); ok { + if rf, ok := ret.Get(0).(func(context.Context, *uid.CreateCaseRequestBody) string); ok { r0 = rf(_a0, _a1) } else { - r0 = ret.Get(0).(uid.CreateCaseResponse) + r0 = ret.Get(0).(string) } if rf, ok := ret.Get(1).(func(context.Context, *uid.CreateCaseRequestBody) error); ok { @@ -40,32 +38,6 @@ func (_m *mockUidClient) CreateCase(_a0 context.Context, _a1 *uid.CreateCaseRequ return r0, r1 } -// Health provides a mock function with given fields: _a0 -func (_m *mockUidClient) Health(_a0 context.Context) (*http.Response, error) { - ret := _m.Called(_a0) - - var r0 *http.Response - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (*http.Response, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(context.Context) *http.Response); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*http.Response) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - type mockConstructorTestingTnewMockUidClient interface { mock.TestingT Cleanup(func()) diff --git a/cmd/event-received/mock_UidStore_test.go b/cmd/event-received/mock_UidStore_test.go new file mode 100644 index 0000000000..a4fb16333f --- /dev/null +++ b/cmd/event-received/mock_UidStore_test.go @@ -0,0 +1,43 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package main + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// mockUidStore is an autogenerated mock type for the UidStore type +type mockUidStore struct { + mock.Mock +} + +// Set provides a mock function with given fields: ctx, lpaID, sessionID, uid +func (_m *mockUidStore) Set(ctx context.Context, lpaID string, sessionID string, uid string) error { + ret := _m.Called(ctx, lpaID, sessionID, uid) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, lpaID, sessionID, uid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTnewMockUidStore interface { + mock.TestingT + Cleanup(func()) +} + +// newMockUidStore creates a new instance of mockUidStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func newMockUidStore(t mockConstructorTestingTnewMockUidStore) *mockUidStore { + mock := &mockUidStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/cmd/event-received/mock_dynamodbClient_test.go b/cmd/event-received/mock_dynamodbClient_test.go index 726e121708..d18c22007d 100644 --- a/cmd/event-received/mock_dynamodbClient_test.go +++ b/cmd/event-received/mock_dynamodbClient_test.go @@ -5,6 +5,7 @@ package main import ( context "context" + types "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" mock "github.com/stretchr/testify/mock" ) @@ -55,6 +56,20 @@ func (_m *mockDynamodbClient) Put(ctx context.Context, v interface{}) error { return r0 } +// Update provides a mock function with given fields: ctx, pk, sk, values, expression +func (_m *mockDynamodbClient) Update(ctx context.Context, pk string, sk string, values map[string]types.AttributeValue, expression string) error { + ret := _m.Called(ctx, pk, sk, values, expression) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, map[string]types.AttributeValue, string) error); ok { + r0 = rf(ctx, pk, sk, values, expression) + } else { + r0 = ret.Error(0) + } + + return r0 +} + type mockConstructorTestingTnewMockDynamodbClient interface { mock.TestingT Cleanup(func()) diff --git a/cmd/mlpa/main.go b/cmd/mlpa/main.go index e176f28926..a835fefdc2 100644 --- a/cmd/mlpa/main.go +++ b/cmd/mlpa/main.go @@ -212,7 +212,6 @@ func main() { staticHash, page.Paths, signInClient, - uidClient, oneloginURL, evidenceS3Client, eventClient, @@ -232,7 +231,6 @@ func main() { staticHash, page.Paths, signInClient, - uidClient, oneloginURL, evidenceS3Client, eventClient, diff --git a/cypress/e2e/donor/dashboard.cy.js b/cypress/e2e/donor/dashboard.cy.js index d92fc9e3a0..9bd766784c 100644 --- a/cypress/e2e/donor/dashboard.cy.js +++ b/cypress/e2e/donor/dashboard.cy.js @@ -17,24 +17,8 @@ describe('Dashboard', () => { it('can create another reusing some previous details', () => { cy.contains('button', 'Start now').click(); - cy.get('#f-first-names').clear().type('Jane'); - 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('1 RICHMOND PLACE, BIRMINGHAM, B14 7ED'); - cy.contains('button', 'Continue').click(); - cy.contains('button', 'Continue').click(); - - cy.get('#f-lpa-type-2').check(); - cy.contains('button', 'Continue').click(); - - cy.visit('/dashboard'); - cy.visit('/dashboard'); - - cy.contains('Property and affairs: Sam Smith'); - cy.contains('Personal welfare: Jane Smith'); + cy.get('#f-first-names').should('have.value', 'Sam'); + cy.get('#f-last-name').should('have.value', 'Smith'); }); }) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a92d83c71d..7a9bdb7a89 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -39,6 +39,7 @@ services: - APP_PUBLIC_URL=http://localhost:5050 - GOVUK_NOTIFY_BASE_URL=http://mock-notify:8080 - UPLOADS_S3_BUCKET_NAME=evidence + - UID_BASE_URL=http://mock-uid:8080 ports: - "9000:8080" entrypoint: aws-lambda-rie /var/task/event-received diff --git a/docker/event-received/Dockerfile b/docker/event-received/Dockerfile index f0165481e4..524ab2e8e0 100644 --- a/docker/event-received/Dockerfile +++ b/docker/event-received/Dockerfile @@ -14,7 +14,7 @@ FROM public.ecr.aws/lambda/provided:al2 AS dev WORKDIR /app -COPY --from=build /app/event-received ./event-received +COPY --from=build /app/event-received /var/task/event-received COPY lang ./lang COPY docker/event-received/aws-lambda-rie ./aws-lambda-rie diff --git a/internal/app/app.go b/internal/app/app.go index 43299c3c40..554639b31f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -29,7 +29,6 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/random" "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" - "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" ) //go:generate mockery --testonly --inpackage --name Logger --structname mockLogger @@ -84,7 +83,6 @@ func App( staticHash string, paths page.AppPaths, oneLoginClient *onelogin.Client, - uidClient *uid.Client, oneloginURL string, s3Client S3Client, eventClient *event.Client, @@ -94,7 +92,6 @@ func App( donorStore := &donorStore{ dynamoClient: lpaDynamoClient, eventClient: eventClient, - uidClient: uidClient, logger: logger, uuidString: uuid.NewString, now: time.Now, diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 370429a818..4d2286e4d9 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -17,12 +17,11 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/s3" "github.com/ministryofjustice/opg-modernising-lpa/internal/sesh" - "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" "github.com/stretchr/testify/assert" ) func TestApp(t *testing.T) { - app := App(&logging.Logger{}, &localize.Localizer{}, localize.En, template.Templates{}, nil, &dynamo.Client{}, "http://public.url", &pay.Client{}, ¬ify.Client{}, &place.Client{}, page.RumConfig{}, "?%3fNEI0t9MN", page.Paths, &onelogin.Client{}, &uid.Client{}, "http://onelogin.url", &s3.Client{}, nil) + app := App(&logging.Logger{}, &localize.Localizer{}, localize.En, template.Templates{}, nil, &dynamo.Client{}, "http://public.url", &pay.Client{}, ¬ify.Client{}, &place.Client{}, page.RumConfig{}, "?%3fNEI0t9MN", page.Paths, &onelogin.Client{}, "http://onelogin.url", &s3.Client{}, nil) assert.Implements(t, (*http.Handler)(nil), app) } diff --git a/internal/app/donor_store.go b/internal/app/donor_store.go index 414e69fd2b..d0ae7a7a85 100644 --- a/internal/app/donor_store.go +++ b/internal/app/donor_store.go @@ -7,9 +7,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" - "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + "github.com/ministryofjustice/opg-modernising-lpa/internal/event" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" - "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" ) @@ -20,7 +19,10 @@ type UidClient interface { //go:generate mockery --testonly --inpackage --name EventClient --structname mockEventClient type EventClient interface { - Send(context.Context, string, any) error + SendUidRequested(context.Context, event.UidRequested) error + SendApplicationUpdated(context.Context, event.ApplicationUpdated) error + SendPreviousApplicationLinked(context.Context, event.PreviousApplicationLinked) error + SendReducedFeeRequested(context.Context, event.ReducedFeeRequested) error } //go:generate mockery --testonly --inpackage --name DocumentStore --structname mockDocumentStore @@ -34,7 +36,6 @@ type DocumentStore interface { type donorStore struct { dynamoClient DynamoClient eventClient EventClient - uidClient UidClient logger Logger uuidString func() string now func() time.Time @@ -130,55 +131,61 @@ func (s *donorStore) Latest(ctx context.Context) (*page.Lpa, error) { } 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(), + // 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.Type.Empty() && !lpa.HasSentUidRequestedEvent { + data, err := page.SessionDataFromContext(ctx) + if err != nil { + return err + } + + if err := s.eventClient.SendUidRequested(ctx, event.UidRequested{ + LpaID: lpa.ID, + DonorSessionID: data.SessionID, + Type: lpa.Type.String(), Donor: uid.DonorDetails{ Name: lpa.Donor.FullName(), Dob: lpa.Donor.DateOfBirth, Postcode: lpa.Donor.Address.Postcode, }, - }) - if err != nil { - s.logger.Print(err) - } else { - lpa.UID = resp.UID + }); err != nil { + return err } - } - // 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() + lpa.HasSentUidRequestedEvent = true } if lpa.UID != "" && !lpa.HasSentApplicationUpdatedEvent { - if err := s.eventClient.Send(ctx, "application-updated", applicationUpdatedEvent{ + if err := s.eventClient.SendApplicationUpdated(ctx, event.ApplicationUpdated{ UID: lpa.UID, Type: lpa.Type.String(), CreatedAt: lpa.CreatedAt, - Donor: applicationUpdatedEventDonor{ + Donor: event.ApplicationUpdatedDonor{ FirstNames: lpa.Donor.FirstNames, LastName: lpa.Donor.LastName, DateOfBirth: lpa.Donor.DateOfBirth, Address: lpa.Donor.Address, }, }); err != nil { - s.logger.Print(err) - } else { - lpa.HasSentApplicationUpdatedEvent = true + return err } + + lpa.HasSentApplicationUpdatedEvent = true } if lpa.UID != "" && lpa.PreviousApplicationNumber != "" && !lpa.HasSentPreviousApplicationLinkedEvent { - if err := s.eventClient.Send(ctx, "previous-application-linked", previousApplicationLinkedEvent{ + if err := s.eventClient.SendPreviousApplicationLinked(ctx, event.PreviousApplicationLinked{ UID: lpa.UID, PreviousApplicationNumber: lpa.PreviousApplicationNumber, }); err != nil { - s.logger.Print(err) - } else { - lpa.HasSentPreviousApplicationLinkedEvent = true + return err } + + lpa.HasSentPreviousApplicationLinkedEvent = true } if lpa.UID != "" && lpa.Tasks.PayForLpa.IsPending() { @@ -197,27 +204,27 @@ func (s *donorStore) Put(ctx context.Context, lpa *page.Lpa) error { } if len(unsentKeys) > 0 { - if err := s.eventClient.Send(ctx, "reduced-fee-requested", reducedFeeRequestedEvent{ + if err := s.eventClient.SendReducedFeeRequested(ctx, event.ReducedFeeRequested{ UID: lpa.UID, RequestType: lpa.FeeType.String(), Evidence: unsentKeys, EvidenceDelivery: lpa.EvidenceDelivery.String(), }); err != nil { - s.logger.Print(err) - } else { - var updatedDocuments page.Documents - - for _, document := range documents { - if document.Sent.IsZero() && !document.Scanned { - document.Sent = s.now() - updatedDocuments = append(updatedDocuments, document) - } - } + return err + } + + var updatedDocuments page.Documents - if err := s.documentStore.BatchPut(ctx, updatedDocuments); err != nil { - s.logger.Print(err) + for _, document := range documents { + if document.Sent.IsZero() && !document.Scanned { + document.Sent = s.now() + updatedDocuments = append(updatedDocuments, document) } } + + if err := s.documentStore.BatchPut(ctx, updatedDocuments); err != nil { + s.logger.Print(err) + } } } @@ -265,29 +272,3 @@ func donorKey(s string) string { func subKey(s string) string { return "#SUB#" + s } - -type applicationUpdatedEvent struct { - UID string `json:"uid"` - Type string `json:"type"` - CreatedAt time.Time `json:"createdAt"` - Donor applicationUpdatedEventDonor `json:"donor"` -} - -type applicationUpdatedEventDonor struct { - FirstNames string `json:"firstNames"` - LastName string `json:"lastName"` - DateOfBirth date.Date `json:"dob"` - Address place.Address `json:"address"` -} - -type previousApplicationLinkedEvent struct { - UID string `json:"uid"` - PreviousApplicationNumber string `json:"previousApplicationNumber"` -} - -type reducedFeeRequestedEvent struct { - UID string `json:"uid"` - RequestType string `json:"requestType"` - Evidence []string `json:"evidence"` - EvidenceDelivery string `json:"evidenceDelivery"` -} diff --git a/internal/app/donor_store_test.go b/internal/app/donor_store_test.go index dddd355a90..c53fd88f69 100644 --- a/internal/app/donor_store_test.go +++ b/internal/app/donor_store_test.go @@ -4,21 +4,19 @@ import ( "context" "encoding/json" "errors" - "os" - "strings" "testing" "time" "github.com/ministryofjustice/opg-modernising-lpa/internal/actor" "github.com/ministryofjustice/opg-modernising-lpa/internal/date" "github.com/ministryofjustice/opg-modernising-lpa/internal/dynamo" + "github.com/ministryofjustice/opg-modernising-lpa/internal/event" "github.com/ministryofjustice/opg-modernising-lpa/internal/page" "github.com/ministryofjustice/opg-modernising-lpa/internal/pay" "github.com/ministryofjustice/opg-modernising-lpa/internal/place" "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" "github.com/stretchr/testify/assert" mock "github.com/stretchr/testify/mock" - "github.com/xeipuuv/gojsonschema" ) var expectedError = errors.New("err") @@ -226,43 +224,29 @@ func TestDonorStorePutWhenError(t *testing.T) { } func TestDonorStorePutWhenUIDNeeded(t *testing.T) { - ctx := context.Background() + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) now := time.Now() eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "application-updated", applicationUpdatedEvent{ - UID: "M-1111", - Type: "hw", - Donor: applicationUpdatedEventDonor{ - FirstNames: "John", - LastName: "Smith", - DateOfBirth: date.New("2000", "01", "01"), - Address: place.Address{Line1: "line", Postcode: "F1 1FF"}, - }, - }). - Return(nil) - - uidClient := newMockUidClient(t) - uidClient. - On("CreateCase", ctx, &uid.CreateCaseRequestBody{ - Type: "hw", + On("SendUidRequested", ctx, event.UidRequested{ + LpaID: "5", + DonorSessionID: "an-id", + Type: "hw", Donor: uid.DonorDetails{ Name: "John Smith", Dob: date.New("2000", "01", "01"), Postcode: "F1 1FF", }, }). - Return(uid.CreateCaseResponse{UID: "M-1111"}, nil) + Return(nil) dynamoClient := newMockDynamoClient(t) dynamoClient. On("Put", ctx, &page.Lpa{ - PK: "LPA#5", - SK: "#DONOR#an-id", - ID: "5", - UID: "M-1111", - UpdatedAt: now, + PK: "LPA#5", + SK: "#DONOR#an-id", + ID: "5", Donor: actor.Donor{ FirstNames: "John", LastName: "Smith", @@ -272,12 +256,12 @@ func TestDonorStorePutWhenUIDNeeded(t *testing.T) { Postcode: "F1 1FF", }, }, - Type: page.LpaTypeHealthWelfare, - HasSentApplicationUpdatedEvent: true, + Type: page.LpaTypeHealthWelfare, + HasSentUidRequestedEvent: true, }). Return(nil) - donorStore := &donorStore{dynamoClient: dynamoClient, uidClient: uidClient, eventClient: eventClient, now: func() time.Time { return now }} + donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, now: func() time.Time { return now }} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -298,24 +282,39 @@ func TestDonorStorePutWhenUIDNeeded(t *testing.T) { assert.Nil(t, err) } -func TestDonorStorePutWhenUIDFails(t *testing.T) { +func TestDonorStorePutWhenUIDNeededMissingSessionData(t *testing.T) { ctx := context.Background() - dynamoClient := newMockDynamoClient(t) - dynamoClient. - On("Put", ctx, mock.Anything). - Return(nil) + donorStore := &donorStore{} + + err := donorStore.Put(ctx, &page.Lpa{ + PK: "LPA#5", + SK: "#DONOR#an-id", + ID: "5", + Donor: actor.Donor{ + FirstNames: "John", + LastName: "Smith", + DateOfBirth: date.New("2000", "01", "01"), + Address: place.Address{ + Line1: "line", + Postcode: "F1 1FF", + }, + }, + Type: page.LpaTypeHealthWelfare, + }) - uidClient := newMockUidClient(t) - uidClient. - On("CreateCase", ctx, mock.Anything). - Return(uid.CreateCaseResponse{UID: "M-1111"}, expectedError) + assert.Equal(t, page.SessionMissingError{}, err) +} - logger := newMockLogger(t) - logger. - On("Print", expectedError) +func TestDonorStorePutWhenUIDFails(t *testing.T) { + ctx := page.ContextWithSessionData(context.Background(), &page.SessionData{SessionID: "an-id"}) + + eventClient := newMockEventClient(t) + eventClient. + On("SendUidRequested", ctx, mock.Anything). + Return(expectedError) - donorStore := &donorStore{dynamoClient: dynamoClient, uidClient: uidClient, logger: logger, now: time.Now} + donorStore := &donorStore{eventClient: eventClient, now: time.Now} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -332,7 +331,7 @@ func TestDonorStorePutWhenUIDFails(t *testing.T) { Type: page.LpaTypeHealthWelfare, }) - assert.Nil(t, err) + assert.Equal(t, expectedError, err) } func TestDonorStorePutWhenApplicationUpdatedWhenError(t *testing.T) { @@ -341,19 +340,10 @@ func TestDonorStorePutWhenApplicationUpdatedWhenError(t *testing.T) { eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "application-updated", mock.Anything). + On("SendApplicationUpdated", ctx, mock.Anything). Return(expectedError) - dynamoClient := newMockDynamoClient(t) - dynamoClient. - On("Put", ctx, mock.Anything). - Return(nil) - - logger := newMockLogger(t) - logger. - On("Print", expectedError) - - donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, logger: logger, now: func() time.Time { return now }} + donorStore := &donorStore{eventClient: eventClient, now: func() time.Time { return now }} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -371,7 +361,7 @@ func TestDonorStorePutWhenApplicationUpdatedWhenError(t *testing.T) { Type: page.LpaTypeHealthWelfare, }) - assert.Nil(t, err) + assert.Equal(t, expectedError, err) } func TestDonorStorePutWhenPreviousApplicationLinked(t *testing.T) { @@ -380,7 +370,7 @@ func TestDonorStorePutWhenPreviousApplicationLinked(t *testing.T) { eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "previous-application-linked", previousApplicationLinkedEvent{ + On("SendPreviousApplicationLinked", ctx, event.PreviousApplicationLinked{ UID: "M-1111", PreviousApplicationNumber: "5555", }). @@ -442,21 +432,12 @@ func TestDonorStorePutWhenPreviousApplicationLinkedWhenError(t *testing.T) { ctx := context.Background() now := time.Now() - dynamoClient := newMockDynamoClient(t) - dynamoClient. - On("Put", ctx, mock.Anything). - Return(nil) - eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "previous-application-linked", mock.Anything). + On("SendPreviousApplicationLinked", ctx, mock.Anything). Return(expectedError) - logger := newMockLogger(t) - logger. - On("Print", expectedError) - - donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, logger: logger, now: func() time.Time { return now }} + donorStore := &donorStore{eventClient: eventClient, now: func() time.Time { return now }} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -466,8 +447,7 @@ func TestDonorStorePutWhenPreviousApplicationLinkedWhenError(t *testing.T) { PreviousApplicationNumber: "5555", HasSentApplicationUpdatedEvent: true, }) - - assert.Nil(t, err) + assert.Equal(t, expectedError, err) } func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocuments(t *testing.T) { @@ -491,7 +471,7 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocuments(t *testing.T) { eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "reduced-fee-requested", reducedFeeRequestedEvent{ + On("SendReducedFeeRequested", ctx, event.ReducedFeeRequested{ UID: "M-1111", RequestType: "HalfFee", Evidence: []string{"lpa-uid-evidence-a-uid"}, @@ -593,7 +573,7 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenEventClientSe eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "reduced-fee-requested", reducedFeeRequestedEvent{ + On("SendReducedFeeRequested", ctx, event.ReducedFeeRequested{ UID: "M-1111", RequestType: "HalfFee", Evidence: []string{"lpa-uid-evidence-a-uid"}, @@ -601,11 +581,6 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenEventClientSe }). Return(expectedError) - dynamoClient := newMockDynamoClient(t) - dynamoClient. - On("Put", ctx, mock.Anything). - Return(nil) - documentStore := newMockDocumentStore(t) documentStore. On("GetAll", ctx). @@ -613,11 +588,7 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenEventClientSe {Key: "lpa-uid-evidence-a-uid", Filename: "whatever.pdf"}, }, nil) - logger := newMockLogger(t) - logger. - On("Print", expectedError) - - donorStore := &donorStore{dynamoClient: dynamoClient, eventClient: eventClient, now: func() time.Time { return now }, documentStore: documentStore, logger: logger} + donorStore := &donorStore{eventClient: eventClient, now: func() time.Time { return now }, documentStore: documentStore} err := donorStore.Put(ctx, &page.Lpa{ PK: "LPA#5", @@ -630,7 +601,7 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenEventClientSe EvidenceDelivery: pay.Upload, }) - assert.Nil(t, err) + assert.Equal(t, expectedError, err) } func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenDocumentStoreBatchPutError(t *testing.T) { @@ -639,7 +610,7 @@ func TestDonorStorePutWhenReducedFeeRequestedAndUnsentDocumentsWhenDocumentStore eventClient := newMockEventClient(t) eventClient. - On("Send", ctx, "reduced-fee-requested", reducedFeeRequestedEvent{ + On("SendReducedFeeRequested", ctx, event.ReducedFeeRequested{ UID: "M-1111", RequestType: "HalfFee", Evidence: []string{"lpa-uid-evidence-a-uid"}, @@ -844,53 +815,3 @@ func TestDonorStoreDeleteWhenSessionMissing(t *testing.T) { }) } } - -func TestEventSchema(t *testing.T) { - testcases := map[string]any{ - "application-updated": applicationUpdatedEvent{ - UID: "M-0000-0000-0000", - Type: "hw", - CreatedAt: time.Now(), - Donor: applicationUpdatedEventDonor{ - FirstNames: "syz", - LastName: "syz", - DateOfBirth: date.New("2000", "01", "01"), - Address: place.Address{ - Line1: "line1", - Line2: "line2", - Line3: "line3", - TownOrCity: "townOrCity", - Postcode: "F1 1FF", - Country: "GB", - }, - }, - }, - "reduced-fee-requested": reducedFeeRequestedEvent{ - UID: "M-0000-0000-0000", - RequestType: "NoFee", - Evidence: []string{"key"}, - EvidenceDelivery: "upload", - }, - } - - dir, _ := os.Getwd() - - for name, event := range testcases { - t.Run(name, func(t *testing.T) { - schemaLoader := gojsonschema.NewReferenceLoader("file:///" + dir + "/testdata/" + name + ".json") - documentLoader := gojsonschema.NewGoLoader(event) - - result, err := gojsonschema.Validate(schemaLoader, documentLoader) - assert.Nil(t, err) - - if !assert.True(t, result.Valid()) { - lines := []string{"The document is not valid:"} - for _, desc := range result.Errors() { - lines = append(lines, "- "+desc.String()) - } - - t.Log(strings.Join(lines, "\n")) - } - }) - } -} diff --git a/internal/app/mock_EventClient_test.go b/internal/app/mock_EventClient_test.go index c0a4b1c57e..4e3662ea72 100644 --- a/internal/app/mock_EventClient_test.go +++ b/internal/app/mock_EventClient_test.go @@ -5,6 +5,7 @@ package app import ( context "context" + event "github.com/ministryofjustice/opg-modernising-lpa/internal/event" mock "github.com/stretchr/testify/mock" ) @@ -13,13 +14,55 @@ type mockEventClient struct { mock.Mock } -// Send provides a mock function with given fields: _a0, _a1, _a2 -func (_m *mockEventClient) Send(_a0 context.Context, _a1 string, _a2 interface{}) error { - ret := _m.Called(_a0, _a1, _a2) +// SendApplicationUpdated provides a mock function with given fields: _a0, _a1 +func (_m *mockEventClient) SendApplicationUpdated(_a0 context.Context, _a1 event.ApplicationUpdated) error { + ret := _m.Called(_a0, _a1) var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, interface{}) error); ok { - r0 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(0).(func(context.Context, event.ApplicationUpdated) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendPreviousApplicationLinked provides a mock function with given fields: _a0, _a1 +func (_m *mockEventClient) SendPreviousApplicationLinked(_a0 context.Context, _a1 event.PreviousApplicationLinked) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, event.PreviousApplicationLinked) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendReducedFeeRequested provides a mock function with given fields: _a0, _a1 +func (_m *mockEventClient) SendReducedFeeRequested(_a0 context.Context, _a1 event.ReducedFeeRequested) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, event.ReducedFeeRequested) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SendUidRequested provides a mock function with given fields: _a0, _a1 +func (_m *mockEventClient) SendUidRequested(_a0 context.Context, _a1 event.UidRequested) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, event.UidRequested) error); ok { + r0 = rf(_a0, _a1) } else { r0 = ret.Error(0) } diff --git a/internal/app/uid_store.go b/internal/app/uid_store.go new file mode 100644 index 0000000000..740ad0608c --- /dev/null +++ b/internal/app/uid_store.go @@ -0,0 +1,35 @@ +package app + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" +) + +type DynamoUpdateClient interface { + Update(ctx context.Context, pk, sk string, values map[string]types.AttributeValue, expression string) error +} + +type uidStore struct { + dynamoClient DynamoUpdateClient + now func() time.Time +} + +func NewUidStore(dynamoClient DynamoUpdateClient, now func() time.Time) *uidStore { + return &uidStore{dynamoClient: dynamoClient, now: now} +} + +func (s *uidStore) Set(ctx context.Context, lpaID, sessionID, uid string) error { + values, err := attributevalue.MarshalMap(map[string]any{ + ":uid": uid, + ":now": s.now(), + }) + if err != nil { + return err + } + + return s.dynamoClient.Update(ctx, lpaKey(lpaID), donorKey(sessionID), values, + "set UID = :uid, UpdatedAt = :now") +} diff --git a/internal/event/client.go b/internal/event/client.go index e66fee1756..eeb3a7b0a0 100644 --- a/internal/event/client.go +++ b/internal/event/client.go @@ -28,7 +28,23 @@ func NewClient(cfg aws.Config, eventBusName string) *Client { } } -func (c *Client) Send(ctx context.Context, detailType string, detail any) error { +func (c *Client) SendUidRequested(ctx context.Context, event UidRequested) error { + return c.send(ctx, "uid-requested", event) +} + +func (c *Client) SendApplicationUpdated(ctx context.Context, event ApplicationUpdated) error { + return c.send(ctx, "application-updated", event) +} + +func (c *Client) SendPreviousApplicationLinked(ctx context.Context, event PreviousApplicationLinked) error { + return c.send(ctx, "previous-application-linked", event) +} + +func (c *Client) SendReducedFeeRequested(ctx context.Context, event ReducedFeeRequested) error { + return c.send(ctx, "reduced-fee-requested", event) +} + +func (c *Client) send(ctx context.Context, detailType string, detail any) error { v, err := json.Marshal(detail) if err != nil { return err diff --git a/internal/event/client_test.go b/internal/event/client_test.go index 589462c447..79ab1f5b5c 100644 --- a/internal/event/client_test.go +++ b/internal/event/client_test.go @@ -2,6 +2,7 @@ package event import ( "context" + "encoding/json" "errors" "testing" @@ -13,23 +14,53 @@ import ( var expectedError = errors.New("err") -func TestClientSend(t *testing.T) { +func TestClientSendApplicationUpdated(t *testing.T) { ctx := context.Background() - svc := newMockEventbridgeClient(t) - svc. - On("PutEvents", ctx, &eventbridge.PutEventsInput{ - Entries: []types.PutEventsRequestEntry{{ - EventBusName: aws.String("my-bus"), - Source: aws.String("opg.poas.makeregister"), - DetailType: aws.String("my-detail"), - Detail: aws.String(`{"my":"event"}`), - }}, - }). - Return(nil, expectedError) - - client := Client{svc: svc, eventBusName: "my-bus"} - err := client.Send(ctx, "my-detail", map[string]string{"my": "event"}) - - assert.Equal(t, expectedError, err) + testcases := map[string]func() (func(*Client) error, any){ + "uid-requested": func() (func(*Client) error, any) { + event := UidRequested{LpaID: "5"} + + return func(client *Client) error { return client.SendUidRequested(ctx, event) }, event + }, + "application-updated": func() (func(*Client) error, any) { + event := ApplicationUpdated{UID: "a"} + + return func(client *Client) error { return client.SendApplicationUpdated(ctx, event) }, event + }, + "previous-application-linked": func() (func(*Client) error, any) { + event := PreviousApplicationLinked{UID: "a"} + + return func(client *Client) error { return client.SendPreviousApplicationLinked(ctx, event) }, event + }, + "reduced-fee-requested": func() (func(*Client) error, any) { + event := ReducedFeeRequested{UID: "a"} + + return func(client *Client) error { return client.SendReducedFeeRequested(ctx, event) }, event + }, + } + + for eventName, setup := range testcases { + t.Run(eventName, func(t *testing.T) { + fn, event := setup() + data, _ := json.Marshal(event) + + svc := newMockEventbridgeClient(t) + svc. + On("PutEvents", ctx, &eventbridge.PutEventsInput{ + Entries: []types.PutEventsRequestEntry{{ + EventBusName: aws.String("my-bus"), + Source: aws.String("opg.poas.makeregister"), + DetailType: aws.String(eventName), + Detail: aws.String(string(data)), + }}, + }). + Return(nil, expectedError) + + client := &Client{svc: svc, eventBusName: "my-bus"} + err := fn(client) + + assert.Equal(t, expectedError, err) + }) + } } diff --git a/internal/event/events.go b/internal/event/events.go new file mode 100644 index 0000000000..7cf1bd66bc --- /dev/null +++ b/internal/event/events.go @@ -0,0 +1,42 @@ +package event + +import ( + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" + "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" +) + +type UidRequested struct { + LpaID string + DonorSessionID string + Type string + Donor uid.DonorDetails +} + +type ApplicationUpdated struct { + UID string `json:"uid"` + Type string `json:"type"` + CreatedAt time.Time `json:"createdAt"` + Donor ApplicationUpdatedDonor `json:"donor"` +} + +type ApplicationUpdatedDonor struct { + FirstNames string `json:"firstNames"` + LastName string `json:"lastName"` + DateOfBirth date.Date `json:"dob"` + Address place.Address `json:"address"` +} + +type PreviousApplicationLinked struct { + UID string `json:"uid"` + PreviousApplicationNumber string `json:"previousApplicationNumber"` +} + +type ReducedFeeRequested struct { + UID string `json:"uid"` + RequestType string `json:"requestType"` + Evidence []string `json:"evidence"` + EvidenceDelivery string `json:"evidenceDelivery"` +} diff --git a/internal/event/events_test.go b/internal/event/events_test.go new file mode 100644 index 0000000000..4833460b11 --- /dev/null +++ b/internal/event/events_test.go @@ -0,0 +1,63 @@ +package event + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/ministryofjustice/opg-modernising-lpa/internal/date" + "github.com/ministryofjustice/opg-modernising-lpa/internal/place" + "github.com/stretchr/testify/assert" + "github.com/xeipuuv/gojsonschema" +) + +func TestEventSchema(t *testing.T) { + testcases := map[string]any{ + "application-updated": ApplicationUpdated{ + UID: "M-0000-0000-0000", + Type: "hw", + CreatedAt: time.Now(), + Donor: ApplicationUpdatedDonor{ + FirstNames: "syz", + LastName: "syz", + DateOfBirth: date.New("2000", "01", "01"), + Address: place.Address{ + Line1: "line1", + Line2: "line2", + Line3: "line3", + TownOrCity: "townOrCity", + Postcode: "F1 1FF", + Country: "GB", + }, + }, + }, + "reduced-fee-requested": ReducedFeeRequested{ + UID: "M-0000-0000-0000", + RequestType: "NoFee", + Evidence: []string{"key"}, + EvidenceDelivery: "upload", + }, + } + + dir, _ := os.Getwd() + + for name, event := range testcases { + t.Run(name, func(t *testing.T) { + schemaLoader := gojsonschema.NewReferenceLoader("file:///" + dir + "/testdata/" + name + ".json") + documentLoader := gojsonschema.NewGoLoader(event) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + assert.Nil(t, err) + + if !assert.True(t, result.Valid()) { + lines := []string{"The document is not valid:"} + for _, desc := range result.Errors() { + lines = append(lines, "- "+desc.String()) + } + + t.Log(strings.Join(lines, "\n")) + } + }) + } +} diff --git a/internal/app/testdata/application-updated.json b/internal/event/testdata/application-updated.json similarity index 100% rename from internal/app/testdata/application-updated.json rename to internal/event/testdata/application-updated.json diff --git a/internal/app/testdata/reduced-fee-requested.json b/internal/event/testdata/reduced-fee-requested.json similarity index 100% rename from internal/app/testdata/reduced-fee-requested.json rename to internal/event/testdata/reduced-fee-requested.json diff --git a/internal/page/common.go b/internal/page/common.go index 0c2178da6d..4dddf9534d 100644 --- a/internal/page/common.go +++ b/internal/page/common.go @@ -11,7 +11,6 @@ import ( "github.com/ministryofjustice/opg-modernising-lpa/internal/localize" "github.com/ministryofjustice/opg-modernising-lpa/internal/notify" "github.com/ministryofjustice/opg-modernising-lpa/internal/onelogin" - "github.com/ministryofjustice/opg-modernising-lpa/internal/uid" ) const FormUrlEncoded = "application/x-www-form-urlencoded" @@ -92,9 +91,3 @@ func PostFormReferenceNumber(r *http.Request, name string) string { //go:generate mockery --testonly --inpackage --name Handler --structname mockHandler type Handler func(data AppData, w http.ResponseWriter, r *http.Request) error - -//go:generate mockery --testonly --inpackage --name UidClient --structname mockUidClient -type UidClient interface { - CreateCase(context.Context, *uid.CreateCaseRequestBody) (uid.CreateCaseResponse, error) - Health(context.Context) (*http.Response, error) -} diff --git a/internal/page/data.go b/internal/page/data.go index 8b460c1d93..8eca1aa9bc 100644 --- a/internal/page/data.go +++ b/internal/page/data.go @@ -155,6 +155,7 @@ type Lpa struct { // PreviousFee is the fee previously paid for an LPA PreviousFee pay.PreviousFee + HasSentUidRequestedEvent bool HasSentApplicationUpdatedEvent bool HasSentPreviousApplicationLinkedEvent bool } diff --git a/internal/page/dependency_health_check.go b/internal/page/dependency_health_check.go index 1cac1fde15..e4467ca33e 100644 --- a/internal/page/dependency_health_check.go +++ b/internal/page/dependency_health_check.go @@ -1,13 +1,19 @@ package page import ( + "context" "fmt" "net/http" ) -func DependencyHealthCheck(logger Logger, uidClient UidClient) http.HandlerFunc { +//go:generate mockery --testonly --inpackage --name HealthChecker --structname mockHealthChecker +type HealthChecker interface { + Health(context.Context) (*http.Response, error) +} + +func DependencyHealthCheck(logger Logger, uidService HealthChecker) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - resp, err := uidClient.Health(r.Context()) + resp, err := uidService.Health(r.Context()) if err != nil { logger.Print(fmt.Sprintf("Error while getting UID service status: %s", err.Error())) diff --git a/internal/page/dependency_health_check_test.go b/internal/page/dependency_health_check_test.go index 967b1ac647..cb2955355c 100644 --- a/internal/page/dependency_health_check_test.go +++ b/internal/page/dependency_health_check_test.go @@ -21,7 +21,7 @@ func TestDependencyHealthCheck(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() - uidClient := newMockUidClient(t) + uidClient := newMockHealthChecker(t) uidClient. On("Health", mock.Anything). Return(&http.Response{StatusCode: status}, nil) @@ -38,7 +38,7 @@ func TestDependencyHealthCheckUidOnRequestError(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() - uidClient := newMockUidClient(t) + uidClient := newMockHealthChecker(t) uidClient. On("Health", mock.Anything). Return(&http.Response{}, expectedError) diff --git a/internal/page/fixtures/dashboard.go b/internal/page/fixtures/dashboard.go index 6e01126b77..cb2cd73e72 100644 --- a/internal/page/fixtures/dashboard.go +++ b/internal/page/fixtures/dashboard.go @@ -56,6 +56,7 @@ func Dashboard( donorCtx := page.ContextWithSessionData(r.Context(), &page.SessionData{SessionID: meSessionID, LpaID: lpa.ID}) + lpa.UID = "M-1111-1111-1111" lpa.Donor = makeDonor() lpa.Type = page.LpaTypePropertyFinance diff --git a/internal/page/mock_HealthChecker_test.go b/internal/page/mock_HealthChecker_test.go new file mode 100644 index 0000000000..16d911ba0b --- /dev/null +++ b/internal/page/mock_HealthChecker_test.go @@ -0,0 +1,56 @@ +// Code generated by mockery v2.20.0. DO NOT EDIT. + +package page + +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" +) + +// mockHealthChecker is an autogenerated mock type for the HealthChecker type +type mockHealthChecker struct { + mock.Mock +} + +// Health provides a mock function with given fields: _a0 +func (_m *mockHealthChecker) Health(_a0 context.Context) (*http.Response, error) { + ret := _m.Called(_a0) + + var r0 *http.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*http.Response, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) *http.Response); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTnewMockHealthChecker interface { + mock.TestingT + Cleanup(func()) +} + +// newMockHealthChecker creates a new instance of mockHealthChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func newMockHealthChecker(t mockConstructorTestingTnewMockHealthChecker) *mockHealthChecker { + mock := &mockHealthChecker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/uid/client.go b/internal/uid/client.go index 9327686b07..d5692fcac5 100644 --- a/internal/uid/client.go +++ b/internal/uid/client.go @@ -69,9 +69,9 @@ type CreateCaseResponseBadRequestError struct { Detail string `json:"detail"` } -func (c *Client) CreateCase(ctx context.Context, body *CreateCaseRequestBody) (CreateCaseResponse, error) { +func (c *Client) CreateCase(ctx context.Context, body *CreateCaseRequestBody) (string, error) { if !body.Valid() { - return CreateCaseResponse{}, errors.New("CreateCaseRequestBody missing details. Requires Type, Donor name, dob and postcode") + return "", errors.New("CreateCaseRequestBody missing details. Requires Type, Donor name, dob and postcode") } body.Source = "APPLICANT" @@ -79,39 +79,38 @@ func (c *Client) CreateCase(ctx context.Context, body *CreateCaseRequestBody) (C r, err := http.NewRequest(http.MethodPost, c.baseURL+"/cases", bytes.NewReader(data)) if err != nil { - return CreateCaseResponse{}, err + return "", err } r.Header.Add("Content-Type", "application/json") err = c.sign(ctx, r, apiGatewayServiceName) if err != nil { - return CreateCaseResponse{}, err + return "", err } resp, err := c.httpClient.Do(r) if err != nil { - return CreateCaseResponse{}, err + return "", err } - defer resp.Body.Close() if resp.StatusCode > http.StatusBadRequest { body, _ := io.ReadAll(resp.Body) - return CreateCaseResponse{}, fmt.Errorf("error POSTing to UID service: (%d) %s", resp.StatusCode, string(body)) + return "", fmt.Errorf("error POSTing to UID service: (%d) %s", resp.StatusCode, string(body)) } var createCaseResponse CreateCaseResponse if err := json.NewDecoder(resp.Body).Decode(&createCaseResponse); err != nil { - return CreateCaseResponse{}, err + return "", err } if len(createCaseResponse.BadRequestErrors) > 0 { - return CreateCaseResponse{}, createCaseResponse.Error() + return "", createCaseResponse.Error() } - return createCaseResponse, nil + return createCaseResponse.UID, nil } func (c *Client) Health(ctx context.Context) (*http.Response, error) { diff --git a/internal/uid/client_test.go b/internal/uid/client_test.go index 4133c5f2e9..4b6516fe12 100644 --- a/internal/uid/client_test.go +++ b/internal/uid/client_test.go @@ -119,7 +119,7 @@ func TestCreateCase(t *testing.T) { now: now, } - resp, err := client.CreateCase(context.Background(), validBody) + uid, err := client.CreateCase(context.Background(), validBody) expectedBody := `{"type":"pfa","source":"APPLICANT","donor":{"name":"Jane Smith","dob":"2000-01-02","postcode":"ABC123"}}` @@ -129,8 +129,7 @@ func TestCreateCase(t *testing.T) { assert.JSONEq(t, expectedBody, requestBody) assert.Nil(t, err) - assert.Nil(t, resp.BadRequestErrors) - assert.Equal(t, "M-789Q-P4DF-4UX3", resp.UID) + assert.Equal(t, "M-789Q-P4DF-4UX3", uid) } func TestCreateCaseOnInvalidBody(t *testing.T) { @@ -293,10 +292,10 @@ func TestCreateCaseOnBadRequestResponse(t *testing.T) { now: now, } - resp, err := client.CreateCase(context.Background(), validBody) + uid, err := client.CreateCase(context.Background(), validBody) assert.Equal(t, errors.New("error POSTing to UID service: (400) /donor/dob must match format YYYY-MM-DD"), err) - assert.Equal(t, "", resp.UID) + assert.Equal(t, "", uid) } func TestCreateCaseNonSuccessResponses(t *testing.T) { @@ -348,10 +347,10 @@ func TestCreateCaseNonSuccessResponses(t *testing.T) { now: now, } - resp, err := client.CreateCase(context.Background(), validBody) + uid, err := client.CreateCase(context.Background(), validBody) assert.Equal(t, tc.expectedError, err) - assert.Equal(t, "", resp.UID) + assert.Equal(t, "", uid) }) } } @@ -572,13 +571,13 @@ func TestPactContract(t *testing.T) { now: now, } - resp, err := client.CreateCase(context.Background(), tc.ActualRequestBody) + uid, err := client.CreateCase(context.Background(), tc.ActualRequestBody) if tc.ResponseStatus == http.StatusCreated { - assert.NotEmpty(t, resp.UID) + assert.NotEmpty(t, uid) assert.NoError(t, err) } else { - assert.Empty(t, resp.UID) + assert.Empty(t, uid) assert.Error(t, err) } diff --git a/scripts/get_event_schemas.sh b/scripts/get_event_schemas.sh index d64dc00702..8157beca86 100644 --- a/scripts/get_event_schemas.sh +++ b/scripts/get_event_schemas.sh @@ -1,5 +1,5 @@ #!/usr/bin/env sh -mkdir -p internal/app/testdata -curl -o internal/app/testdata/application-updated.json "https://raw.githubusercontent.com/ministryofjustice/opg-event-store/main/domains/POAS/events/application-updated/schema.json" -curl -o internal/app/testdata/reduced-fee-requested.json "https://raw.githubusercontent.com/ministryofjustice/opg-event-store/main/domains/POAS/events/reduced-fee-requested/schema.json" +mkdir -p internal/event/testdata +curl -o internal/event/testdata/application-updated.json "https://raw.githubusercontent.com/ministryofjustice/opg-event-store/main/domains/POAS/events/application-updated/schema.json" +curl -o internal/event/testdata/reduced-fee-requested.json "https://raw.githubusercontent.com/ministryofjustice/opg-event-store/main/domains/POAS/events/reduced-fee-requested/schema.json" diff --git a/terraform/environment/region/ecs.tf b/terraform/environment/region/ecs.tf index 397cfb476b..40f669937f 100644 --- a/terraform/environment/region/ecs.tf +++ b/terraform/environment/region/ecs.tf @@ -29,9 +29,9 @@ module "app" { ecs_application_log_group_name = module.application_logs.cloudwatch_log_group.name ecs_capacity_provider = var.ecs_capacity_provider app_env_vars = var.app_env_vars - app_allowed_api_arns = var.app_allowed_api_arns app_service_repository_url = var.app_service_repository_url app_service_container_version = var.app_service_container_version + app_allowed_api_arns = var.uid_service.api_arns ingress_allow_list_cidr = concat(var.ingress_allow_list_cidr, split(",", data.aws_ssm_parameter.additional_allowed_ingress_cidrs.value)) alb_deletion_protection_enabled = var.alb_deletion_protection_enabled lpas_table = var.lpas_table @@ -52,6 +52,7 @@ module "app" { } aws_rum_guest_role_arn = data.aws_iam_role.rum_monitor_unauthenticated.arn rum_monitor_application_id_secretsmanager_secret_arn = aws_secretsmanager_secret.rum_monitor_application_id.id + uid_base_url = var.uid_service.base_url providers = { aws.region = aws.region } diff --git a/terraform/environment/region/event_received.tf b/terraform/environment/region/event_received.tf index 26a3b4f3f6..cd6a44d3da 100644 --- a/terraform/environment/region/event_received.tf +++ b/terraform/environment/region/event_received.tf @@ -11,6 +11,8 @@ module "event_received" { event_bus_name = module.event_bus.event_bus.name app_public_url = aws_route53_record.app.fqdn uploads_bucket = module.uploads_s3_bucket.bucket + uid_base_url = var.uid_service.base_url + allowed_api_arns = var.uid_service.api_arns lpas_table = { arn = var.lpas_table.arn diff --git a/terraform/environment/region/modules/app/ecs.tf b/terraform/environment/region/modules/app/ecs.tf index 84acdaaa3d..ed86187ccb 100644 --- a/terraform/environment/region/modules/app/ecs.tf +++ b/terraform/environment/region/modules/app/ecs.tf @@ -246,12 +246,12 @@ data "aws_iam_policy_document" "task_role_access_policy" { } statement { - sid = "uidApiAccess" + sid = "allowApiAccess" effect = "Allow" actions = [ "execute-api:Invoke", ] - resources = var.app_allowed_api_arns.uid_service + resources = var.app_allowed_api_arns } statement { @@ -387,7 +387,7 @@ locals { }, { name = "UID_BASE_URL", - value = var.app_env_vars.uid_base_url + value = var.uid_base_url }, { name = "ONELOGIN_URL", diff --git a/terraform/environment/region/modules/app/variables.tf b/terraform/environment/region/modules/app/variables.tf index d4e17fea98..b96db6876a 100644 --- a/terraform/environment/region/modules/app/variables.tf +++ b/terraform/environment/region/modules/app/variables.tf @@ -54,6 +54,10 @@ variable "app_service_container_version" { description = "(optional) describe your variable" } +variable "app_allowed_api_arns" { + type = list(string) +} + variable "ingress_allow_list_cidr" { type = list(string) description = "List of CIDR ranges permitted to access the service" @@ -96,11 +100,6 @@ variable "rum_monitor_application_id_secretsmanager_secret_arn" { nullable = true } -variable "app_allowed_api_arns" { - type = map(list(string)) - description = "ARNs of allowed APIs" -} - variable "uploads_s3_bucket" { type = object({ bucket_name = string @@ -116,3 +115,7 @@ variable "event_bus" { }) description = "Name and ARN of the event bus to send events to" } + +variable "uid_base_url" { + type = string +} diff --git a/terraform/environment/region/modules/event_received/lambda.tf b/terraform/environment/region/modules/event_received/lambda.tf index 3d216c4773..00b10bbcfe 100644 --- a/terraform/environment/region/modules/event_received/lambda.tf +++ b/terraform/environment/region/modules/event_received/lambda.tf @@ -7,20 +7,33 @@ module "event_received" { GOVUK_NOTIFY_IS_PRODUCTION = data.aws_default_tags.current.tags.environment-name == "production" ? "1" : "0" APP_PUBLIC_URL = "https://${var.app_public_url}" UPLOADS_S3_BUCKET_NAME = var.uploads_bucket.bucket + UID_BASE_URL = var.uid_base_url } - image_uri = "${var.lambda_function_image_ecr_url}:${var.lambda_function_image_tag}" - ecr_arn = var.lambda_function_image_ecr_arn - environment = data.aws_default_tags.current.tags.environment-name - kms_key = data.aws_kms_alias.cloudwatch_application_logs_encryption.target_key_arn - timeout = 300 - memory = 1024 + image_uri = "${var.lambda_function_image_ecr_url}:${var.lambda_function_image_tag}" + ecr_arn = var.lambda_function_image_ecr_arn + environment = data.aws_default_tags.current.tags.environment-name + kms_key = data.aws_kms_alias.cloudwatch_application_logs_encryption.target_key_arn + iam_policy_documents = [data.aws_iam_policy_document.api_access_policy.json] + timeout = 300 + memory = 1024 providers = { aws.region = aws.region } } -resource "aws_cloudwatch_event_rule" "receive_events" { - name = "${data.aws_default_tags.current.tags.environment-name}-receive-events" +data "aws_iam_policy_document" "api_access_policy" { + statement { + sid = "allowApiAccess" + effect = "Allow" + resources = var.allowed_api_arns + actions = [ + "execute-api:Invoke", + ] + } +} + +resource "aws_cloudwatch_event_rule" "receive_events_sirius" { + name = "${data.aws_default_tags.current.tags.environment-name}-receive-events-sirius" description = "receive events from sirius" event_bus_name = var.event_bus_name @@ -31,21 +44,51 @@ resource "aws_cloudwatch_event_rule" "receive_events" { provider = aws.region } -resource "aws_cloudwatch_event_target" "receive_events" { - target_id = "${data.aws_default_tags.current.tags.environment-name}-receive-events" +resource "aws_cloudwatch_event_target" "receive_events_sirius" { + target_id = "${data.aws_default_tags.current.tags.environment-name}-receive-events-sirius" event_bus_name = var.event_bus_name - rule = aws_cloudwatch_event_rule.receive_events.name + rule = aws_cloudwatch_event_rule.receive_events_sirius.name arn = module.event_received.lambda.arn provider = aws.region } -resource "aws_lambda_permission" "allow_cloudwatch_to_call_event_received" { - statement_id = "AllowExecutionFromCloudWatch" +resource "aws_cloudwatch_event_rule" "receive_events_mlpa" { + name = "${data.aws_default_tags.current.tags.environment-name}-receive-events-mlpa" + description = "receive events from mlpa" + event_bus_name = var.event_bus_name + + event_pattern = jsonencode({ + source = ["opg.poas.makeregister"], + detail-type = ["uid-requested"], + }) + provider = aws.region +} + +resource "aws_cloudwatch_event_target" "receive_events_mlpa" { + target_id = "${data.aws_default_tags.current.tags.environment-name}-receive-events-mlpa" + event_bus_name = var.event_bus_name + rule = aws_cloudwatch_event_rule.receive_events_mlpa.name + arn = module.event_received.lambda.arn + provider = aws.region +} + +resource "aws_lambda_permission" "allow_cloudwatch_to_call_event_received_sirius" { + statement_id = "AllowExecutionFromCloudWatchSirius" + action = "lambda:InvokeFunction" + function_name = module.event_received.lambda.function_name + principal = "events.amazonaws.com" + source_account = data.aws_caller_identity.current.account_id + source_arn = aws_cloudwatch_event_rule.receive_events_sirius.arn + provider = aws.region +} + +resource "aws_lambda_permission" "allow_cloudwatch_to_call_event_received_mlpa" { + statement_id = "AllowExecutionFromCloudWatchMlpa" action = "lambda:InvokeFunction" function_name = module.event_received.lambda.function_name principal = "events.amazonaws.com" source_account = data.aws_caller_identity.current.account_id - source_arn = aws_cloudwatch_event_rule.receive_events.arn + source_arn = aws_cloudwatch_event_rule.receive_events_mlpa.arn provider = aws.region } diff --git a/terraform/environment/region/modules/event_received/variables.tf b/terraform/environment/region/modules/event_received/variables.tf index a5a08db540..789d30ad9e 100644 --- a/terraform/environment/region/modules/event_received/variables.tf +++ b/terraform/environment/region/modules/event_received/variables.tf @@ -28,3 +28,11 @@ variable "app_public_url" { variable "uploads_bucket" { type = any } + +variable "uid_base_url" { + type = string +} + +variable "allowed_api_arns" { + type = list(string) +} diff --git a/terraform/environment/region/modules/lambda/iam.tf b/terraform/environment/region/modules/lambda/iam.tf index 4d51f0e262..658ddd5983 100644 --- a/terraform/environment/region/modules/lambda/iam.tf +++ b/terraform/environment/region/modules/lambda/iam.tf @@ -49,7 +49,8 @@ data "aws_iam_policy_document" "lambda" { ] } - provider = aws.region + override_policy_documents = var.iam_policy_documents + provider = aws.region } resource "aws_iam_role_policy_attachment" "vpc_access_execution_role" { diff --git a/terraform/environment/region/modules/lambda/variables.tf b/terraform/environment/region/modules/lambda/variables.tf index f94efc8159..0fe5cc7203 100644 --- a/terraform/environment/region/modules/lambda/variables.tf +++ b/terraform/environment/region/modules/lambda/variables.tf @@ -59,3 +59,9 @@ variable "ecr_arn" { variable "kms_key" { description = "KMS key for the lambda log group" } + +variable "iam_policy_documents" { + description = "List of IAM policy documents that are merged together. Documents later in the list override earlier ones" + type = list(string) + default = [] +} diff --git a/terraform/environment/region/variables.tf b/terraform/environment/region/variables.tf index c18a2efd58..fae8c195fd 100644 --- a/terraform/environment/region/variables.tf +++ b/terraform/environment/region/variables.tf @@ -72,11 +72,6 @@ variable "dns_weighting" { description = "Weighting for DNS records" } -variable "app_allowed_api_arns" { - type = map(list(string)) - description = "ARNs of allowed APIs" -} - variable "reduced_fees" { type = object({ s3_object_replication_enabled = bool @@ -106,3 +101,10 @@ variable "s3_antivirus_provisioned_concurrency" { error_message = "s3_antivirus_provisioned_concurrency must be between 0 and 6" } } + +variable "uid_service" { + type = object({ + base_url = string + api_arns = list(string) + }) +} diff --git a/terraform/environment/regions.tf b/terraform/environment/regions.tf index 71925b2d5d..261bfaa2b1 100644 --- a/terraform/environment/regions.tf +++ b/terraform/environment/regions.tf @@ -37,11 +37,14 @@ module "eu_west_1" { target_event_bus_arn = local.environment.event_bus.target_event_bus_arn receive_account_ids = local.environment.event_bus.receive_account_ids app_env_vars = local.environment.app.env - app_allowed_api_arns = local.environment.app.allowed_api_arns public_access_enabled = var.public_access_enabled pagerduty_service_name = local.environment.pagerduty_service_name dns_weighting = 100 s3_antivirus_provisioned_concurrency = local.environment.s3_antivirus_provisioned_concurrency + uid_service = { + base_url = local.environment.uid_service.base_url + api_arns = local.environment.uid_service.api_arns + } providers = { aws.region = aws.eu_west_1 aws.global = aws.global @@ -80,11 +83,14 @@ module "eu_west_2" { target_event_bus_arn = local.environment.event_bus.target_event_bus_arn receive_account_ids = local.environment.event_bus.receive_account_ids app_env_vars = local.environment.app.env - app_allowed_api_arns = local.environment.app.allowed_api_arns public_access_enabled = var.public_access_enabled pagerduty_service_name = local.environment.pagerduty_service_name dns_weighting = 0 s3_antivirus_provisioned_concurrency = local.environment.s3_antivirus_provisioned_concurrency + uid_service = { + base_url = local.environment.uid_service.base_url + api_arns = local.environment.uid_service.api_arns + } providers = { aws.region = aws.eu_west_2 aws.global = aws.global diff --git a/terraform/environment/terraform.tfvars.json b/terraform/environment/terraform.tfvars.json index ecb03f8ce3..96b437d4ec 100644 --- a/terraform/environment/terraform.tfvars.json +++ b/terraform/environment/terraform.tfvars.json @@ -12,22 +12,22 @@ "app_public_url": "", "auth_redirect_base_url": "https://demo.app.modernising.opg.service.justice.gov.uk", "notify_is_production": "", - "uid_base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": false, "copy_action_enabled": false @@ -72,22 +72,22 @@ "app_public_url": "", "auth_redirect_base_url": "https://demo.app.modernising.opg.service.justice.gov.uk", "notify_is_production": "", - "uid_base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": false, "copy_action_enabled": false @@ -132,22 +132,22 @@ "app_public_url": "", "auth_redirect_base_url": "https://demo.app.modernising.opg.service.justice.gov.uk", "notify_is_production": "", - "uid_base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": false, "copy_action_enabled": false @@ -192,22 +192,22 @@ "app_public_url": "https://ur.app.modernising.opg.service.justice.gov.uk", "auth_redirect_base_url": "https://ur.app.modernising.opg.service.justice.gov.uk", "notify_is_production": "", - "uid_base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": true, "copy_action_enabled": false @@ -252,22 +252,22 @@ "app_public_url": "https://preproduction.app.modernising.opg.service.justice.gov.uk", "auth_redirect_base_url": "https://preproduction.app.modernising.opg.service.justice.gov.uk", "notify_is_production": "", - "uid_base_url": "https://preproduction.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:492687888235:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:492687888235:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:492687888235:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:492687888235:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://preproduction.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:492687888235:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:492687888235:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:492687888235:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:492687888235:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": false, "copy_action_enabled": false @@ -312,22 +312,22 @@ "app_public_url": "https://app.modernising.opg.service.justice.gov.uk", "auth_redirect_base_url": "https://app.modernising.opg.service.justice.gov.uk", "notify_is_production": "1", - "uid_base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", "onelogin_url": "https://home.integration.account.gov.uk" }, "autoscaling": { "minimum": 1, "maximum": 3 - }, - "allowed_api_arns": { - "uid_service": [ - "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", - "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", - "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" - ] } }, + "uid_service": { + "base_url": "https://development.lpa-uid.api.opg.service.justice.gov.uk", + "api_arns": [ + "arn:aws:execute-api:eu-west-1:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/POST/cases", + "arn:aws:execute-api:eu-west-1:288342028542:*/*/GET/health", + "arn:aws:execute-api:eu-west-2:288342028542:*/*/GET/health" + ] + }, "backups": { "backup_plan_enabled": true, "copy_action_enabled": false diff --git a/terraform/environment/variables.tf b/terraform/environment/variables.tf index 3dc8f0729e..d8c17232f6 100644 --- a/terraform/environment/variables.tf +++ b/terraform/environment/variables.tf @@ -37,16 +37,16 @@ variable "environments" { app_public_url = string auth_redirect_base_url = string notify_is_production = string - uid_base_url = string onelogin_url = string }) autoscaling = object({ minimum = number maximum = number }) - allowed_api_arns = object({ - uid_service = list(string) - }) + }) + uid_service = object({ + base_url = string + api_arns = list(string) }) backups = object({ backup_plan_enabled = bool