Skip to content

Commit

Permalink
refactor service hook webhook resource, add acceptance tests and docs…
Browse files Browse the repository at this point in the history
… for azuredevops_servicehook_webhook resource
  • Loading branch information
iswym committed Oct 9, 2021
1 parent 4b8fe82 commit 6856988
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// +build all resource_servicehook_webhook
// +build !exclude_servicehook

package acceptancetests

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/acceptancetests/testutils"
)

func TestAccServiceHookWebhook_basic(t *testing.T) {
projectName := testutils.GenerateResourceName()
eventType := "git.push"
url := "https://webhooks.org"

resourceType := "azuredevops_servicehook_webhook"
tfSvcHookNode := resourceType + ".test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckServiceHookWebhookDestroyed(resourceType),
Steps: []resource.TestStep{
{
Config: hclSvcHookWebhookResourceBasic(projectName, eventType, url),
Check: resource.ComposeTestCheckFunc(
testutils.CheckServiceHookWebhookExistsWithEventTypeAndUrl(tfSvcHookNode, eventType, url),
resource.TestCheckResourceAttrSet(tfSvcHookNode, "project_id"),
resource.TestCheckResourceAttr(tfSvcHookNode, "event_type", eventType),
resource.TestCheckResourceAttr(tfSvcHookNode, "url", url),
),
},
},
})
}

func TestAccServiceHookWebhook_complete(t *testing.T) {
projectName := testutils.GenerateResourceName()
repositoryName := testutils.GenerateResourceName()
eventType := "git.push"
url := "https://webhooks.org"
basicAuth := BasicAuth{
Username: "some_username",
Password: "some_password",
}
httpHeaders := map[string]string{
"Authorization": "Bearer bearing",
"X-My-Secret-Token": "whatever",
}

resourceType := "azuredevops_servicehook_webhook"
tfSvcHookNode := resourceType + ".test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckServiceHookWebhookDestroyed(resourceType),
Steps: []resource.TestStep{
{
Config: hclSvcHookWebhookResourceComplete(projectName, repositoryName, eventType, url, basicAuth, httpHeaders),
Check: resource.ComposeTestCheckFunc(
testutils.CheckServiceHookWebhookExistsWithEventTypeAndUrl(tfSvcHookNode, eventType, url),
resource.TestCheckResourceAttrSet(tfSvcHookNode, "project_id"),
resource.TestCheckResourceAttr(tfSvcHookNode, "event_type", eventType),
resource.TestCheckResourceAttr(tfSvcHookNode, "url", url),
),
},
},
})
}

func TestAccServiceHookWebhook_update(t *testing.T) {
projectName := testutils.GenerateResourceName()
eventType := "git.push"
url := "https://webhooks.org"

updatedEventType := "git.pullrequest.created"
updatedUrl := "https://webhooks2.org"

resourceType := "azuredevops_servicehook_webhook"
tfSvcHookNode := resourceType + ".test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.GetProviders(),
CheckDestroy: testutils.CheckServiceHookWebhookDestroyed(resourceType),
Steps: []resource.TestStep{
{
Config: hclSvcHookWebhookResourceBasic(projectName, eventType, url),
Check: resource.ComposeTestCheckFunc(
testutils.CheckServiceHookWebhookExistsWithEventTypeAndUrl(tfSvcHookNode, eventType, url),
resource.TestCheckResourceAttrSet(tfSvcHookNode, "project_id"),
resource.TestCheckResourceAttr(tfSvcHookNode, "event_type", eventType),
resource.TestCheckResourceAttr(tfSvcHookNode, "url", url),
),
},
{
Config: hclSvcHookWebhookResourceUpdate(projectName, updatedEventType, updatedUrl),
Check: resource.ComposeTestCheckFunc(
testutils.CheckServiceHookWebhookExistsWithEventTypeAndUrl(tfSvcHookNode, updatedEventType, updatedUrl),
resource.TestCheckResourceAttrSet(tfSvcHookNode, "project_id"),
resource.TestCheckResourceAttr(tfSvcHookNode, "event_type", updatedEventType),
resource.TestCheckResourceAttr(tfSvcHookNode, "url", updatedUrl),
),
},
},
})
}

