diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index 06e85fc7f5..1a57053f4d 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -154,6 +154,7 @@ jobs: search_index: ${{ steps.filter.outputs.search_index == 'true' || env.mustTrigger == 'true' }} serverless: ${{ steps.filter.outputs.serverless == 'true' || env.mustTrigger == 'true' }} stream: ${{ steps.filter.outputs.stream == 'true' || env.mustTrigger == 'true' }} + push_based_log_export: ${{ steps.filter.outputs.push_based_log_export == 'true' || env.mustTrigger == 'true' }} steps: - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 @@ -234,6 +235,8 @@ jobs: stream: - 'internal/service/streamconnection/*.go' - 'internal/service/streaminstance/*.go' + push_based_log_export: + - 'internal/service/pushbasedlogexport/*.go' advanced_cluster: needs: [ change-detection, get-provider-version ] @@ -727,3 +730,27 @@ jobs: ./internal/service/streamconnection ./internal/service/streaminstance run: make testacc + + push_based_log_export: + needs: [ change-detection, get-provider-version ] + if: ${{ needs.change-detection.outputs.push_based_log_export == 'true' || inputs.test_group == 'push_based_log_export' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + with: + ref: ${{ inputs.ref || github.ref }} + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 + with: + go-version-file: 'go.mod' + - uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 + with: + terraform_version: ${{ inputs.terraform_version }} + terraform_wrapper: false + - name: Acceptance Tests + env: + AWS_REGION: ${{ vars.AWS_REGION }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.aws_secret_access_key }} + AWS_ACCESS_KEY_ID: ${{ secrets.aws_access_key_id }} + MONGODB_ATLAS_LAST_VERSION: ${{ needs.get-provider-version.outputs.provider_version }} + ACCTEST_PACKAGES: ./internal/service/pushbasedlogexport + run: make testacc diff --git a/internal/service/pushbasedlogexport/model_test.go b/internal/service/pushbasedlogexport/model_test.go new file mode 100644 index 0000000000..eb57a1df7d --- /dev/null +++ b/internal/service/pushbasedlogexport/model_test.go @@ -0,0 +1,149 @@ +package pushbasedlogexport_test + +import ( + "context" + "reflect" + "testing" + "time" + + "go.mongodb.org/atlas-sdk/v20231115010/admin" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/pushbasedlogexport" +) + +var ( + testBucketName = "test-bucket-name" + testIAMRoleID = "661fe3ad234b02027ddee196" + testPrefixPath = "prefix/path" + prefixPathEmpty = "" + testProjectID = "661fe3ad234b02027dabcabc" +) + +type sdkToTFModelTestCase struct { + apiResp *admin.PushBasedLogExportProject + timeout *timeouts.Value + expectedTFModel *pushbasedlogexport.TFPushBasedLogExportRSModel + name string + projectID string +} + +func TestNewTFPushBasedLogExport(t *testing.T) { + currentTime := time.Now() + + testCases := []sdkToTFModelTestCase{ + { + name: "Complete API response", + projectID: testProjectID, + apiResp: &admin.PushBasedLogExportProject{ + BucketName: admin.PtrString(testBucketName), + CreateDate: admin.PtrTime(currentTime), + IamRoleId: admin.PtrString(testIAMRoleID), + PrefixPath: admin.PtrString(testPrefixPath), + State: admin.PtrString(activeState), + }, + expectedTFModel: &pushbasedlogexport.TFPushBasedLogExportRSModel{ + ProjectID: types.StringValue(testProjectID), + BucketName: types.StringValue(testBucketName), + IamRoleID: types.StringValue(testIAMRoleID), + PrefixPath: types.StringValue(testPrefixPath), + State: types.StringValue(activeState), + CreateDate: types.StringPointerValue(conversion.TimePtrToStringPtr(¤tTime)), + }, + }, + { + name: "Complete API response with empty prefix path", + projectID: testProjectID, + apiResp: &admin.PushBasedLogExportProject{ + BucketName: admin.PtrString(testBucketName), + CreateDate: admin.PtrTime(currentTime), + IamRoleId: admin.PtrString(testIAMRoleID), + PrefixPath: admin.PtrString(prefixPathEmpty), + State: admin.PtrString(activeState), + }, + expectedTFModel: &pushbasedlogexport.TFPushBasedLogExportRSModel{ + ProjectID: types.StringValue(testProjectID), + BucketName: types.StringValue(testBucketName), + IamRoleID: types.StringValue(testIAMRoleID), + PrefixPath: types.StringValue(prefixPathEmpty), + State: types.StringValue(activeState), + CreateDate: types.StringPointerValue(conversion.TimePtrToStringPtr(¤tTime)), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resultModel, _ := pushbasedlogexport.NewTFPushBasedLogExport(context.Background(), tc.projectID, tc.apiResp, tc.timeout) + if !reflect.DeepEqual(resultModel, tc.expectedTFModel) { + t.Errorf("result model does not match expected output: expected %+v, got %+v", tc.expectedTFModel, resultModel) + } + }) + } +} + +type pushBasedLogExportReqTestCase struct { + input *pushbasedlogexport.TFPushBasedLogExportRSModel + expectedCreateReq *admin.CreatePushBasedLogExportProjectRequest + expectedUpdateReq *admin.PushBasedLogExportProject + name string +} + +func TestNewPushBasedLogExportReq(t *testing.T) { + testCases := []pushBasedLogExportReqTestCase{ + { + name: "Valid TF state", + input: &pushbasedlogexport.TFPushBasedLogExportRSModel{ + BucketName: types.StringValue(testBucketName), + IamRoleID: types.StringValue(testIAMRoleID), + PrefixPath: types.StringValue(testPrefixPath), + }, + expectedCreateReq: &admin.CreatePushBasedLogExportProjectRequest{ + BucketName: testBucketName, + IamRoleId: testIAMRoleID, + PrefixPath: testPrefixPath, + }, + expectedUpdateReq: &admin.PushBasedLogExportProject{ + BucketName: admin.PtrString(testBucketName), + IamRoleId: admin.PtrString(testIAMRoleID), + PrefixPath: admin.PtrString(testPrefixPath), + }, + }, + { + name: "Valid TF state", + input: &pushbasedlogexport.TFPushBasedLogExportRSModel{ + BucketName: types.StringValue(testBucketName), + IamRoleID: types.StringValue(testIAMRoleID), + PrefixPath: types.StringValue(prefixPathEmpty), + }, + expectedCreateReq: &admin.CreatePushBasedLogExportProjectRequest{ + BucketName: testBucketName, + IamRoleId: testIAMRoleID, + PrefixPath: prefixPathEmpty, + }, + expectedUpdateReq: &admin.PushBasedLogExportProject{ + BucketName: admin.PtrString(testBucketName), + IamRoleId: admin.PtrString(testIAMRoleID), + PrefixPath: admin.PtrString(prefixPathEmpty), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name+" Create", func(t *testing.T) { + createReq := pushbasedlogexport.NewPushBasedLogExportCreateReq(tc.input) + if !reflect.DeepEqual(createReq, tc.expectedCreateReq) { + t.Errorf("Create request does not match expected output: expected %+v, got %+v", tc.expectedCreateReq, createReq) + } + }) + t.Run(tc.name+" Update", func(t *testing.T) { + updateReq := pushbasedlogexport.NewPushBasedLogExportUpdateReq(tc.input) + if !reflect.DeepEqual(updateReq, tc.expectedUpdateReq) { + t.Errorf("Update request does not match expected output: expected %+v, got %+v", tc.expectedUpdateReq, updateReq) + } + }) + } +} diff --git a/internal/service/pushbasedlogexport/resource_test.go b/internal/service/pushbasedlogexport/resource_test.go index ecacbfa33c..5b0d51cc60 100644 --- a/internal/service/pushbasedlogexport/resource_test.go +++ b/internal/service/pushbasedlogexport/resource_test.go @@ -110,9 +110,7 @@ func addAttrChecks(checks []resource.TestCheckFunc, mapChecks map[string]string) func configBasic(projectID, s3BucketName1, s3BucketName2, s3BucketPolicyName, awsIAMRoleName, awsIAMRolePolicyName, prefixPath string, usePrefixPath bool) string { test := fmt.Sprintf(` locals { - #project_name = %[1]q project_id = %[1]q - #org_id = %[2]q s3_bucket_name_1 = %[2]q s3_bucket_name_2 = %[3]q s3_bucket_policy_name = %[4]q @@ -131,9 +129,7 @@ func configBasic(projectID, s3BucketName1, s3BucketName2, s3BucketPolicyName, aw func configBasicUpdated(projectID, s3BucketName1, s3BucketName2, s3BucketPolicyName, awsIAMRoleName, awsIAMRolePolicyName, prefixPath string, usePrefixPath bool) string { test := fmt.Sprintf(` locals { - #project_name = %[1]q project_id = %[1]q - #org_id = %[2]q s3_bucket_name_1 = %[2]q s3_bucket_name_2 = %[3]q s3_bucket_policy_name = %[4]q diff --git a/internal/service/pushbasedlogexport/state_transition_test.go b/internal/service/pushbasedlogexport/state_transition_test.go new file mode 100644 index 0000000000..9e6058740a --- /dev/null +++ b/internal/service/pushbasedlogexport/state_transition_test.go @@ -0,0 +1,166 @@ +package pushbasedlogexport_test + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "go.mongodb.org/atlas-sdk/v20231115010/admin" + "go.mongodb.org/atlas-sdk/v20231115010/mockadmin" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/retrystrategy" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/service/pushbasedlogexport" +) + +var ( + activeState = "ACTIVE" + unconfiguredState = "UNCONFIGURED" + initiatingState = "INITIATING" + bucketVerifiedState = "BUCKET_VERIFIED" + assumeRoleFailedState = "ASSUME_ROLE_FAILED" + unknown = "" + sc500 = conversion.IntPtr(500) + currentTime = time.Now() +) + +var testTimeoutConfig = retrystrategy.TimeConfig{ + Timeout: 30 * time.Second, + MinTimeout: 100 * time.Millisecond, + Delay: 0, +} + +type testCase struct { + expectedState *string + name string + mockResponses []response + expectedError bool +} + +func TestPushBasedLogExportStateTransition(t *testing.T) { + testCases := []testCase{ + { + name: "Successful transition to ACTIVE", + mockResponses: []response{ + {state: &initiatingState}, + {state: &bucketVerifiedState}, + {state: &activeState}, + }, + expectedState: &activeState, + expectedError: false, + }, + { + name: "Error when transitioning to an unknown state", + mockResponses: []response{ + {state: &initiatingState}, + {state: &assumeRoleFailedState}, + }, + expectedState: nil, + expectedError: true, + }, + { + name: "Error when API responds with error", + mockResponses: []response{ + {statusCode: sc500, err: errors.New("Internal server error")}, + }, + expectedState: nil, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := mockadmin.NewPushBasedLogExportApi(t) + m.EXPECT().GetPushBasedLogConfiguration(mock.Anything, mock.Anything).Return(admin.GetPushBasedLogConfigurationApiRequest{ApiService: m}) + + for _, resp := range tc.mockResponses { + modelResp, httpResp, err := resp.get() + m.EXPECT().GetPushBasedLogConfigurationExecute(mock.Anything).Return(modelResp, httpResp, err).Once() + } + resp, err := pushbasedlogexport.WaitStateTransition(context.Background(), testProjectID, m, testTimeoutConfig) + assert.Equal(t, tc.expectedError, err != nil) + assert.Equal(t, responseWithState(tc.expectedState), resp) + }) + } +} + +func TestPushBasedLogExportStateTransitionForDelete(t *testing.T) { + testCases := []testCase{ + { + name: "Successful transition to UNCONFIGURED from ACTIVE", + mockResponses: []response{ + {state: &activeState}, + {state: &unconfiguredState}, + }, + expectedError: false, + }, + { + name: "Successful transition to UNCONFIGURED", + mockResponses: []response{ + {state: &unconfiguredState}, + }, + expectedError: false, + }, + { + name: "Error when API responds with error", + mockResponses: []response{ + {statusCode: sc500, err: errors.New("Internal server error")}, + }, + expectedError: true, + }, + { + name: "Failed delete when responding with unknown state", + mockResponses: []response{ + {state: &activeState}, + {state: &unknown}, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := mockadmin.NewPushBasedLogExportApi(t) + m.EXPECT().GetPushBasedLogConfiguration(mock.Anything, mock.Anything).Return(admin.GetPushBasedLogConfigurationApiRequest{ApiService: m}) + + for _, resp := range tc.mockResponses { + modelResp, httpResp, err := resp.get() + m.EXPECT().GetPushBasedLogConfigurationExecute(mock.Anything).Return(modelResp, httpResp, err).Once() + } + err := pushbasedlogexport.WaitResourceDelete(context.Background(), testProjectID, m, testTimeoutConfig) + assert.Equal(t, tc.expectedError, err != nil) + }) + } +} + +type response struct { + state *string + statusCode *int + err error +} + +func (r *response) get() (*admin.PushBasedLogExportProject, *http.Response, error) { + var httpResp *http.Response + if r.statusCode != nil { + httpResp = &http.Response{StatusCode: *r.statusCode} + } + return responseWithState(r.state), httpResp, r.err +} + +func responseWithState(state *string) *admin.PushBasedLogExportProject { + if state == nil { + return nil + } + return &admin.PushBasedLogExportProject{ + BucketName: admin.PtrString(testBucketName), + CreateDate: admin.PtrTime(currentTime), + IamRoleId: admin.PtrString(testIAMRoleID), + PrefixPath: admin.PtrString(testPrefixPath), + State: state, + } +}