diff --git a/docs/resources/credential_username_password_domain.md b/docs/resources/credential_username_password_domain.md new file mode 100644 index 00000000..8a44d025 --- /dev/null +++ b/docs/resources/credential_username_password_domain.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "boundary_credential_username_password_domain Resource - terraform-provider-boundary" +subcategory: "" +description: |- + The username-password-domain credential resource allows you to configure a credential using a username, password and domain. +--- + +# boundary_credential_username_password_domain (Resource) + +The username-password-domain credential resource allows you to configure a credential using a username, password and domain. + +## Example Usage + +```terraform +resource "boundary_scope" "org" { + name = "organization_one" + description = "global scope" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_static" "example" { + name = "example_static_credential_store" + description = "My first static credential store!" + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_username_password_domain" "example" { + name = "example_username_password_domain" + description = "My first username password domain credential!" + credential_store_id = boundary_credential_store_static.example.id + username = "my-username" + password = "my-password" + domain = "my-domain" +} +``` + + +## Schema + +### Required + +- `credential_store_id` (String) The credential store in which to save this username-password-domain credential. +- `password` (String, Sensitive) The password of this username-password-domain credential. + +### Optional + +- `description` (String) The description of this username-password-domain credential. +- `domain` (String) The domain of this username-password-domain credential. This field is required unless provided as part of the username field instead (see username field description). +- `name` (String) The name of this username-password-domain credential. Defaults to the resource name. +- `username` (String) This field is required even though it is marked as optional. The username of this username-password-domain credential. Can also contain a domain if provided as username@domain or domain\username + +### Read-Only + +- `id` (String) The ID of this username-password-domain credential. +- `password_hmac` (String) The password hmac. + +## Import + +Import is supported using the following syntax: + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +```shell +terraform import boundary_credential_username_password_domain.example_username_password_domain +``` diff --git a/examples/resources/boundary_credential_username_password_domain/import.sh b/examples/resources/boundary_credential_username_password_domain/import.sh new file mode 100644 index 00000000..c66d39d2 --- /dev/null +++ b/examples/resources/boundary_credential_username_password_domain/import.sh @@ -0,0 +1 @@ +terraform import boundary_credential_username_password_domain.example_username_password_domain diff --git a/examples/resources/boundary_credential_username_password_domain/resource.tf b/examples/resources/boundary_credential_username_password_domain/resource.tf new file mode 100644 index 00000000..bd721c9b --- /dev/null +++ b/examples/resources/boundary_credential_username_password_domain/resource.tf @@ -0,0 +1,29 @@ +resource "boundary_scope" "org" { + name = "organization_one" + description = "global scope" + scope_id = "global" + auto_create_admin_role = true + auto_create_default_role = true +} + +resource "boundary_scope" "project" { + name = "project_one" + description = "My first scope!" + scope_id = boundary_scope.org.id + auto_create_admin_role = true +} + +resource "boundary_credential_store_static" "example" { + name = "example_static_credential_store" + description = "My first static credential store!" + scope_id = boundary_scope.project.id +} + +resource "boundary_credential_username_password_domain" "example" { + name = "example_username_password_domain" + description = "My first username password domain credential!" + credential_store_id = boundary_credential_store_static.example.id + username = "my-username" + password = "my-password" + domain = "my-domain" +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index a36a0b74..6c724d29 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -108,6 +108,7 @@ func New() *schema.Provider { "boundary_credential_store_vault": resourceCredentialStoreVault(), "boundary_credential_store_static": resourceCredentialStoreStatic(), "boundary_credential_username_password": resourceCredentialUsernamePassword(), + "boundary_credential_username_password_domain": resourceCredentialUsernamePasswordDomain(), "boundary_credential_ssh_private_key": resourceCredentialSshPrivateKey(), "boundary_credential_json": resourceCredentialJson(), "boundary_managed_group": resourceManagedGroup(), diff --git a/internal/provider/resource_credential_json_test.go b/internal/provider/resource_credential_json_test.go index 5f243095..2b060a17 100644 --- a/internal/provider/resource_credential_json_test.go +++ b/internal/provider/resource_credential_json_test.go @@ -140,9 +140,10 @@ func testAccCheckCredentialResourceExists(testProvider *schema.Provider, name st type credentialType string const ( - jsonCredentialType credentialType = "boundary_credential_json" - sshPrivateKeyCredentialType credentialType = "boundary_credential_ssh_private_key" - usernamePasswordCredentialType credentialType = "boundary_credential_username_password" + jsonCredentialType credentialType = "boundary_credential_json" + sshPrivateKeyCredentialType credentialType = "boundary_credential_ssh_private_key" + usernamePasswordCredentialType credentialType = "boundary_credential_username_password" + usernamePasswordDomainCredentialType credentialType = "boundary_credential_username_password_domain" ) func testAccCheckCredentialResourceDestroy(t *testing.T, testProvider *schema.Provider, typ credentialType) resource.TestCheckFunc { diff --git a/internal/provider/resource_credential_username_password_domain.go b/internal/provider/resource_credential_username_password_domain.go new file mode 100644 index 00000000..5a7dacbc --- /dev/null +++ b/internal/provider/resource_credential_username_password_domain.go @@ -0,0 +1,351 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + credentialUsernamePasswordDomainUsernameKey = "username" + credentialUsernamePasswordDomainPasswordKey = "password" + credentialUsernamePasswordDomainPasswordHmacKey = "password_hmac" + credentialUsernamePasswordDomainDomainKey = "domain" + credentialUsernamePasswordDomainCredentialType = "username_password_domain" +) + +func resourceCredentialUsernamePasswordDomain() *schema.Resource { + return &schema.Resource{ + Description: "The username-password-domain credential resource allows you to configure a credential using a username, password and domain.", + + CreateContext: resourceCredentialUsernamePasswordDomainCreate, + ReadContext: resourceCredentialUsernamePasswordDomainRead, + UpdateContext: resourceCredentialUsernamePasswordDomainUpdate, + DeleteContext: resourceCredentialUsernamePasswordDomainDelete, + CustomizeDiff: resourceCredentialUsernamePasswordDomainCustomizeDiff, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + IDKey: { + Description: "The ID of this username-password-domain credential.", + Type: schema.TypeString, + Computed: true, + }, + NameKey: { + Description: "The name of this username-password-domain credential. Defaults to the resource name.", + Type: schema.TypeString, + Optional: true, + }, + DescriptionKey: { + Description: "The description of this username-password-domain credential.", + Type: schema.TypeString, + Optional: true, + }, + credentialStoreIdKey: { + Description: "The credential store in which to save this username-password-domain credential.", + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + credentialUsernamePasswordDomainUsernameKey: { + Description: "This field is required even though it is marked as optional. The username of this username-password-domain credential. Can also contain a domain if provided as username@domain or domain\\username", + Type: schema.TypeString, + Optional: true, // Explicitly set as optional so that Terraform doesn't interpret this field as read-only. + Computed: true, // Required and Computed are mutually exclusive. + }, + credentialUsernamePasswordDomainPasswordKey: { + Description: "The password of this username-password-domain credential.", + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + credentialUsernamePasswordDomainPasswordHmacKey: { + Description: "The password hmac.", + Type: schema.TypeString, + Computed: true, + }, + credentialUsernamePasswordDomainDomainKey: { + Description: "The domain of this username-password-domain credential. This field is required unless provided as part of the username field instead (see username field description).", + Type: schema.TypeString, + Optional: true, // Explicitly set as optional so that Terraform doesn't interpret this field as read-only. + Computed: true, // Required and Computed are mutually exclusive. + }, + }, + } +} + +func setFromCredentialUsernamePasswordDomainResponseMap(d *schema.ResourceData, raw map[string]interface{}, fromRead bool) error { + if err := d.Set(NameKey, raw[NameKey]); err != nil { + return err + } + if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil { + return err + } + if err := d.Set(credentialStoreIdKey, raw[credentialStoreIdKey]); err != nil { + return err + } + + if attrsVal, ok := raw["attributes"]; ok { + attrs := attrsVal.(map[string]interface{}) + if err := d.Set(credentialUsernamePasswordDomainUsernameKey, attrs[credentialUsernamePasswordDomainUsernameKey]); err != nil { + return err + } + + statePasswordHmac := d.Get(credentialUsernamePasswordDomainPasswordHmacKey) + boundaryPasswordHmac := attrs[credentialUsernamePasswordDomainPasswordHmacKey].(string) + if statePasswordHmac.(string) != boundaryPasswordHmac && fromRead { + // PasswordHmac has changed in Boundary, therefore the password has changed. + // Update password value to force tf to attempt update. + if err := d.Set(credentialUsernamePasswordDomainPasswordKey, "(changed in Boundary)"); err != nil { + return err + } + } + if err := d.Set(credentialUsernamePasswordDomainPasswordHmacKey, boundaryPasswordHmac); err != nil { + return err + } + + if err := d.Set(credentialUsernamePasswordDomainDomainKey, attrs[credentialUsernamePasswordDomainDomainKey]); err != nil { + return err + } + } + + d.SetId(raw["id"].(string)) + + return nil +} + +func resourceCredentialUsernamePasswordDomainCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + + var opts []credentials.Option + if v, ok := d.GetOk(NameKey); ok { + opts = append(opts, credentials.WithName(v.(string))) + } + if v, ok := d.GetOk(DescriptionKey); ok { + opts = append(opts, credentials.WithDescription(v.(string))) + } + + // Username and Domain can be set separately, but can also be set together + // in the form of "username@domain" or "domain\\username". + // We need to parse them out in order to set them correctly. + usernameKey, domainKey := "", "" + if v, ok := d.GetOk(credentialUsernamePasswordDomainUsernameKey); ok { + usernameKey = v.(string) + } + if v, ok := d.GetOk(credentialUsernamePasswordDomainDomainKey); ok { + domainKey = v.(string) + } + + username, domain, err := credentials.ParseUsernameDomain(usernameKey, domainKey) + if err != nil { + return diag.Errorf("error parsing username and domain: %v", err) + } + + switch username { + case "": + default: + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialUsername(username)) + } + switch domain { + case "": + default: + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialDomain(domain)) + } + + if v, ok := d.GetOk(credentialUsernamePasswordDomainPasswordKey); ok { + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialPassword(v.(string))) + } + + var credentialStoreId string + retrievedStoreId, ok := d.GetOk(credentialStoreIdKey) + if ok { + credentialStoreId = retrievedStoreId.(string) + } else { + return diag.Errorf("credential store id is unset") + } + + client := credentials.NewClient(md.client) + cr, err := client.Create(ctx, credentialUsernamePasswordDomainCredentialType, credentialStoreId, opts...) + if err != nil { + return diag.Errorf("error creating credential: %v", err) + } + if cr == nil { + return diag.Errorf("nil credential after create") + } + + if err := setFromCredentialUsernamePasswordDomainResponseMap(d, cr.GetResponse().Map, false); err != nil { + return diag.Errorf("error generating credential from response map: %v", err) + } + + return nil +} + +func resourceCredentialUsernamePasswordDomainRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentials.NewClient(md.client) + + cr, err := client.Read(ctx, d.Id()) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound { + d.SetId("") + return nil + } + return diag.Errorf("error reading credential: %v", err) + } + if cr == nil { + return diag.Errorf("credential nil after read") + } + + if err := setFromCredentialUsernamePasswordDomainResponseMap(d, cr.GetResponse().Map, true); err != nil { + return diag.Errorf("error generating credential from response map: %v", err) + } + + return nil +} + +func resourceCredentialUsernamePasswordDomainUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentials.NewClient(md.client) + + var opts []credentials.Option + if d.HasChange(NameKey) { + opts = append(opts, credentials.DefaultName()) + nameVal, ok := d.GetOk(NameKey) + if ok { + opts = append(opts, credentials.WithName(nameVal.(string))) + } + } + + if d.HasChange(DescriptionKey) { + opts = append(opts, credentials.DefaultDescription()) + descVal, ok := d.GetOk(DescriptionKey) + if ok { + opts = append(opts, credentials.WithDescription(descVal.(string))) + } + } + + // Username and Domain can be set separately, but can also be set together + // in the form of "username@domain" or "domain\\username". + // Since we do not know if they were originally set together or separately, + // we need to check if either of them has changed. + if d.HasChange(credentialUsernamePasswordDomainUsernameKey) || d.HasChange(credentialUsernamePasswordDomainDomainKey) { + usernameKey, domainKey := "", "" + if v, ok := d.GetOk(credentialUsernamePasswordDomainUsernameKey); ok { + usernameKey = v.(string) + } + if v, ok := d.GetOk(credentialUsernamePasswordDomainDomainKey); ok { + domainKey = v.(string) + } + + username, domain, err := credentials.ParseUsernameDomain(usernameKey, domainKey) + if err != nil { + return diag.Errorf("error parsing username and domain: %v", err) + } + + switch username { + case "": + default: + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialUsername(username)) + } + switch domain { + case "": + default: + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialDomain(domain)) + } + } + + if d.HasChange(credentialUsernamePasswordDomainPasswordKey) { + passwordVal, ok := d.GetOk(credentialUsernamePasswordDomainPasswordKey) + if ok { + opts = append(opts, credentials.WithUsernamePasswordDomainCredentialPassword(passwordVal.(string))) + } + } + + if len(opts) > 0 { + opts = append(opts, credentials.WithAutomaticVersioning(true)) + crUpdate, err := client.Update(ctx, d.Id(), 0, opts...) + if err != nil { + return diag.Errorf("error updating credential: %v", err) + } + if crUpdate == nil { + return diag.Errorf("credential nil after update") + } + + if err = setFromCredentialUsernamePasswordDomainResponseMap(d, crUpdate.GetResponse().Map, false); err != nil { + return diag.Errorf("error generating credential from response map: %v", err) + } + } + + return nil +} + +func resourceCredentialUsernamePasswordDomainDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + md := meta.(*metaData) + client := credentials.NewClient(md.client) + + _, err := client.Delete(ctx, d.Id()) + if err != nil { + return diag.Errorf("error deleting credential: %v", err) + } + + return nil +} + +func resourceCredentialUsernamePasswordDomainCustomizeDiff(ctx context.Context, rd *schema.ResourceDiff, _ interface{}) error { + usernameRaw := rd.Get(credentialUsernamePasswordDomainUsernameKey).(string) + domainRaw := rd.Get(credentialUsernamePasswordDomainDomainKey).(string) + + if usernameRaw != "" && domainRaw != "" { + if strings.Contains(usernameRaw, "@") || strings.Contains(usernameRaw, "\\") { + // This is an error condition for ParseUsernameDomain, so handle it + // here by blanking the domain input. We've already asserted that + // the username appears to contain a domain, so we'll extract it + // below. + domainRaw = "" + } + } + + username, domain, err := credentials.ParseUsernameDomain(usernameRaw, domainRaw) + if err != nil { + return fmt.Errorf("error parsing username and domain: %w", err) + } + // We can't set the fields as Required in the config (because we + // have to set Computed), so enforce it here. + if username == "" { + return fmt.Errorf("username field is required") + } + if domain == "" { + return fmt.Errorf("domain field is required") + } + + err = rd.Clear(credentialUsernamePasswordDomainUsernameKey) + if err != nil { + return fmt.Errorf("failed to clear username field: %w", err) + } + err = rd.SetNew(credentialUsernamePasswordDomainUsernameKey, username) + if err != nil { + return fmt.Errorf("failed to set new username: %w", err) + } + + err = rd.Clear(credentialUsernamePasswordDomainDomainKey) + if err != nil { + return fmt.Errorf("failed to clear domain field: %w", err) + } + err = rd.SetNew(credentialUsernamePasswordDomainDomainKey, domain) + if err != nil { + return fmt.Errorf("failed to set new domain: %w", err) + } + + return nil +} diff --git a/internal/provider/resource_credential_username_password_domain_test.go b/internal/provider/resource_credential_username_password_domain_test.go new file mode 100644 index 00000000..71d8e375 --- /dev/null +++ b/internal/provider/resource_credential_username_password_domain_test.go @@ -0,0 +1,640 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/boundary/testing/controller" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +const ( + usernamePasswordDomainCredResc = "boundary_credential_username_password_domain.example" + usernamePasswordDomainCredName = "foo" + usernamePasswordDomainCredDesc = "the foo" + usernamePasswordDomainCredUsername = "default_username" + usernamePasswordDomainCredUsernameAt = "default_username@default_domain" + usernamePasswordDomainCredUsernameSlash = `default_domain\\default_username` + usernamePasswordDomainCredPassword = "default_password" + usernamePasswordDomainCredDomain = "default_domain" + usernamePasswordDomainCredUpdate = "_random" +) + +func usernamePasswordDomainCredResourceWithoutDomain(name, description, username, password string) string { + return fmt.Sprintf(` +resource "boundary_credential_store_static" "example" { + name = "static store name" + description = "static store description" + scope_id = boundary_scope.proj1.id + depends_on = [boundary_role.proj1_admin] +} + +resource "boundary_credential_username_password_domain" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_static.example.id + username = "%s" + password = "%s" +}`, name, + description, + username, + password) +} + +func usernamePasswordDomainCredResourceWithDomain(name, description, username, password, domain string) string { + return fmt.Sprintf(` +resource "boundary_credential_store_static" "example" { + name = "static store name" + description = "static store description" + scope_id = boundary_scope.proj1.id + depends_on = [boundary_role.proj1_admin] +} + +resource "boundary_credential_username_password_domain" "example" { + name = "%s" + description = "%s" + credential_store_id = boundary_credential_store_static.example.id + username = "%s" + password = "%s" + domain = "%s" +}`, name, + description, + username, + password, + domain) +} + +func TestAccCredentialUsernamePasswordDomain(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + res := usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsername, + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ) + + resUpdate := usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate, + usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate, + usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate, + ) + + resUpdateAt := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate, + ) + + resUpdateSlash := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate, + ) + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, res), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // run a plan only update and verify no changes + PlanOnly: true, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update again but apply a preConfig to externally update resource + PreConfig: func() { usernamePasswordDomainCredExternalUpdate(t, provider) }, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + }) + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, res), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update from 3 distinct fields to username@domain. + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdateAt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + }) + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, res), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update from 3 distinct fields to domain\username. + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdateSlash), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + }) +} + +func TestAccCredentialUsernamePasswordDomain_DomainInUsernameFieldUsingAt(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + res := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsernameAt, + usernamePasswordDomainCredPassword, + ) + + resUpdate := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate, + ) + + resUpdateSeparateFields := usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordCredUsername+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + usernamePasswordDomainCredPassword+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + usernamePasswordDomainCredDomain+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + ) + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, res), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // Run a plan only update and verify no changes + PlanOnly: true, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update again but apply a preConfig to externally update resource + PreConfig: func() { usernamePasswordDomainCredExternalUpdate(t, provider) }, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update from username@domain to use all 3 fields separately. + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdateSeparateFields), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + }) +} + +func TestAccCredentialUsernamePasswordDomain_DomainInUsernameFieldUsingSlash(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + res := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsernameSlash, + usernamePasswordDomainCredPassword, + ) + + resUpdate := usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate, + ) + + resUpdateSeparateFields := usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordCredUsername+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + usernamePasswordDomainCredPassword+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + usernamePasswordDomainCredDomain+usernamePasswordCredUpdate+usernamePasswordCredUpdate, + ) + + var provider *schema.Provider + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // create + Config: testConfig(url, fooOrg, firstProjectFoo, res), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // Run a plan only update and verify no changes + PlanOnly: true, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update again but apply a preConfig to externally update resource + PreConfig: func() { usernamePasswordDomainCredExternalUpdate(t, provider) }, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // update from domain\username to use all 3 fields separately. + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdateSeparateFields), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain+usernamePasswordDomainCredUpdate+usernamePasswordDomainCredUpdate), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + }) +} + +// TestResourceCredentialUsernamePasswordDomainCustomizeDiff tests the diff +// customization function by testing the diff equivalency of different +// combinations of inputs. +func TestResourceCredentialUsernamePasswordDomainCustomizeDiff(t *testing.T) { + tc := controller.NewTestController(t, tcConfig...) + defer tc.Shutdown() + url := tc.ApiAddrs()[0] + + tests := []struct { + name string + createResource string + planOnlyUpdates []string + }{ + { + name: "usernamePasswordDomainFormatToOtherFormats", + createResource: usernamePasswordDomainCredResourceWithDomain( // Start w/ username, password, domain. + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsername, + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + planOnlyUpdates: []string{ + // Plan an update to username@domain (with domain field still filled). + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + // Plan an update to username@domain and empty out domain. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + "", + ), + // Plan an update to username@domain and nil out domain. + usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + ), + // Plan an update to domain\username (with domain field still filled). + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain, usernamePasswordDomainCredUsername), + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + // Plan an update to domain\username and empty out domain. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain, usernamePasswordDomainCredUsername), + usernamePasswordDomainCredPassword, + "", + ), + // Plan an update to domain\username and nil out domain. + usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain, usernamePasswordDomainCredUsername), + usernamePasswordDomainCredPassword, + ), + }, + }, + { + name: "usernameAtpasswordFormatToOtherFormats", + createResource: usernamePasswordDomainCredResourceWithoutDomain( // Start w/ username@domain and password. + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + ), + planOnlyUpdates: []string{ + // Plan an update to domain\username. + usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain, usernamePasswordDomainCredUsername), + usernamePasswordDomainCredPassword, + ), + // Plan an update to add the same domain in the domain field. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + // Plan an update to add the same username in the username field. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsername, + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + }, + }, + { + name: "domainSlashUsernameFormatToOtherFormats", + createResource: usernamePasswordDomainCredResourceWithoutDomain( // Start w/ domain\username and password. + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf(`%s\\%s`, usernamePasswordDomainCredDomain, usernamePasswordDomainCredUsername), + usernamePasswordDomainCredPassword, + ), + planOnlyUpdates: []string{ + // Plan an update to username@domain. + usernamePasswordDomainCredResourceWithoutDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + ), + // Plan an update to add the same domain in the domain field. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + fmt.Sprintf("%s@%s", usernamePasswordDomainCredUsername, usernamePasswordDomainCredDomain), + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + // Plan an update to add the same username in the username field. + usernamePasswordDomainCredResourceWithDomain( + usernamePasswordDomainCredName, + usernamePasswordDomainCredDesc, + usernamePasswordDomainCredUsername, + usernamePasswordDomainCredPassword, + usernamePasswordDomainCredDomain, + ), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var provider *schema.Provider + tc := resource.TestCase{ + ProviderFactories: providerFactories(&provider), + CheckDestroy: testAccCheckCredentialResourceDestroy(t, provider, usernamePasswordDomainCredentialType), + Steps: []resource.TestStep{ + { + // Create resource. + Config: testConfig(url, fooOrg, firstProjectFoo, tt.createResource), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, NameKey, usernamePasswordDomainCredName), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, DescriptionKey, usernamePasswordDomainCredDesc), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainUsernameKey, usernamePasswordDomainCredUsername), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey, usernamePasswordDomainCredPassword), + resource.TestCheckResourceAttr(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainDomainKey, usernamePasswordDomainCredDomain), + + testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac(), + testAccCheckCredentialResourceExists(provider, usernamePasswordDomainCredResc), + ), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + { + // Run a plan-only update on itself and verify no changes. + PlanOnly: true, + Config: testConfig(url, fooOrg, firstProjectFoo, tt.createResource), + }, + importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey), + }, + } + + for _, resUpdate := range tt.planOnlyUpdates { + tc.Steps = append(tc.Steps, resource.TestStep{ + PlanOnly: true, + Config: testConfig(url, fooOrg, firstProjectFoo, resUpdate), + }, importStep(usernamePasswordDomainCredResc, credentialUsernamePasswordDomainPasswordKey)) + } + + resource.Test(t, tc) + }) + } +} + +func usernamePasswordDomainCredExternalUpdate(t *testing.T, testProvider *schema.Provider) { + if storeId == "" { + t.Fatal("storeId must be set before testing an external update") + } + + md := testProvider.Meta().(*metaData) + c := credentials.NewClient(md.client) + cr, err := c.Read(context.Background(), storeId) + if err != nil { + t.Fatal(fmt.Errorf("got an error reading %q: %w", storeId, err)) + } + + // update credential options + var opts []credentials.Option + opts = append(opts, credentials.WithDescription("this is an updated description, my guy")) + + _, err = c.Update(context.Background(), cr.Item.Id, cr.Item.Version, opts...) + if err != nil { + t.Fatal(fmt.Errorf("got an error updating %q: %w", cr.Item.Id, err)) + } +} + +func testAccCheckCredentialStoreUsernamePasswordDomainPasswordHmac() resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[usernamePasswordDomainCredResc] + if !ok { + return fmt.Errorf("not found: %s", usernamePasswordDomainCredResc) + } + + computed := rs.Primary.Attributes["password_hmac"] + if len(computed) != 43 { + return fmt.Errorf("Computed password hmac not the expected length of 43 characters. hmac: %q", computed) + } + + return nil + } +}