func hclSvcHookWebhookResourceBasic(projectName string, eventType string, url string) string {
serviceHookResource := fmt.Sprintf(`
resource "azuredevops_servicehook_webhook" "test" {
project_id = azuredevops_project.project.id
event_type = "%s"
url = "%s"
}`, eventType, url)

projectResource := testutils.HclProjectResource(projectName)
return fmt.Sprintf("%s\n%s", projectResource, serviceHookResource)
}

type BasicAuth struct {
Username string
Password string
}

func hclSvcHookWebhookResourceComplete(projectName string, repositoryName string, eventType string, url string, basicAuth BasicAuth, httpHeaders map[string]string) string {
headers := []string{}
for key, val := range httpHeaders {
headers = append(headers, fmt.Sprintf("%s = \"%s\"", key, val))
}

serviceHookResource := fmt.Sprintf(`
resource "azuredevops_servicehook_webhook" "test" {
project_id = azuredevops_project.project.id
event_type = "%s"
url = "%s"
basic_auth {
username = "%s"
password = "%s"
}
filters = {
repository = azuredevops_git_repository.repository.id
}
http_headers = {
%s
}
}`, eventType, url, basicAuth.Username, basicAuth.Password, strings.Join(headers, "\n"))

projectAndRepositoryResource := testutils.HclGitRepoResource(projectName, repositoryName, "Clean")
return fmt.Sprintf("%s\n%s", projectAndRepositoryResource, serviceHookResource)
}

func hclSvcHookWebhookResourceUpdate(projectName string, eventType string, url string) string {
serviceHookResource := fmt.Sprintf(`
resource "azuredevops_servicehook_webhook" "test" {
project_id = azuredevops_project.project.id
event_type = "%s"
url = "%s"
}`, eventType, url)

projectResource := testutils.HclProjectResource(projectName)
return fmt.Sprintf("%s\n%s", projectResource, serviceHookResource)
}
69 changes: 69 additions & 0 deletions azuredevops/internal/acceptancetests/testutils/servicehook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package testutils

import (
"fmt"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/microsoft/azure-devops-go-api/azuredevops/servicehooks"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client"
)

// CheckServiceHookWebhookExistsWithEventTypeAndUrl verifies that a service hook webhook exists in the state,
// and that it has the expected event type and url when compared against the data in Azure DevOps.
func CheckServiceHookWebhookExistsWithEventTypeAndUrl(tfNode string, expectedEventType string, expectedUrl string) resource.TestCheckFunc {
return func(s *terraform.State) error {
resourceState, ok := s.RootModule().Resources[tfNode]
if !ok {
return fmt.Errorf("Did not find a service hook webhook in the state")
}

subscription, err := getSvcHookWebhookFromState(resourceState)
if err != nil {
return err
}

if *subscription.EventType != expectedEventType {
return fmt.Errorf("Service Hook webhook has event type=%s, but expected event type=%s", *subscription.EventType, expectedEventType)
}

if (*subscription.ConsumerInputs)["url"] != expectedUrl {
return fmt.Errorf("Service Hook webhook has url=%s, but expected url=%s", *subscription.Url, expectedUrl)
}

return nil
}
}

// CheckServiceHookWebhookDestroyed verifies that all service hook webhoks the state are destroyed.
// This will be invoked *after* terraform destroys the resource but *before* the state is wiped clean.
func CheckServiceHookWebhookDestroyed(resourceType string) resource.TestCheckFunc {
return func(s *terraform.State) error {
for _, resource := range s.RootModule().Resources {
if resource.Type != resourceType {
continue
}

// indicates the resource exists - this should fail the test
if _, err := getSvcHookWebhookFromState(resource); err == nil {
return fmt.Errorf("Unexpectedly found a service hook webhook that should have been deleted")
}
}

return nil
}
}

