diff --git a/docs/resources/webhook_recipient.md b/docs/resources/webhook_recipient.md index 43ca3d8a..260c183e 100644 --- a/docs/resources/webhook_recipient.md +++ b/docs/resources/webhook_recipient.md @@ -17,7 +17,7 @@ resource "honeycombio_webhook_recipient" "prod" { The following arguments are supported: * `name` - (Required) The name of the Webhook Integration to create. -* `secret` - (Required) The secret to include when sending the notification to the webhook. +* `secret` - (Optional) The secret to include when sending the notification to the webhook. * `url` - (Required) The URL of the endpoint to send the notification to. ## Attribute Reference diff --git a/honeycombio/provider.go b/honeycombio/provider.go index c5bc0be4..653379c5 100644 --- a/honeycombio/provider.go +++ b/honeycombio/provider.go @@ -71,7 +71,6 @@ func Provider(version string) *schema.Provider { "honeycombio_msteams_recipient": newMSTeamsRecipient(), // deprecated "honeycombio_msteams_workflow_recipient": newMSTeamsWorkflowRecipient(), "honeycombio_slack_recipient": newSlackRecipient(), - "honeycombio_webhook_recipient": newWebhookRecipient(), "honeycombio_slo": newSLO(), }, } diff --git a/honeycombio/resource_webhook_recipient.go b/honeycombio/resource_webhook_recipient.go deleted file mode 100644 index 94333101..00000000 --- a/honeycombio/resource_webhook_recipient.go +++ /dev/null @@ -1,60 +0,0 @@ -package honeycombio - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client" -) - -func newWebhookRecipient() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceWebhookRecipientCreate, - ReadContext: resourceWebhookRecipientRead, - UpdateContext: resourceWebhookRecipientUpdate, - DeleteContext: resourceWebhookRecipientDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Description: "Honeycomb Webhook Recipient allows you to define and manage a Webhook recipient that can be used by Triggers or BurnAlerts notifications.", - - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - Description: "The name of the Webhook Integration to create", - }, - "secret": { - Type: schema.TypeString, - Required: true, - Sensitive: true, - Description: "The secret to include when sending the notification to the webhook", - }, - "url": { - Type: schema.TypeString, - Required: true, - Description: "The URL of the endpoint to send the integration to", - ValidateFunc: validation.IsURLWithHTTPorHTTPS, - }, - }, - } -} - -func resourceWebhookRecipientCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return createRecipient(ctx, d, meta, honeycombio.RecipientTypeWebhook) -} - -func resourceWebhookRecipientRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return readRecipient(ctx, d, meta, honeycombio.RecipientTypeWebhook) -} - -func resourceWebhookRecipientUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return updateRecipient(ctx, d, meta, honeycombio.RecipientTypeWebhook) -} - -func resourceWebhookRecipientDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return deleteRecipient(ctx, d, meta) -} diff --git a/internal/helper/validation/divisible_by_test.go b/internal/helper/validation/divisible_by_test.go index 27752c40..4925388b 100644 --- a/internal/helper/validation/divisible_by_test.go +++ b/internal/helper/validation/divisible_by_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" ) @@ -54,13 +55,11 @@ func Test_BetweenValidator(t *testing.T) { response := validator.Int64Response{} validation.Int64DivisibleBy(test.divisor).ValidateInt64(context.Background(), request, &response) - if !response.Diagnostics.HasError() && test.expectError { - t.Fatal("expected error, got no error") - } - - if response.Diagnostics.HasError() && !test.expectError { - t.Fatalf("got unexpected error: %s", response.Diagnostics) - } + assert.Equal(t, + test.expectError, + response.Diagnostics.HasError(), + "unexpected error: %s", response.Diagnostics, + ) }) } } diff --git a/internal/helper/validation/precision_at_most_test.go b/internal/helper/validation/precision_at_most_test.go index bb0d969d..f6522878 100644 --- a/internal/helper/validation/precision_at_most_test.go +++ b/internal/helper/validation/precision_at_most_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" ) @@ -64,13 +65,11 @@ func TestValidation_PrecisionAtMost(t *testing.T) { response := validator.Float64Response{} validation.Float64PrecisionAtMost(test.maxPrecision).ValidateFloat64(context.Background(), request, &response) - if !response.Diagnostics.HasError() && test.expectError { - t.Fatal("expected error, got no error") - } - - if response.Diagnostics.HasError() && !test.expectError { - t.Fatalf("got unexpected error: %s", response.Diagnostics) - } + assert.Equal(t, + test.expectError, + response.Diagnostics.HasError(), + "unexpected error: %s", response.Diagnostics, + ) }) } } diff --git a/internal/helper/validation/query_spec_test.go b/internal/helper/validation/query_spec_test.go index e3f665bb..9b23fa32 100644 --- a/internal/helper/validation/query_spec_test.go +++ b/internal/helper/validation/query_spec_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" ) @@ -48,13 +49,11 @@ func Test_QuerySpecValidator(t *testing.T) { response := validator.StringResponse{} validation.ValidQuerySpec().ValidateString(context.Background(), request, &response) - if !response.Diagnostics.HasError() && test.expectError { - t.Fatal("expected error, got no error") - } - - if response.Diagnostics.HasError() && !test.expectError { - t.Fatalf("got unexpected error: %s", response.Diagnostics) - } + assert.Equal(t, + test.expectError, + response.Diagnostics.HasError(), + "unexpected error: %s", response.Diagnostics, + ) }) } } diff --git a/internal/helper/validation/valid_regex_test.go b/internal/helper/validation/valid_regex_test.go index b71279d9..0bddec93 100644 --- a/internal/helper/validation/valid_regex_test.go +++ b/internal/helper/validation/valid_regex_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" ) @@ -45,13 +46,11 @@ func Test_IsValidRegexValidator(t *testing.T) { response := validator.StringResponse{} validation.IsValidRegExp().ValidateString(context.Background(), request, &response) - if !response.Diagnostics.HasError() && test.expectError { - t.Fatal("expected error, got no error") - } - - if response.Diagnostics.HasError() && !test.expectError { - t.Fatalf("got unexpected error: %s", response.Diagnostics) - } + assert.Equal(t, + test.expectError, + response.Diagnostics.HasError(), + "unexpected error: %s", response.Diagnostics, + ) }) } } diff --git a/internal/helper/validation/valid_url.go b/internal/helper/validation/valid_url.go new file mode 100644 index 00000000..f4e1744b --- /dev/null +++ b/internal/helper/validation/valid_url.go @@ -0,0 +1,66 @@ +package validation + +import ( + "context" + "fmt" + "net/url" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.String = isValidURLValidator{} + +type isValidURLValidator struct { + schemes []string +} + +func (v isValidURLValidator) Description(_ context.Context) string { + return "value must be a valid URL" +} + +func (v isValidURLValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v isValidURLValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) { + if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() { + return + } + + u, err := url.Parse(request.ConfigValue.ValueString()) + if err != nil { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + v.Description(ctx), + fmt.Sprintf("%q: %s", request.ConfigValue.ValueString(), err.Error()), + )) + return + } + + if u.Host == "" { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + v.Description(ctx)+" but is missing a host", + request.ConfigValue.ValueString(), + )) + } + + if !slices.Contains(v.schemes, u.Scheme) { + response.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic( + request.Path, + fmt.Sprintf("%s with a schema of %q", v.Description(ctx), strings.Join(v.schemes, ",")), + request.ConfigValue.ValueString(), + )) + } +} + +// IsURLWithHTTPorHTTPS returns an AttributeValidator which ensures that any configured +// attribute value is a valid HTTP or HTTPS URL. +// +// Null (unconfigured) and unknown (known after apply) values are skipped. +func IsURLWithHTTPorHTTPS() validator.String { + return isValidURLValidator{[]string{"http", "https"}} +} diff --git a/internal/helper/validation/valid_url_test.go b/internal/helper/validation/valid_url_test.go new file mode 100644 index 00000000..d2eb6983 --- /dev/null +++ b/internal/helper/validation/valid_url_test.go @@ -0,0 +1,73 @@ +package validation_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" + + "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" +) + +func Test_IsURLWithHTTPorHTTPS(t *testing.T) { + t.Parallel() + ctx := context.Background() + + type testCase struct { + val types.String + expectError bool + } + tests := map[string]testCase{ + "unknown": { + val: types.StringUnknown(), + }, + "null": { + val: types.StringNull(), + }, + "valid http": { + val: types.StringValue("http://sub.example.com/a/b/c/d?e=f#g"), + }, + "valid https": { + val: types.StringValue("https://sub.example.com/a/b/c/d?e=f#g"), + }, + "empty": { + val: types.StringValue(""), + expectError: true, + }, + "garbage": { + val: types.StringValue("not-a-url"), + expectError: true, + }, + "missing host": { + val: types.StringValue("http:///a/b/c/d?e=f#g"), + expectError: true, + }, + "invalid scheme": { + val: types.StringValue("ftp://sub.example.com/"), + expectError: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + request := validator.StringRequest{ + Path: path.Root("test"), + PathExpression: path.MatchRoot("test"), + ConfigValue: test.val, + } + response := validator.StringResponse{} + validation.IsURLWithHTTPorHTTPS().ValidateString(ctx, request, &response) + + assert.Equal(t, + test.expectError, + response.Diagnostics.HasError(), + "unexpected error: %s", response.Diagnostics, + ) + }) + } +} diff --git a/internal/models/recipients.go b/internal/models/recipients.go new file mode 100644 index 00000000..c98586ec --- /dev/null +++ b/internal/models/recipients.go @@ -0,0 +1,12 @@ +package models + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type WebhookRecipientModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Secret types.String `tfsdk:"secret"` + URL types.String `tfsdk:"url"` +} diff --git a/internal/provider/dataset_resource_test.go b/internal/provider/dataset_resource_test.go index e368fedd..43cd6bac 100644 --- a/internal/provider/dataset_resource_test.go +++ b/internal/provider/dataset_resource_test.go @@ -195,8 +195,7 @@ resource "honeycombio_dataset" "test" { }`, name), }, }, - }, - ) + }) } func testAccConfigBasicDatasetTest(name, description string, jsonDepth int, protected bool) string { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c24e3308..7bdb87b6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -80,6 +80,7 @@ func (p *HoneycombioProvider) Resources(ctx context.Context) []func() resource.R NewDatasetResource, NewTriggerResource, NewQueryResource, + NewWebhookRecipientResource, NewAPIKeyResource, NewEnvironmentResource, } diff --git a/internal/provider/query_resource_test.go b/internal/provider/query_resource_test.go index f92e4573..ca5d4b65 100644 --- a/internal/provider/query_resource_test.go +++ b/internal/provider/query_resource_test.go @@ -139,7 +139,7 @@ func TestAcc_QueryResourceUpgradeFromVersion022(t *testing.T) { }) } -// TestAcc_QueryResourceEquivalentQuerySpecSupressed tests the behavior of the +// TestAcc_QueryResourceEquivalentQuerySpecSupressed tests the behavior of the // resource when an equivalent query is suppressed by the plan modifier. func TestAcc_QueryResourceEquivalentQuerySpecSupressed(t *testing.T) { dataset := testAccDataset() diff --git a/internal/provider/webhook_recipient_resource.go b/internal/provider/webhook_recipient_resource.go new file mode 100644 index 00000000..1066f7c5 --- /dev/null +++ b/internal/provider/webhook_recipient_resource.go @@ -0,0 +1,249 @@ +package provider + +import ( + "context" + "errors" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/honeycombio/terraform-provider-honeycombio/client" + "github.com/honeycombio/terraform-provider-honeycombio/internal/helper" + "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation" + "github.com/honeycombio/terraform-provider-honeycombio/internal/models" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &webhookRecipientResource{} + _ resource.ResourceWithConfigure = &webhookRecipientResource{} + _ resource.ResourceWithImportState = &webhookRecipientResource{} +) + +type webhookRecipientResource struct { + client *client.Client +} + +func NewWebhookRecipientResource() resource.Resource { + return &webhookRecipientResource{} +} + +func (*webhookRecipientResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_webhook_recipient" +} + +func (r *webhookRecipientResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + w := getClientFromResourceRequest(&req) + if w == nil { + return + } + + c, err := w.V1Client() + if err != nil || c == nil { + resp.Diagnostics.AddError("Failed to configure client", err.Error()) + return + } + r.client = c +} + +func (*webhookRecipientResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A Webhook recipient can be used by Triggers or BurnAlerts notifications to send an event to an HTTP endpoint.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier for this Recipient.", + Computed: true, + Required: false, + Optional: false, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of this Webhook recipient.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "secret": schema.StringAttribute{ + Description: "The secret to include when sending the notification to the webhook.", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(255), + }, + }, + "url": schema.StringAttribute{ + Description: "The URL of the endpoint the notification will be sent to.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 2048), + validation.IsURLWithHTTPorHTTPS(), + }, + }, + }, + } +} + +func (r *webhookRecipientResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if req.ID == "" { + resp.Diagnostics.AddError("Invalid Import ID", "The Recipient ID must be provided") + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &models.WebhookRecipientModel{ + ID: types.StringValue(req.ID), + })...) +} + +func (r *webhookRecipientResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan models.WebhookRecipientModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + rcpt, err := r.client.Recipients.Create(ctx, &client.Recipient{ + Type: client.RecipientTypeWebhook, + Details: client.RecipientDetails{ + WebhookName: plan.Name.ValueString(), + WebhookURL: plan.URL.ValueString(), + WebhookSecret: plan.Secret.ValueString(), + }, + }) + if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Webhook Recipient", err) { + return + } + + var state models.WebhookRecipientModel + state.ID = types.StringValue(rcpt.ID) + state.Name = types.StringValue(rcpt.Details.WebhookName) + state.URL = types.StringValue(rcpt.Details.WebhookURL) + if rcpt.Details.WebhookSecret != "" { + state.Secret = types.StringValue(rcpt.Details.WebhookSecret) + } else { + state.Secret = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *webhookRecipientResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state models.WebhookRecipientModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var detailedErr client.DetailedError + rcpt, err := r.client.Recipients.Get(ctx, state.ID.ValueString()) + if errors.As(err, &detailedErr) { + if detailedErr.IsNotFound() { + // if not found consider it deleted -- so just remove it from state + resp.State.RemoveResource(ctx) + return + } else { + resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic( + "Error Reading Honeycomb Webhook Recipient", + &detailedErr, + )) + } + } else if err != nil { + resp.Diagnostics.AddError( + "Error Reading Honeycomb Webhook Recipient", + "Unexpected error reading Webhook Recipient "+state.ID.ValueString()+": "+err.Error(), + ) + } + if resp.Diagnostics.HasError() { + return + } + if rcpt.Type != client.RecipientTypeWebhook { + resp.Diagnostics.AddError( + "Error Reading Honeycomb Webhook Recipient", + "Unexpected recipient type "+rcpt.Type.String(), + ) + return + } + + state.ID = types.StringValue(rcpt.ID) + state.Name = types.StringValue(rcpt.Details.WebhookName) + state.URL = types.StringValue(rcpt.Details.WebhookURL) + if rcpt.Details.WebhookSecret != "" { + state.Secret = types.StringValue(rcpt.Details.WebhookSecret) + } else { + state.Secret = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *webhookRecipientResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan models.WebhookRecipientModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.Recipients.Update(ctx, &client.Recipient{ + ID: plan.ID.ValueString(), + Type: client.RecipientTypeWebhook, + Details: client.RecipientDetails{ + WebhookName: plan.Name.ValueString(), + WebhookURL: plan.URL.ValueString(), + WebhookSecret: plan.Secret.ValueString(), + }, + }) + if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Webhook Recipient", err) { + return + } + + rcpt, err := r.client.Recipients.Get(ctx, plan.ID.ValueString()) + if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Burn Alert", err) { + return + } + + var state models.WebhookRecipientModel + state.ID = types.StringValue(rcpt.ID) + state.Name = types.StringValue(rcpt.Details.WebhookName) + state.URL = types.StringValue(rcpt.Details.WebhookURL) + if rcpt.Details.WebhookSecret != "" { + state.Secret = types.StringValue(rcpt.Details.WebhookSecret) + } else { + state.Secret = types.StringNull() + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *webhookRecipientResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state models.WebhookRecipientModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var detailedErr client.DetailedError + err := r.client.Recipients.Delete(ctx, state.ID.ValueString()) + if err != nil { + if errors.As(err, &detailedErr) { + // if not found consider it deleted -- so don't error + if !detailedErr.IsNotFound() { + resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic( + "Error Deleting Honeycomb Webhook Recipient", + &detailedErr, + )) + } + } else { + resp.Diagnostics.AddError( + "Error Deleting Honeycomb Webhook Recipient", + "Could not delete Webhook Recipient ID "+state.ID.ValueString()+": "+err.Error(), + ) + } + } +} diff --git a/internal/provider/webhook_recipient_resource_test.go b/internal/provider/webhook_recipient_resource_test.go new file mode 100644 index 00000000..fab27236 --- /dev/null +++ b/internal/provider/webhook_recipient_resource_test.go @@ -0,0 +1,120 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test" +) + +func TestAcc_WebhookRecipientResource(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + name := test.RandomStringWithPrefix("test.", 20) + url := test.RandomURL() + + resource.Test(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` +resource "honeycombio_webhook_recipient" "test" { + name = "%s" + url = "%s" +}`, name, url), + Check: resource.ComposeAggregateTestCheckFunc( + testAccEnsureRecipientExists(t, "honeycombio_webhook_recipient.test"), + resource.TestCheckResourceAttrSet("honeycombio_webhook_recipient.test", "id"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "name", name), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "url", url), + resource.TestCheckNoResourceAttr("honeycombio_webhook_recipient.test", "secret"), + ), + }, + { + Config: fmt.Sprintf(` +resource "honeycombio_webhook_recipient" "test" { + name = "%s" + url = "%s" + + secret = "so-secret" +}`, name, url), + Check: resource.ComposeAggregateTestCheckFunc( + testAccEnsureRecipientExists(t, "honeycombio_webhook_recipient.test"), + resource.TestCheckResourceAttrSet("honeycombio_webhook_recipient.test", "id"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "name", name), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "url", url), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "secret", "so-secret"), + ), + }, + { + ResourceName: "honeycombio_webhook_recipient.test", + ImportState: true, + }, + }, + }) + }) +} + +// TestAcc_WebhookRecipientResource_UpgradeFromVersion027 tests the migration case from the +// last SDK-based version of the Webhook Recipient resource to the current Framework-based version. +// +// See: https://developer.hashicorp.com/terraform/plugin/framework/migrating/testing#testing-migration +func TestAcc_WebhookRecipientResource_UpgradeFromVersion027(t *testing.T) { + name := test.RandomStringWithPrefix("test.", 20) + url := test.RandomURL() + config := fmt.Sprintf(` +resource "honeycombio_webhook_recipient" "test" { + name = "%s" + url = "%s" + secret = "so-secret" +}`, name, url) + + resource.Test(t, resource.TestCase{ + PreCheck: testAccPreCheck(t), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "honeycombio": { + VersionConstraint: "0.27", + Source: "honeycombio/honeycombio", + }, + }, + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + testAccEnsureRecipientExists(t, "honeycombio_webhook_recipient.test"), + ), + }, + { + ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory, + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("honeycombio_webhook_recipient.test", "id"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "name", name), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "url", url), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "secret", "so-secret"), + ), + }, + }, + }) +} + +func testAccEnsureRecipientExists(t *testing.T, name string) resource.TestCheckFunc { //nolint:unparam + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("resource not found: %s", name) + } + + client := testAccClient(t) + _, err := client.Recipients.Get(context.Background(), rs.Primary.ID) + if err != nil { + return fmt.Errorf("failed to fetch created recipient: %s", err) + } + + return nil + } +}