From 182352b6d46d04968908dffa6766151410218bd4 Mon Sep 17 00:00:00 2001 From: Hugo <10965479+hugoghx@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:17:28 +0100 Subject: [PATCH] feat(provider): Implement boundary_credential_username_password_domain resource This resource creates a static "Username, Password & Domain" credential in Boundary. The "username" field is required. It can also contain the domain in one of two formats: - "username@domain.com" - "domain.com\username" The "password" field is required. The "domain" field is optional but the domain data itself is required (ie. it must be provided as part of the username or in the dedicated "domain" field) Co-authored-by: April-May <18632637+AprilMay0@users.noreply.github.com> --- .../credential_username_password_domain.md | 75 ++ .../import.sh | 1 + .../resource.tf | 29 + internal/provider/provider.go | 1 + .../provider/resource_credential_json_test.go | 7 +- ...rce_credential_username_password_domain.go | 351 ++++++++++ ...redential_username_password_domain_test.go | 640 ++++++++++++++++++ 7 files changed, 1101 insertions(+), 3 deletions(-) create mode 100644 docs/resources/credential_username_password_domain.md create mode 100644 examples/resources/boundary_credential_username_password_domain/import.sh create mode 100644 examples/resources/boundary_credential_username_password_domain/resource.tf create mode 100644 internal/provider/resource_credential_username_password_domain.go create mode 100644 internal/provider/resource_credential_username_password_domain_test.go 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 + } +}