// given a resource from the state, return a service hook webhook (and error)
func getSvcHookWebhookFromState(resource *terraform.ResourceState) (*servicehooks.Subscription, error) {
serviceHookWebhookDefID, err := uuid.Parse(resource.Primary.ID)
if err != nil {
return nil, err
}

clients := GetProvider().Meta().(*client.AggregatedClient)
return clients.ServiceHooksClient.GetSubscription(clients.Ctx, servicehooks.GetSubscriptionArgs{
SubscriptionId: &serviceHookWebhookDefID,
})
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package servicehooks
package servicehook

import (
"bufio"
Expand Down Expand Up @@ -31,7 +31,7 @@ func ResourceServiceHookWebhook() *schema.Resource {
},
"url": {
Type: schema.TypeString,
Optional: true,
Required: true,
ValidateFunc: validation.IsURLWithHTTPS,
},
"event_type": {
Expand Down Expand Up @@ -165,7 +165,9 @@ func createResourceWebhookRead(afterCreateOrUpdate bool) func(d *schema.Resource
}
filters[key] = value
}
d.Set("filters", filters)
if len(filters) != 0 {
d.Set("filters", filters)
}

return nil
}
Expand Down
4 changes: 2 additions & 2 deletions azuredevops/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/policy/branch"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/policy/repository"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/serviceendpoint"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/servicehooks"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/servicehook"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/taskagent"
"github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/service/workitemtracking"
)
Expand Down Expand Up @@ -58,7 +58,7 @@ func Provider() *schema.Provider {
"azuredevops_serviceendpoint_npm": serviceendpoint.ResourceServiceEndpointNpm(),
"azuredevops_serviceendpoint_generic": serviceendpoint.ResourceServiceEndpointGeneric(),
"azuredevops_serviceendpoint_generic_git": serviceendpoint.ResourceServiceEndpointGenericGit(),
"azuredevops_servicehooks_webhook": servicehooks.ResourceServiceHookWebhook(),
"azuredevops_servicehook_webhook": servicehook.ResourceServiceHookWebhook(),
"azuredevops_git_repository": git.ResourceGitRepository(),
"azuredevops_git_repository_file": git.ResourceGitRepositoryFile(),
"azuredevops_user_entitlement": memberentitlementmanagement.ResourceUserEntitlement(),
Expand Down
2 changes: 1 addition & 1 deletion azuredevops/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestProvider_HasChildResources(t *testing.T) {
"azuredevops_serviceendpoint_npm",
"azuredevops_serviceendpoint_generic",
"azuredevops_serviceendpoint_generic_git",
"azuredevops_servicehooks_webhook",
"azuredevops_servicehook_webhook",
"azuredevops_variable_group",
"azuredevops_repository_policy_author_email_pattern",
"azuredevops_repository_policy_case_enforcement",
Expand Down
78 changes: 78 additions & 0 deletions website/docs/r/servicehook_webhook.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
layout: "azuredevops"
page_title: "AzureDevops: azuredevops_servicehook_webhook"
description: |-
Manages a Webhook service hook Azure DevOps organization.
---

# azuredevops_servicehook_webhook (Resource)

Manages a Webhook service hook Azure DevOps organization.

## Example Usage

```hcl
resource "azuredevops_project" "project" {
name = "Sample Project"
visibility = "private"
version_control = "Git"
work_item_template = "Agile"
}
resource "azuredevops_git_repository" "repo" {
project_id = azuredevops_project.project.id
name = "Sample Empty Git Repository"
initialization {
init_type = "Clean"
}
}
resource "azuredevops_servicehook_webhook" "webhook" {
project_id = azuredevops_project.project.id
event_type = "git.push"
url = "https://my-webhooks.org"
# optional
basic_auth {
username = "my_username"
password = "my_password"
}
# optional
filters = {
repository = azuredevops_git_repository.repo.id
}
# optional
http_headers = {
Authorization = "Bearer bearing"
X-My-Header = "header value"
}
}
```

## Argument Reference

The following arguments are supported:

- `project_id` - (Required) The project ID or project name.
- `event_type` - (Required) Event type.
- `url` - (Required) The url of the hook to invoke.
- `basic_auth` - (Optional) Basic authentication.
- `username`
- `password`
- `filters` - (Optional) Filters that depend on event type.
- `http_headers` - (Optional) HTTP headers included in request.

## Attributes Reference

The following attributes are exported:

- `id` - The ID of the service hook webhook.
- `project_id` - (Required) The project ID or project name.
- `event_type` - (Required) Event type.
- `url` - (Required) The url of the hook to invoke.

## Relevant Links

- [Azure DevOps Service REST API 5.1 - Service hooks](https://docs.microsoft.com/en-us/rest/api/azure/devops/hooks/?view=azure-devops-rest-5.1)

0 comments on commit 6856988

Please sign in to comment.