diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4373e847..3ec11485 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: name: Test US needs: build runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 concurrency: group: hnytf-testacc-us env: @@ -58,7 +58,6 @@ jobs: ./scripts/setup-testsuite-dataset - name: Run client acceptance tests - timeout-minutes: 10 env: HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }} @@ -66,7 +65,6 @@ jobs: HONEYCOMB_DATASET: testacc run: | go test -v ./client/... \ - -coverprofile=client-coverage.txt \ -covermode=atomic | \ go-junit-report \ -set-exit-code \ @@ -79,7 +77,6 @@ jobs: terraform_wrapper: false - name: Run TF acceptance tests - timeout-minutes: 10 env: HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }} @@ -89,7 +86,6 @@ jobs: TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }} run: | go test -v ./internal/... ./honeycombio/... \ - -coverprofile=tf-coverage.txt \ -covermode=atomic | \ go-junit-report \ -set-exit-code \ @@ -110,7 +106,6 @@ jobs: - name: Cleanup Dangling Resources if: ${{ always() }} - timeout-minutes: 5 env: HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }} @@ -125,16 +120,11 @@ jobs: paths: "*-report.xml" show: "fail, skip" - - name: Generate Coverage Report - uses: codecov/codecov-action@v4.6.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - test-eu: name: Test EU needs: build runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 20 concurrency: group: hnytf-testacc-eu env: @@ -159,7 +149,6 @@ jobs: ./scripts/setup-testsuite-dataset - name: Run client acceptance tests - timeout-minutes: 10 env: HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }} @@ -168,7 +157,6 @@ jobs: HONEYCOMB_DATASET: testacc run: | go test -v ./client/... \ - -coverprofile=client-coverage.txt \ -covermode=atomic | \ go-junit-report \ -set-exit-code \ @@ -181,7 +169,6 @@ jobs: terraform_wrapper: false - name: Run TF acceptance tests - timeout-minutes: 10 env: HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }} @@ -192,7 +179,6 @@ jobs: TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }} run: | go test -v ./internal/... ./honeycombio/... \ - -coverprofile=tf-coverage.txt \ -covermode=atomic | \ go-junit-report \ -set-exit-code \ @@ -213,7 +199,6 @@ jobs: - name: Cleanup Dangling Resources if: ${{ always() }} - timeout-minutes: 5 env: HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }} @@ -228,8 +213,3 @@ jobs: with: paths: "*-report.xml" show: "fail, skip" - - - name: Generate Coverage Report - uses: codecov/codecov-action@v4.6.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd9012f6..7ef3bdbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Any new additions should be built with the Plugin Framework. * The Plugin SDK-based code is contained in the `honeycombio/` directory in the root of the repository. * The Plugin Framework-based code is contained in the `internal/provider` directory. -Any PRs reimplmenting Plugin SDKv2 resources or datasources in the Plugin Framework with be enthusiastically accepted. 🙏 +Any PRs reimplementing Plugin SDKv2 resources or datasources in the Plugin Framework will be enthusiastically accepted. 🙏 ### Preview document changes diff --git a/README.md b/README.md index c76c620f..fefb01cc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![OSS Lifecycle](https://img.shields.io/osslifecycle/honeycombio/terraform-provider-honeycombio)](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md) [![CI](https://github.com/honeycombio/terraform-provider-honeycombio/workflows/CI/badge.svg)](https://github.com/honeycombio/terraform-provider-honeycombio/actions) -[![codecov](https://codecov.io/gh/honeycombio/terraform-provider-honeycombio/branch/main/graph/badge.svg)](https://codecov.io/gh/honeycombio/terraform-provider-honeycombio) [![Terraform Registry](https://img.shields.io/github/v/release/honeycombio/terraform-provider-honeycombio?color=5e4fe3&label=Terraform%20Registry&logo=terraform&sort=semver)](https://registry.terraform.io/providers/honeycombio/honeycombio/latest) A Terraform provider for Honeycomb.io. diff --git a/client/recipient.go b/client/recipient.go index 7d3a06c5..e5e90e4f 100644 --- a/client/recipient.go +++ b/client/recipient.go @@ -65,13 +65,41 @@ type RecipientDetails struct { WebhookName string `json:"webhook_name,omitempty"` WebhookURL string `json:"webhook_url,omitempty"` // webhook only - WebhookSecret string `json:"webhook_secret,omitempty"` + WebhookSecret string `json:"webhook_secret,omitempty"` + WebhookHeaders []WebhookHeader `json:"webhook_headers"` + // custom webhook + WebhookPayloads *WebhookPayloads `json:"webhook_payloads,omitempty"` } type NotificationRecipientDetails struct { PDSeverity PagerDutySeverity `json:"pagerduty_severity,omitempty"` } +type WebhookPayloads struct { + PayloadTemplates PayloadTemplates `json:"payload_templates"` + TemplateVariables []TemplateVariable `json:"template_variables"` +} + +type PayloadTemplates struct { + Trigger *PayloadTemplate `json:"trigger,omitempty"` + ExhaustionTime *PayloadTemplate `json:"exhaustion_time,omitempty"` + BudgetRate *PayloadTemplate `json:"budget_rate,omitempty"` +} + +type PayloadTemplate struct { + Body string `json:"body"` +} + +type TemplateVariable struct { + Name string `json:"name"` + Default string `json:"default_value"` +} + +type WebhookHeader struct { + Key string `json:"header"` + Value string `json:"value"` +} + // RecipientType holds all the possible recipient types. type RecipientType string diff --git a/client/recipient_test.go b/client/recipient_test.go index cce341e7..731f9a9e 100644 --- a/client/recipient_test.go +++ b/client/recipient_test.go @@ -80,6 +80,65 @@ func TestRecipientsEmail(t *testing.T) { }) } +func TestRecipientsCustomWebhook(t *testing.T) { + t.Parallel() + + ctx := context.Background() + c := newTestClient(t) + + body := `{"hello": "world"}` + + testCases := []struct { + rcpt client.Recipient + expectErr bool + }{ + { + rcpt: client.Recipient{ + Type: client.RecipientTypeWebhook, + Details: client.RecipientDetails{ + WebhookName: test.RandomStringWithPrefix("test.", 10), + WebhookURL: test.RandomURL(), + WebhookSecret: "secret", + WebhookHeaders: []client.WebhookHeader{{Key: "Authorization", Value: "Bearer 123"}}, + WebhookPayloads: &client.WebhookPayloads{ + PayloadTemplates: client.PayloadTemplates{Trigger: &client.PayloadTemplate{Body: body}}, + TemplateVariables: []client.TemplateVariable{{Name: "severity", Default: "warning"}}, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tr := tc.rcpt + t.Run(tr.Type.String(), func(t *testing.T) { + r, err := c.Recipients.Create(ctx, &tr) + t.Cleanup(func() { + _ = c.Recipients.Delete(ctx, r.ID) + }) + + if tc.expectErr { + require.Error(t, err, "expected error creating %s recipient", tr.Type) + return + } + require.NoError(t, err, "failed to create %s recipient", tr.Type) + r, err = c.Recipients.Get(ctx, r.ID) + require.NoError(t, err) + + assert.Equal(t, tr.Type, r.Type) + assert.Equal(t, tr.Details.WebhookName, r.Details.WebhookName) + assert.Equal(t, tr.Details.WebhookURL, r.Details.WebhookURL) + assert.Equal(t, tr.Details.WebhookSecret, r.Details.WebhookSecret) + assert.Equal(t, tr.Details.WebhookPayloads, r.Details.WebhookPayloads) + assert.Equal(t, tr.Details.WebhookPayloads.TemplateVariables, r.Details.WebhookPayloads.TemplateVariables) + if assert.Len(t, r.Details.WebhookHeaders, 1) { + assert.Equal(t, tr.Details.WebhookHeaders[0].Key, r.Details.WebhookHeaders[0].Key) + assert.Equal(t, tr.Details.WebhookHeaders[0].Value, r.Details.WebhookHeaders[0].Value) + } + }) + } +} + func TestRecipientsWebhooksandMSTeams(t *testing.T) { t.Parallel() diff --git a/docs/resources/webhook_recipient.md b/docs/resources/webhook_recipient.md index 260c183e..7e68a242 100644 --- a/docs/resources/webhook_recipient.md +++ b/docs/resources/webhook_recipient.md @@ -2,7 +2,7 @@ `honeycombio_webhook_recipient` allows you to define and manage a Webhook recipient that can be used by Triggers or BurnAlerts notifications. -## Example Usage +## Standard Webhook Example ```hcl resource "honeycombio_webhook_recipient" "prod" { @@ -12,6 +12,41 @@ resource "honeycombio_webhook_recipient" "prod" { } ``` +## Custom Webhook Example + +```hcl +resource "honeycombio_webhook_recipient" "prod" { + name = "Production Alerts" + secret = "a63dab148496ecbe04a1a802ca9b95b8" + url = "https://my.url.corp.net" + + header { + name = "Authorization" + value = "Bearer 123" + } + + template { + type = "trigger" + body = <= 1 && len(templates) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("variable").AtListIndex(0), + "Conflicting configuration arguments", + "cannot configure a \"variable\" without also configuring a \"template\"", + ) + } + + // variable names cannot be duplicated + duplicateMap := make(map[string]bool) + for i, v := range variables { + name := v.Name.ValueString() + if duplicateMap[name] { + resp.Diagnostics.AddAttributeError( + path.Root("variable").AtListIndex(i).AtName("name"), + "Conflicting configuration arguments", + "cannot have more than one \"variable\" with the same \"name\"", + ) + } + duplicateMap[name] = true + } + + // webhook headers must be valid http headers + for i, h := range headers { + if !httpguts.ValidHeaderFieldName(h.Name.ValueString()) { + resp.Diagnostics.AddAttributeError( + path.Root("header").AtListIndex(i).AtName("name"), + "Conflicting configuration arguments", + "invalid webhook header name", + ) + } + if !httpguts.ValidHeaderFieldValue(h.Value.ValueString()) { + resp.Diagnostics.AddAttributeError( + path.Root("header").AtListIndex(i).AtName("value"), + "Conflicting configuration arguments", + "invalid webhook header value", + ) + } + } +} + 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)...) @@ -112,9 +298,11 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea 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(), + WebhookName: plan.Name.ValueString(), + WebhookURL: plan.URL.ValueString(), + WebhookSecret: plan.Secret.ValueString(), + WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics), + WebhookHeaders: expandWebhookHeaders(ctx, plan.Headers, &resp.Diagnostics), }, }) if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb Webhook Recipient", err) { @@ -131,6 +319,22 @@ func (r *webhookRecipientResource) Create(ctx context.Context, req resource.Crea state.Secret = types.StringNull() } + // to prevent confusing if/else blocks, set null by default and override it if we have that detail on the recipient + state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}) + state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}) + state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}) + + if rcpt.Details.WebhookPayloads != nil { + state.Templates = plan.Templates + if rcpt.Details.WebhookPayloads.TemplateVariables != nil { + state.Variables = plan.Variables + } + } + + if rcpt.Details.WebhookHeaders != nil { + state.Headers = plan.Headers + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } @@ -180,6 +384,19 @@ func (r *webhookRecipientResource) Read(ctx context.Context, req resource.ReadRe state.Secret = types.StringNull() } + if rcpt.Details.WebhookPayloads != nil { + state.Templates, state.Variables = clientPayloadsToWebhookTemplateSets(ctx, rcpt.Details.WebhookPayloads, &resp.Diagnostics) + } else { + state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}) + state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}) + } + + if rcpt.Details.WebhookHeaders != nil { + state.Headers = flattenWebhookHeaders(ctx, rcpt.Details.WebhookHeaders, &resp.Diagnostics) + } else { + state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}) + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } @@ -194,9 +411,11 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda ID: plan.ID.ValueString(), Type: client.RecipientTypeWebhook, Details: client.RecipientDetails{ - WebhookName: plan.Name.ValueString(), - WebhookURL: plan.URL.ValueString(), - WebhookSecret: plan.Secret.ValueString(), + WebhookName: plan.Name.ValueString(), + WebhookURL: plan.URL.ValueString(), + WebhookSecret: plan.Secret.ValueString(), + WebhookHeaders: expandWebhookHeaders(ctx, plan.Headers, &resp.Diagnostics), + WebhookPayloads: webhookTemplatesToClientPayloads(ctx, plan.Templates, plan.Variables, &resp.Diagnostics), }, }) if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Webhook Recipient", err) { @@ -204,7 +423,7 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda } rcpt, err := r.client.Recipients.Get(ctx, plan.ID.ValueString()) - if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Burn Alert", err) { + if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb Webhook Recipient", err) { return } @@ -218,6 +437,22 @@ func (r *webhookRecipientResource) Update(ctx context.Context, req resource.Upda state.Secret = types.StringNull() } + // to prevent confusing if/else blocks, set null by default and override it if we have that detail on the recipient + state.Templates = types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}) + state.Variables = types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}) + state.Headers = types.SetNull(types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}) + + if rcpt.Details.WebhookPayloads != nil { + state.Templates = plan.Templates + if rcpt.Details.WebhookPayloads.TemplateVariables != nil { + state.Variables = plan.Variables + } + } + + if rcpt.Details.WebhookHeaders != nil { + state.Headers = plan.Headers + } + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } @@ -247,3 +482,154 @@ func (r *webhookRecipientResource) Delete(ctx context.Context, req resource.Dele } } } + +func webhookTemplatesToClientPayloads(ctx context.Context, templateSet types.Set, variableSet types.Set, diags *diag.Diagnostics) *client.WebhookPayloads { + var templates []models.WebhookTemplateModel + diags.Append(templateSet.ElementsAs(ctx, &templates, false)...) + if diags.HasError() { + return nil + } + + var variables []models.TemplateVariableModel + diags.Append(variableSet.ElementsAs(ctx, &variables, false)...) + if diags.HasError() { + return nil + } + + clientWebhookPayloads := &client.WebhookPayloads{} + + for _, t := range templates { + switch t.Type { + case types.StringValue("trigger"): + clientWebhookPayloads.PayloadTemplates.Trigger = &client.PayloadTemplate{ + Body: t.Body.ValueString(), + } + case types.StringValue("exhaustion_time"): + clientWebhookPayloads.PayloadTemplates.ExhaustionTime = &client.PayloadTemplate{ + Body: t.Body.ValueString(), + } + case types.StringValue("budget_rate"): + clientWebhookPayloads.PayloadTemplates.BudgetRate = &client.PayloadTemplate{ + Body: t.Body.ValueString(), + } + } + } + + clientVars := make([]client.TemplateVariable, len(variables)) + for i, v := range variables { + tmplVar := client.TemplateVariable{ + Name: v.Name.ValueString(), + Default: v.DefaultValue.ValueString(), + } + + clientVars[i] = tmplVar + } + clientWebhookPayloads.TemplateVariables = clientVars + + return clientWebhookPayloads +} + +func clientPayloadsToWebhookTemplateSets(ctx context.Context, p *client.WebhookPayloads, diags *diag.Diagnostics) (types.Set, types.Set) { + if p == nil { + return types.SetNull(types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}), types.SetNull(types.ObjectType{AttrTypes: models.TemplateVariableAttrType}) + } + + tmplValues := webhookTemplatesToObjectValues(p.PayloadTemplates, diags) + tmplResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookTemplateAttrType}, tmplValues) + diags.Append(d...) + + var tmplVarValues []attr.Value + for _, v := range p.TemplateVariables { + tmplVarValues = append(tmplVarValues, webhookVariableToObjectValue(v, diags)) + } + varResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.TemplateVariableAttrType}, tmplVarValues) + diags.Append(d...) + + return tmplResult, varResult +} + +func webhookTemplatesToObjectValues(templates client.PayloadTemplates, diags *diag.Diagnostics) []basetypes.ObjectValue { + var templateObjs []basetypes.ObjectValue + + if templates.Trigger != nil { + templateObjVal, d := types.ObjectValue(models.WebhookTemplateAttrType, map[string]attr.Value{ + "type": types.StringValue("trigger"), + "body": types.StringValue(templates.Trigger.Body), + }) + templateObjs = append(templateObjs, templateObjVal) + diags.Append(d...) + } + + if templates.BudgetRate != nil { + templateObjVal, d := types.ObjectValue(models.WebhookTemplateAttrType, map[string]attr.Value{ + "type": types.StringValue("budget_rate"), + "body": types.StringValue(templates.BudgetRate.Body), + }) + templateObjs = append(templateObjs, templateObjVal) + diags.Append(d...) + } + + if templates.ExhaustionTime != nil { + templateObjVal, d := types.ObjectValue(models.WebhookTemplateAttrType, map[string]attr.Value{ + "type": types.StringValue("exhaustion_time"), + "body": types.StringValue(templates.ExhaustionTime.Body), + }) + templateObjs = append(templateObjs, templateObjVal) + diags.Append(d...) + } + + return templateObjs +} + +func webhookVariableToObjectValue(v client.TemplateVariable, diags *diag.Diagnostics) basetypes.ObjectValue { + variableObj := map[string]attr.Value{ + "name": types.StringValue(v.Name), + "default_value": types.StringValue(v.Default), + } + varObjVal, d := types.ObjectValue(models.TemplateVariableAttrType, variableObj) + diags.Append(d...) + + return varObjVal +} + +func expandWebhookHeaders(ctx context.Context, set types.Set, diags *diag.Diagnostics) []client.WebhookHeader { + var headers []models.WebhookHeaderModel + diags.Append(set.ElementsAs(ctx, &headers, false)...) + if diags.HasError() { + return nil + } + + clientHeaders := make([]client.WebhookHeader, len(headers)) + for i, h := range headers { + hdr := client.WebhookHeader{ + Key: h.Name.ValueString(), + Value: h.Value.ValueString(), + } + + clientHeaders[i] = hdr + } + + return clientHeaders +} + +func flattenWebhookHeaders(ctx context.Context, hdrs []client.WebhookHeader, diags *diag.Diagnostics) types.Set { + var hdrValues []attr.Value + for _, h := range hdrs { + hdrValues = append(hdrValues, webhookHeaderToObjectValue(h, diags)) + } + hdrResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}, hdrValues) + diags.Append(d...) + + return hdrResult +} + +func webhookHeaderToObjectValue(h client.WebhookHeader, diags *diag.Diagnostics) basetypes.ObjectValue { + headerObj := map[string]attr.Value{ + "name": types.StringValue(h.Key), + "value": types.StringValue(h.Value), + } + headerObjVal, d := types.ObjectValue(models.WebhookHeaderAttrType, headerObj) + diags.Append(d...) + + return headerObjVal +} diff --git a/internal/provider/webhook_recipient_resource_test.go b/internal/provider/webhook_recipient_resource_test.go index 538e0bf5..ca617251 100644 --- a/internal/provider/webhook_recipient_resource_test.go +++ b/internal/provider/webhook_recipient_resource_test.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -12,13 +13,14 @@ import ( ) func TestAcc_WebhookRecipientResource(t *testing.T) { - t.Run("happy path", func(t *testing.T) { + t.Run("happy path standard webhook", func(t *testing.T) { name := test.RandomStringWithPrefix("test.", 20) url := test.RandomURL() resource.Test(t, resource.TestCase{ PreCheck: testAccPreCheck(t), ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory, + CheckDestroy: testAccEnsureRecipientDestroyed(t), Steps: []resource.TestStep{ { Config: fmt.Sprintf(` @@ -32,6 +34,8 @@ resource "honeycombio_webhook_recipient" "test" { resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "name", name), resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "url", url), resource.TestCheckNoResourceAttr("honeycombio_webhook_recipient.test", "secret"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "template.#", "0"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "variable.#", "0"), ), }, { @@ -48,6 +52,7 @@ resource "honeycombio_webhook_recipient" "test" { 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"), + resource.TestCheckResourceAttr("honeycombio_webhook_recipient.test", "template.#", "0"), ), }, { @@ -57,6 +62,692 @@ resource "honeycombio_webhook_recipient" "test" { }, }) }) + + t.Run("happy path custom webhook", func(t *testing.T) { + name := test.RandomStringWithPrefix("test.", 20) + url := test.RandomURL() + createBody := `<