diff --git a/.github/workflows/acceptance-tests-runner.yml b/.github/workflows/acceptance-tests-runner.yml index b5c3d295c9..4b1de51ac6 100644 --- a/.github/workflows/acceptance-tests-runner.yml +++ b/.github/workflows/acceptance-tests-runner.yml @@ -150,6 +150,7 @@ jobs: ldap: ${{ steps.filter.outputs.ldap == 'true' || env.mustTrigger == 'true' }} network: ${{ steps.filter.outputs.network == 'true' || env.mustTrigger == 'true' }} project: ${{ steps.filter.outputs.project == 'true' || env.mustTrigger == 'true' }} + push_based_log_export: ${{ steps.filter.outputs.push_based_log_export == 'true' || env.mustTrigger == 'true' }} search_deployment: ${{ steps.filter.outputs.search_deployment == 'true' || env.mustTrigger == 'true' }} search_index: ${{ steps.filter.outputs.search_index == 'true' || env.mustTrigger == 'true' }} serverless: ${{ steps.filter.outputs.serverless == 'true' || env.mustTrigger == 'true' }} @@ -223,6 +224,8 @@ jobs: - 'internal/service/project/*.go' - 'internal/service/projectinvitation/*.go' - 'internal/service/projectipaccesslist/*.go' + push_based_log_export: + - 'internal/service/pushbasedlogexport/*.go' search_deployment: - 'internal/service/searchdeployment/*.go' search_index: @@ -636,6 +639,30 @@ jobs: ./internal/service/projectipaccesslist 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_LOWERCASE }} + 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 + search_deployment: needs: [ change-detection, get-provider-version ] if: ${{ needs.change-detection.outputs.search_deployment == 'true' || inputs.test_group == 'search_deployment' }} diff --git a/internal/service/pushbasedlogexport/model_test.go b/internal/service/pushbasedlogexport/model_test.go new file mode 100644 index 0000000000..b0a3ca9e46 --- /dev/null +++ b/internal/service/pushbasedlogexport/model_test.go @@ -0,0 +1,149 @@ +package pushbasedlogexport_test + +import ( + "context" + "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/stretchr/testify/assert" + + "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 !assert.Equal(t, tc.expectedTFModel, resultModel) { + 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 with empty prefix path", + 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 !assert.Equal(t, tc.expectedCreateReq, createReq) { + 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 !assert.Equal(t, tc.expectedUpdateReq, updateReq) { + 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..2d74b95885 100644 --- a/internal/service/pushbasedlogexport/resource_test.go +++ b/internal/service/pushbasedlogexport/resource_test.go @@ -28,7 +28,7 @@ func basicTestCase(tb testing.TB) *resource.TestCase { var ( projectID = acc.ProjectIDExecution(tb) - s3BucketNamePrefix = fmt.Sprintf("tf-%s", acc.RandomName()) + s3BucketNamePrefix = acc.RandomS3BucketName() s3BucketName1 = fmt.Sprintf("%s-1", s3BucketNamePrefix) s3BucketName2 = fmt.Sprintf("%s-2", s3BucketNamePrefix) s3BucketPolicyName = fmt.Sprintf("%s-s3-policy", s3BucketNamePrefix) @@ -71,7 +71,7 @@ func noPrefixPathTestCase(tb testing.TB) *resource.TestCase { var ( projectID = acc.ProjectIDExecution(tb) - s3BucketNamePrefix = fmt.Sprintf("tf-%s", acc.RandomName()) + s3BucketNamePrefix = acc.RandomS3BucketName() s3BucketName1 = fmt.Sprintf("%s-1", s3BucketNamePrefix) s3BucketName2 = fmt.Sprintf("%s-2", s3BucketNamePrefix) s3BucketPolicyName = fmt.Sprintf("%s-s3-policy", s3BucketNamePrefix) @@ -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 @@ -122,7 +120,7 @@ func configBasic(projectID, s3BucketName1, s3BucketName2, s3BucketPolicyName, aw %[7]s - %[8]s + %[8]s `, projectID, s3BucketName1, s3BucketName2, s3BucketPolicyName, awsIAMRoleName, awsIAMRolePolicyName, awsIAMroleAuthAndS3Config(), pushBasedLogExportConfig(false, usePrefixPath, prefixPath)) return test @@ -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, + } +} diff --git a/internal/testutil/acc/name.go b/internal/testutil/acc/name.go index a56c71c421..d5d284ce86 100644 --- a/internal/testutil/acc/name.go +++ b/internal/testutil/acc/name.go @@ -7,11 +7,12 @@ import ( ) const ( - prefixName = "test-acc-tf" - prefixProject = prefixName + "-p" - prefixCluster = prefixName + "-c" - prefixIAMRole = "mongodb-atlas-" + prefixName - prefixIAMUser = "arn:aws:iam::358363220050:user/mongodb-aws-iam-auth-test-user" + prefixName = "test-acc-tf" + prefixProject = prefixName + "-p" + prefixCluster = prefixName + "-c" + prefixIAMRole = "mongodb-atlas-" + prefixName + prefixIAMUser = "arn:aws:iam::358363220050:user/mongodb-aws-iam-auth-test-user" + prefixS3Bucket = "mongodb-atlas-tf" ) func RandomName() string { @@ -45,3 +46,7 @@ func RandomEmail() string { func RandomLDAPName() string { return fmt.Sprintf("CN=%s-%s@example.com,OU=users,DC=example,DC=com", prefixName, acctest.RandString(10)) } + +func RandomS3BucketName() string { + return fmt.Sprintf("%s-%s", prefixS3Bucket, acctest.RandString(10)) +}