From 8e17ed241665dfaaa4314e22ad762e3011abdbf7 Mon Sep 17 00:00:00 2001 From: Bobby Iliev Date: Thu, 14 Mar 2024 10:45:15 +0200 Subject: [PATCH] Add acceptance tests for SSO domain resource (#497) * Add acceptance tests for SSO domain resource * Fix failing test * Fix failing test --- pkg/frontegg/sso_config.go | 8 -- pkg/frontegg/sso_domain.go | 133 ++++++++++++++++++ pkg/frontegg/sso_domain_test.go | 81 +++++++++++ pkg/provider/acceptance_sso_domain_test.go | 145 ++++++++++++++++++++ pkg/resources/resource_sso_domain.go | 150 ++------------------- 5 files changed, 373 insertions(+), 144 deletions(-) create mode 100644 pkg/frontegg/sso_domain.go create mode 100644 pkg/frontegg/sso_domain_test.go create mode 100644 pkg/provider/acceptance_sso_domain_test.go diff --git a/pkg/frontegg/sso_config.go b/pkg/frontegg/sso_config.go index 8ae54f91..88f0100d 100644 --- a/pkg/frontegg/sso_config.go +++ b/pkg/frontegg/sso_config.go @@ -33,14 +33,6 @@ type SSOConfig struct { Domains []Domain } -// Domain represents the structure for SSO domain. -type Domain struct { - ID string `json:"id"` - Domain string `json:"domain"` - Validated bool `json:"validated"` - SsoConfigId string `json:"ssoConfigId"` -} - type SSOConfigurationsResponse []SSOConfig // Helper function to flatten the SSO configurations data diff --git a/pkg/frontegg/sso_domain.go b/pkg/frontegg/sso_domain.go new file mode 100644 index 00000000..e07bdac0 --- /dev/null +++ b/pkg/frontegg/sso_domain.go @@ -0,0 +1,133 @@ +package frontegg + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/MaterializeInc/terraform-provider-materialize/pkg/clients" +) + +const ( + SSODomainsApiPathV1 = "/frontegg/team/resources/sso/v1/configurations" +) + +// Domain represents the structure for SSO domain. +type Domain struct { + ID string `json:"id"` + Domain string `json:"domain"` + Validated bool `json:"validated"` + SsoConfigId string `json:"ssoConfigId"` +} + +// FetchSSODomain fetches a specific SSO domain. +func FetchSSODomain(ctx context.Context, client *clients.FronteggClient, configID, domainName string) (*Domain, error) { + endpoint := fmt.Sprintf("%s%s", client.Endpoint, SSODomainsApiPathV1) + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+client.Token) + + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("error reading SSO configurations: status %d, response: %s", resp.StatusCode, string(responseData)) + } + + var configs []SSOConfig + if err := json.NewDecoder(resp.Body).Decode(&configs); err != nil { + return nil, err + } + + for _, config := range configs { + if config.Id == configID { + for _, domain := range config.Domains { + if domain.Domain == domainName { + return &domain, nil + } + } + } + } + + return nil, fmt.Errorf("domain not found") +} + +// CreateDomain creates a new SSO domain. +func CreateSSODomain(ctx context.Context, client *clients.FronteggClient, configID, domainName string) (*Domain, error) { + endpoint := fmt.Sprintf("%s%s/%s/domains", client.Endpoint, SSODomainsApiPathV1, configID) + payload := map[string]string{"domain": domainName} + requestBody, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(requestBody)) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+client.Token) + req.Header.Add("Content-Type", "application/json") + + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return nil, fmt.Errorf("error creating SSO domain: status %d, response: %s", resp.StatusCode, string(responseData)) + } + + var result Domain + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +// DeleteSSODomain deletes a specific SSO domain. +func DeleteSSODomain(ctx context.Context, client *clients.FronteggClient, configID, domainID string) error { + endpoint := fmt.Sprintf("%s%s/%s/domains/%s", client.Endpoint, SSODomainsApiPathV1, configID, domainID) + + req, err := http.NewRequestWithContext(ctx, "DELETE", endpoint, nil) + if err != nil { + return err + } + + req.Header.Add("Authorization", "Bearer "+client.Token) + + resp, err := client.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("error deleting SSO domain: status %d, response: %s", resp.StatusCode, string(responseData)) + } + + return nil +} diff --git a/pkg/frontegg/sso_domain_test.go b/pkg/frontegg/sso_domain_test.go new file mode 100644 index 00000000..c7b22d77 --- /dev/null +++ b/pkg/frontegg/sso_domain_test.go @@ -0,0 +1,81 @@ +package frontegg + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/MaterializeInc/terraform-provider-materialize/pkg/clients" + "github.com/stretchr/testify/assert" +) + +func TestFetchSSODomainSuccess(t *testing.T) { + assert := assert.New(t) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + // Mimic the actual structure with configurations containing domains + w.Write([]byte(`[ + { + "id": "config-id", + "domains": [ + {"id": "domain-id", "domain": "example.com", "validated": true} + ] + } + ]`)) + })) + defer mockServer.Close() + + client := &clients.FronteggClient{ + Endpoint: mockServer.URL, + HTTPClient: mockServer.Client(), + } + + domain, err := FetchSSODomain(context.Background(), client, "config-id", "example.com") + assert.NoError(err) + assert.NotNil(domain) + assert.Equal("domain-id", domain.ID) + assert.Equal("example.com", domain.Domain) + assert.True(domain.Validated) +} + +func TestCreateSSODomainSuccess(t *testing.T) { + assert := assert.New(t) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"id":"new-domain-id","domain":"new-example.com","validated":false,"ssoConfigId":"config-id"}`)) + })) + defer mockServer.Close() + + client := &clients.FronteggClient{ + Endpoint: mockServer.URL, + HTTPClient: mockServer.Client(), + } + + domain, err := CreateSSODomain(context.Background(), client, "config-id", "new-example.com") + assert.NoError(err) + assert.NotNil(domain) + assert.Equal("new-domain-id", domain.ID) + assert.Equal("new-example.com", domain.Domain) + assert.False(domain.Validated) + assert.Equal("config-id", domain.SsoConfigId) +} + +func TestDeleteSSODomainSuccess(t *testing.T) { + assert := assert.New(t) + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer mockServer.Close() + + client := &clients.FronteggClient{ + Endpoint: mockServer.URL, + HTTPClient: mockServer.Client(), + } + + err := DeleteSSODomain(context.Background(), client, "config-id", "domain-id") + assert.NoError(err) +} diff --git a/pkg/provider/acceptance_sso_domain_test.go b/pkg/provider/acceptance_sso_domain_test.go new file mode 100644 index 00000000..b0601c3d --- /dev/null +++ b/pkg/provider/acceptance_sso_domain_test.go @@ -0,0 +1,145 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/MaterializeInc/terraform-provider-materialize/pkg/frontegg" + "github.com/MaterializeInc/terraform-provider-materialize/pkg/utils" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccSSODomain_basic(t *testing.T) { + resourceName := "materialize_sso_domain.example" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSSODomainConfig("example.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSSODomainExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "domain", "example.com"), + resource.TestCheckResourceAttr(resourceName, "validated", "false"), + resource.TestCheckResourceAttrSet(resourceName, "id"), + ), + }, + }, + }) +} + +func TestAccSSODomain_update(t *testing.T) { + resourceName := "materialize_sso_domain.example" + initialDomain := "example.com" + updatedDomain := "updated-example.com" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSSODomainConfig(initialDomain), + Check: resource.ComposeTestCheckFunc( + testAccCheckSSODomainExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "domain", initialDomain), + resource.TestCheckResourceAttr(resourceName, "validated", "false"), + ), + }, + { + Config: testAccSSODomainConfig(updatedDomain), + Check: resource.ComposeTestCheckFunc( + testAccCheckSSODomainExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "domain", updatedDomain), + resource.TestCheckResourceAttr(resourceName, "validated", "false"), + ), + }, + }, + }) +} + +func TestAccSSODomain_disappears(t *testing.T) { + resourceName := "materialize_sso_domain.example" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSSODomainConfig("example.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSSODomainExists(resourceName), + testAccCheckSSODomainDisappears(resourceName), + ), + PlanOnly: true, + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccSSODomainConfig(domain string) string { + return fmt.Sprintf(` +resource "materialize_sso_config" "example" { + enabled = false + sign_request = false + sso_endpoint = "https://example.com/sso" + public_certificate = "test-certificate" + type = "saml" +} +resource "materialize_sso_domain" "example" { + sso_config_id = materialize_sso_config.example.id + domain = "%s" + depends_on = [materialize_sso_config.example] +} +`, domain) +} + +func testAccCheckSSODomainExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No SSO Domain ID is set") + } + + meta := testAccProvider.Meta() + providerMeta, _ := utils.GetProviderMeta(meta) + client := providerMeta.Frontegg + + domain, err := frontegg.FetchSSODomain(context.Background(), client, rs.Primary.Attributes["sso_config_id"], rs.Primary.Attributes["domain"]) + if err != nil { + return err + } + + if domain == nil || domain.ID != rs.Primary.ID { + return fmt.Errorf("SSO Domain not found") + } + + return nil + } +} + +func testAccCheckSSODomainDisappears(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + meta := testAccProvider.Meta() + providerMeta, _ := utils.GetProviderMeta(meta) + client := providerMeta.Frontegg + + err := frontegg.DeleteSSODomain(context.Background(), client, rs.Primary.Attributes["sso_config_id"], rs.Primary.ID) + if err != nil { + return fmt.Errorf("Error deleting SSO domain: %s", err) + } + + return nil + } +} diff --git a/pkg/resources/resource_sso_domain.go b/pkg/resources/resource_sso_domain.go index ce8a1a80..af1e2b81 100644 --- a/pkg/resources/resource_sso_domain.go +++ b/pkg/resources/resource_sso_domain.go @@ -1,16 +1,10 @@ package resources import ( - "bytes" "context" - "encoding/json" "fmt" - "io" - "log" - "net/http" "strings" - "github.com/MaterializeInc/terraform-provider-materialize/pkg/clients" "github.com/MaterializeInc/terraform-provider-materialize/pkg/frontegg" "github.com/MaterializeInc/terraform-provider-materialize/pkg/utils" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -62,12 +56,12 @@ func ssoDomainCreate(ctx context.Context, d *schema.ResourceData, meta interface ssoConfigID := d.Get("sso_config_id").(string) domainName := d.Get("domain").(string) - domainID, err := createDomain(ctx, client, ssoConfigID, domainName) + domain, err := frontegg.CreateSSODomain(ctx, client, ssoConfigID, domainName) if err != nil { return diag.FromErr(err) } - d.SetId(domainID) + d.SetId(domain.ID) return ssoDomainRead(ctx, d, meta) } @@ -78,52 +72,20 @@ func ssoDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{} } client := providerMeta.Frontegg - // Make the request to the SSO configurations endpoint as there is no specific endpoint for domains - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/frontegg/team/resources/sso/v1/configurations", client.Endpoint), nil) - if err != nil { - return diag.FromErr(err) - } - req.Header.Add("Authorization", "Bearer "+client.Token) - - resp, err := client.HTTPClient.Do(req) - if err != nil { - return diag.FromErr(err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - responseData, err := io.ReadAll(resp.Body) - if err != nil { - return diag.FromErr(err) - } - return diag.Errorf("error reading SSO configurations: status %d, response: %s", resp.StatusCode, string(responseData)) - } - - var configs []frontegg.SSOConfig - if err := json.NewDecoder(resp.Body).Decode(&configs); err != nil { - return diag.FromErr(err) - } - - // Extract sso_config_id and domain from the Terraform resource data ssoConfigID := d.Get("sso_config_id").(string) domainName := d.Get("domain").(string) - // Iterate over the configurations to find the specific domain - for _, config := range configs { - if config.Id == ssoConfigID { - for _, domain := range config.Domains { - if domain.Domain == domainName { - // Set the Terraform resource data from the domain - d.SetId(domain.ID) - d.Set("validated", domain.Validated) - return nil - } - } + domain, err := frontegg.FetchSSODomain(ctx, client, ssoConfigID, domainName) + if err != nil { + if err.Error() == "domain not found" { + d.SetId("") + return nil } + return diag.FromErr(err) } - // If domain not found, set the resource ID to empty to indicate it doesn't exist - d.SetId("") + d.SetId(domain.ID) + d.Set("validated", domain.Validated) return nil } @@ -134,25 +96,20 @@ func ssoDomainUpdate(ctx context.Context, d *schema.ResourceData, meta interface } client := providerMeta.Frontegg - // Extract the sso_config_id and domain ssoConfigID := d.Get("sso_config_id").(string) domainName := d.Get("domain").(string) - // Delete the existing domain - err = deleteDomain(ctx, client, ssoConfigID, d.Id()) + err = frontegg.DeleteSSODomain(ctx, client, ssoConfigID, d.Id()) if err != nil { return diag.FromErr(err) } - // Create the new domain with the updated details - newDomainID, err := createDomain(ctx, client, ssoConfigID, domainName) + newDomain, err := frontegg.CreateSSODomain(ctx, client, ssoConfigID, domainName) if err != nil { return diag.FromErr(err) } - // Update the Terraform resource ID to the new domain ID - d.SetId(newDomainID) - + d.SetId(newDomain.ID) return ssoDomainRead(ctx, d, meta) } @@ -164,10 +121,8 @@ func ssoDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface client := providerMeta.Frontegg ssoConfigID := d.Get("sso_config_id").(string) - domainName := d.Get("domain").(string) - log.Printf("[DEBUG] Domain name: %s", domainName) - err = deleteDomain(ctx, client, ssoConfigID, d.Id()) + err = frontegg.DeleteSSODomain(ctx, client, ssoConfigID, d.Id()) if err != nil { return diag.FromErr(err) } @@ -176,83 +131,6 @@ func ssoDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface return nil } -func createDomain(ctx context.Context, client *clients.FronteggClient, configID string, domainName string) (string, error) { - endpoint := fmt.Sprintf("%s/frontegg/team/resources/sso/v1/configurations/%s/domains", client.Endpoint, configID) - payload := map[string]string{"domain": domainName} - - requestBody, err := json.Marshal(payload) - if err != nil { - return "", err - } - - req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(requestBody)) - if err != nil { - return "", err - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Bearer "+client.Token) - - resp, err := client.HTTPClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - responseData, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // Handle the specific case where the domain already exists - if resp.StatusCode == http.StatusConflict { - return "", fmt.Errorf("error creating domain: domain '%s' already exists in another configuration", domainName) - } - - return "", fmt.Errorf("error creating domain: status %d, response: %s", resp.StatusCode, string(responseData)) - } - - var result map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", err - } - - domainID, ok := result["id"].(string) - if !ok { - return "", fmt.Errorf("error retrieving ID from domain creation response") - } - - log.Printf("[DEBUG] Domain create response ID: %s", domainID) - - return domainID, nil -} - -func deleteDomain(ctx context.Context, client *clients.FronteggClient, configID string, domainId string) error { - endpoint := fmt.Sprintf("%s/frontegg/team/resources/sso/v1/configurations/%s/domains/%s", client.Endpoint, configID, domainId) - - req, err := http.NewRequestWithContext(ctx, "DELETE", endpoint, nil) - if err != nil { - return err - } - req.Header.Add("Authorization", "Bearer "+client.Token) - - resp, err := client.HTTPClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - responseData, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return fmt.Errorf("error deleting domain: status %d, response: %s", resp.StatusCode, string(responseData)) - } - - return nil -} - func ssoDomainImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { compositeID := d.Id() parts := strings.Split(compositeID, ":")