diff --git a/internal/services/organization/resource.go b/internal/services/organization/resource.go index aaf58f1404..e958d5f3e7 100644 --- a/internal/services/organization/resource.go +++ b/internal/services/organization/resource.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "time" "github.com/cloudflare/cloudflare-go/v6" "github.com/cloudflare/cloudflare-go/v6/option" @@ -91,6 +92,7 @@ func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRe data = &env.Result resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + time.Sleep(30 * time.Second) } func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -138,6 +140,7 @@ func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRe data = &env.Result resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { diff --git a/internal/services/organization/resource_test.go b/internal/services/organization/resource_test.go new file mode 100644 index 0000000000..b2b046d533 --- /dev/null +++ b/internal/services/organization/resource_test.go @@ -0,0 +1,161 @@ +package organization_test + +import ( + "context" + "fmt" + "testing" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" + "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// TestMain is the entry point for test execution +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +// TestAccCloudflareOrganization_Basic tests the basic CRUD operations for organization resource +func TestAccCloudflareOrganization_Basic(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + resourceName := "cloudflare_organization." + rnd + orgName := fmt.Sprintf("tf-acctest-%s", rnd) + updatedOrgName := fmt.Sprintf("tf-acctest-%s-updated", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckCloudflareOrganizationDestroy, + Steps: []resource.TestStep{ + // Step 1: Create - Test resource creation with all required attributes + { + Config: testAccOrganizationConfig(rnd, orgName), + Check: resource.ComposeTestCheckFunc( + // Verify required attributes + resource.TestCheckResourceAttr(resourceName, "name", orgName), + // Verify computed attributes are set + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + // Verify meta attributes + resource.TestCheckResourceAttrSet(resourceName, "meta.%"), + ), + }, + // Step 2: Update - Test modifying updatable attributes + { + Config: testAccOrganizationConfig(rnd, updatedOrgName), + Check: resource.ComposeTestCheckFunc( + // Verify the name was updated + resource.TestCheckResourceAttr(resourceName, "name", updatedOrgName), + // Verify ID remains the same (it should be set) + resource.TestCheckResourceAttrSet(resourceName, "id"), + // Verify other attributes remain consistent + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + ), + }, + // Step 3: Import - Test import functionality with proper ID format + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // Organization import uses just the ID, no prefix needed + }, + }, + }) +} + +// TestAccCloudflareOrganization_WithProfile tests organization creation with profile information +func TestAccCloudflareOrganization_WithProfile(t *testing.T) { + rnd := utils.GenerateRandomResourceName() + resourceName := "cloudflare_organization." + rnd + orgName := fmt.Sprintf("tf-acctest-%s", rnd) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckCloudflareOrganizationDestroy, + Steps: []resource.TestStep{ + // Create organization with profile + { + Config: testAccOrganizationConfigWithProfile(rnd, orgName), + ExpectNonEmptyPlan: true, // Allow non-empty plan due to profile field handling + Check: resource.ComposeTestCheckFunc( + // Basic attribute checks + resource.TestCheckResourceAttr(resourceName, "name", orgName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + // Profile checks + resource.TestCheckResourceAttr(resourceName, "profile.business_name", "Test Business"), + resource.TestCheckResourceAttr(resourceName, "profile.business_email", "test@example.com"), + resource.TestCheckResourceAttr(resourceName, "profile.business_phone", "+1234567890"), + resource.TestCheckResourceAttr(resourceName, "profile.business_address", "123 Test St, Test City, TC 12345"), + ), + }, + // Update profile information + { + Config: testAccOrganizationConfigWithProfileUpdated(rnd, orgName), + ExpectNonEmptyPlan: true, // Allow non-empty plan due to profile field handling + Check: resource.ComposeTestCheckFunc( + // Verify profile was updated + resource.TestCheckResourceAttr(resourceName, "profile.business_name", "Updated Business"), + resource.TestCheckResourceAttr(resourceName, "profile.business_email", "updated@example.com"), + ), + }, + // Import test + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + + ImportStateVerifyIgnore: []string{ + "profile", // Profile fields not populated on import + "profile.%", + "profile.business_name", + "profile.business_email", + "profile.business_phone", + "profile.business_address", + "profile.external_metadata", + }, + }, + }, + }) +} + + + +// Test configuration functions that load from testdata files + +func testAccOrganizationConfig(rnd, name string) string { + return acctest.LoadTestCase("basic.tf", rnd, name) +} + +func testAccOrganizationConfigWithProfile(rnd, name string) string { + return acctest.LoadTestCase("with_profile.tf", rnd, name) +} + +func testAccOrganizationConfigWithProfileUpdated(rnd, name string) string { + return acctest.LoadTestCase("with_profile_updated.tf", rnd, name) +} + +func testAccOrganizationConfigWithParent(rnd, name, parentID string) string { + return acctest.LoadTestCase("with_parent.tf", rnd, name, parentID) +} + +// testAccCheckCloudflareOrganizationDestroy verifies the organization has been destroyed +func testAccCheckCloudflareOrganizationDestroy(s *terraform.State) error { + client := acctest.SharedClient() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudflare_organization" { + continue + } + + // Try to fetch the organization + _, err := client.Organizations.Get(context.Background(), rs.Primary.ID) + if err == nil { + return fmt.Errorf("organization %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/internal/services/organization/testdata/basic.tf b/internal/services/organization/testdata/basic.tf new file mode 100644 index 0000000000..1a380c8269 --- /dev/null +++ b/internal/services/organization/testdata/basic.tf @@ -0,0 +1,3 @@ +resource "cloudflare_organization" "%[1]s" { + name = "%[2]s" +} diff --git a/internal/services/organization/testdata/with_parent.tf b/internal/services/organization/testdata/with_parent.tf new file mode 100644 index 0000000000..747ba4faaf --- /dev/null +++ b/internal/services/organization/testdata/with_parent.tf @@ -0,0 +1,7 @@ +resource "cloudflare_organization" "%[1]s" { + name = "%[2]s" + + parent = { + id = "%[3]s" + } +} diff --git a/internal/services/organization/testdata/with_profile.tf b/internal/services/organization/testdata/with_profile.tf new file mode 100644 index 0000000000..2c761e3502 --- /dev/null +++ b/internal/services/organization/testdata/with_profile.tf @@ -0,0 +1,11 @@ +resource "cloudflare_organization" "%[1]s" { + name = "%[2]s" + + profile = { + business_name = "Test Business" + business_email = "test@example.com" + business_phone = "+1234567890" + business_address = "123 Test St, Test City, TC 12345" + external_metadata = "{\"key\":\"value\"}" + } +} diff --git a/internal/services/organization/testdata/with_profile_updated.tf b/internal/services/organization/testdata/with_profile_updated.tf new file mode 100644 index 0000000000..805560c16f --- /dev/null +++ b/internal/services/organization/testdata/with_profile_updated.tf @@ -0,0 +1,11 @@ +resource "cloudflare_organization" "%[1]s" { + name = "%[2]s" + + profile = { + business_name = "Updated Business" + business_email = "updated@example.com" + business_phone = "+9876543210" + business_address = "456 Updated Ave, New City, NC 54321" + external_metadata = "{\"key\":\"updated_value\"}" + } +} diff --git a/internal/services/organization_profile/resource_test.go b/internal/services/organization_profile/resource_test.go new file mode 100644 index 0000000000..18fb6882ab --- /dev/null +++ b/internal/services/organization_profile/resource_test.go @@ -0,0 +1,187 @@ +package organization_profile_test + +import ( + "os" + "testing" + + "github.com/cloudflare/terraform-provider-cloudflare/internal/acctest" + "github.com/cloudflare/terraform-provider-cloudflare/internal/utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +// TestMain is the entry point for test execution +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +// TestAccCloudflareOrganizationProfile_Basic tests the basic CRUD operations for organization_profile resource +func TestAccCloudflareOrganizationProfile_Basic(t *testing.T) { + // Skip if no organization ID is provided + orgID := os.Getenv("CLOUDFLARE_ORGANIZATION_ID") + if orgID == "" { + t.Skip("CLOUDFLARE_ORGANIZATION_ID not set, skipping organization profile test") + } + + rnd := utils.GenerateRandomResourceName() + resourceName := "cloudflare_organization_profile." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + // Note: Organization profiles cannot be destroyed via API, only updated + // No CheckDestroy as the resource doesn't support deletion + Steps: []resource.TestStep{ + // Step 1: Create - Test resource creation with all required attributes + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Test Business", + "test@example.com", + "+1234567890", + `{\"line1\":\"123 Test St\",\"line2\":\"\",\"country\":\"US\",\"zipcode\":\"12345\",\"city\":\"Test City\",\"stateOrProvince\":\"TC\"}`, + `{\"department\":\"IT\"}`), + ConfigStateChecks: []statecheck.StateCheck{ + // Verify required attributes + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.StringExact(orgID)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_name"), knownvalue.StringExact("Test Business")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_email"), knownvalue.StringExact("test@example.com")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_phone"), knownvalue.StringExact("+1234567890")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_address"), knownvalue.StringExact(`{"line1":"123 Test St","line2":"","country":"US","zipcode":"12345","city":"Test City","stateOrProvince":"TC"}`)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metadata"), knownvalue.StringExact(`{"department":"IT"}`)), + }, + }, + // Step 2: Read - Verify all attributes are correctly set (implicit in state checks) + + // Step 3: Update - Test modifying updatable attributes + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Updated Business Name", + "updated@example.com", + "+9876543210", + `{\"line1\":\"456 Updated Ave\",\"line2\":\"Suite 200\",\"country\":\"US\",\"zipcode\":\"54321\",\"city\":\"New City\",\"stateOrProvince\":\"NC\"}`, + `{\"department\":\"Engineering\",\"team\":\"Platform\"}`), + ConfigStateChecks: []statecheck.StateCheck{ + // Verify the attributes were updated + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.StringExact(orgID)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_name"), knownvalue.StringExact("Updated Business Name")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_email"), knownvalue.StringExact("updated@example.com")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_phone"), knownvalue.StringExact("+9876543210")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_address"), knownvalue.StringExact(`{"line1":"456 Updated Ave","line2":"Suite 200","country":"US","zipcode":"54321","city":"New City","stateOrProvince":"NC"}`)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metadata"), knownvalue.StringExact(`{"department":"Engineering","team":"Platform"}`)), + }, + }, + // Note: No import test as this resource doesn't support import functionality + }, + }) +} + + +// TestAccCloudflareOrganizationProfile_MinimalMetadata tests with minimal external metadata +func TestAccCloudflareOrganizationProfile_MinimalMetadata(t *testing.T) { + // Skip if no organization ID is provided + orgID := os.Getenv("CLOUDFLARE_ORGANIZATION_ID") + if orgID == "" { + t.Skip("CLOUDFLARE_ORGANIZATION_ID not set, skipping organization profile test") + } + + rnd := utils.GenerateRandomResourceName() + resourceName := "cloudflare_organization_profile." + rnd + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create with minimal metadata + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Minimal Business", + "minimal@example.com", + "+1111111111", + `{\"line1\":\"111 Minimal St\",\"line2\":\"\",\"country\":\"US\",\"zipcode\":\"11111\",\"city\":\"Minimal City\",\"stateOrProvince\":\"MC\"}`, + "{}"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("organization_id"), knownvalue.StringExact(orgID)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_name"), knownvalue.StringExact("Minimal Business")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_address"), knownvalue.StringExact(`{"line1":"111 Minimal St","line2":"","country":"US","zipcode":"11111","city":"Minimal City","stateOrProvince":"MC"}`)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metadata"), knownvalue.StringExact("{}")), + }, + }, + // Update to add more metadata + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Minimal Business", + "minimal@example.com", + "+1111111111", + `{\"line1\":\"111 Minimal St\",\"line2\":\"\",\"country\":\"US\",\"zipcode\":\"11111\",\"city\":\"Minimal City\",\"stateOrProvince\":\"MC\"}`, + `{\"status\":\"active\"}`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("external_metadata"), knownvalue.StringExact(`{"status":"active"}`)), + }, + }, + }, + }) +} + +// TestAccCloudflareOrganizationProfile_UpdateSingleField tests updating individual fields +func TestAccCloudflareOrganizationProfile_UpdateSingleField(t *testing.T) { + // Skip if no organization ID is provided + orgID := os.Getenv("CLOUDFLARE_ORGANIZATION_ID") + if orgID == "" { + t.Skip("CLOUDFLARE_ORGANIZATION_ID not set, skipping organization profile test") + } + + rnd := utils.GenerateRandomResourceName() + resourceName := "cloudflare_organization_profile." + rnd + + initialEmail := "initial@example.com" + updatedEmail := "updated@example.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Initial configuration + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Test Company", + initialEmail, + "+1234567890", + `{\"line1\":\"123 Main St\",\"line2\":\"\",\"country\":\"US\",\"zipcode\":\"10001\",\"city\":\"Test City\",\"stateOrProvince\":\"NY\"}`, + "{}"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_email"), knownvalue.StringExact(initialEmail)), + }, + }, + // Update only the email + { + Config: testAccOrganizationProfileConfig(rnd, orgID, + "Test Company", + updatedEmail, + "+1234567890", + `{\"line1\":\"123 Main St\",\"line2\":\"\",\"country\":\"US\",\"zipcode\":\"10001\",\"city\":\"Test City\",\"stateOrProvince\":\"NY\"}`, + "{}"), + ConfigStateChecks: []statecheck.StateCheck{ + // Verify only email changed + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_email"), knownvalue.StringExact(updatedEmail)), + // Verify other fields remain unchanged + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_name"), knownvalue.StringExact("Test Company")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_phone"), knownvalue.StringExact("+1234567890")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("business_address"), knownvalue.StringExact(`{"line1":"123 Main St","line2":"","country":"US","zipcode":"10001","city":"Test City","stateOrProvince":"NY"}`)), + }, + }, + }, + }) +} + + + +// Test configuration functions that load from testdata files + +func testAccOrganizationProfileConfig(rnd, orgID, businessName, businessEmail, businessPhone, businessAddress, externalMetadata string) string { + return acctest.LoadTestCase("basic.tf", rnd, orgID, businessName, businessEmail, businessPhone, businessAddress, externalMetadata) +} + +// Note: No CheckDestroy function as organization profiles cannot be deleted via API +// The Delete operation in the resource is a no-op that only removes from Terraform state diff --git a/internal/services/organization_profile/testdata/basic.tf b/internal/services/organization_profile/testdata/basic.tf new file mode 100644 index 0000000000..4d27ac1df1 --- /dev/null +++ b/internal/services/organization_profile/testdata/basic.tf @@ -0,0 +1,8 @@ +resource "cloudflare_organization_profile" "%[1]s" { + organization_id = "%[2]s" + business_name = "%[3]s" + business_email = "%[4]s" + business_phone = "%[5]s" + business_address = "%[6]s" + external_metadata = "%[7]s" +}