Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comments object #355

Merged
merged 4 commits into from Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
129 changes: 129 additions & 0 deletions comment.go
@@ -0,0 +1,129 @@
package tfe

import (
"context"
"fmt"
"net/url"
)

// Compile-time proof of interface implementation.
var _ Comments = (*comments)(nil)

// Comments describes all the comment related methods that the
// Terraform Enterprise API supports.
//
// TFE API docs:
// https://www.terraform.io/docs/cloud/api/comments.html
type Comments interface {
// List all comments of the given run.
List(ctx context.Context, runID string) (*CommentList, error)

// Read a comment by its ID.
Read(ctx context.Context, commentID string) (*Comment, error)

// Create a new comment with the given options.
Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error)
}

// Comments implements Comments.
type comments struct {
client *Client
}

// CommentList represents a list of comments.
type CommentList struct {
*Pagination
Items []*Comment
}

// Comment represents a Terraform Enterprise comment.
type Comment struct {
ID string `jsonapi:"primary,comments"`
Body string `jsonapi:"attr,body"`
}

type CommentCreateOptions struct {
// Type is a public field utilized by JSON:API to
// set the resource type via the field tag.
// It is not a user-defined value and does not need to be set.
// https://jsonapi.org/format/#crud-creating
Type string `jsonapi:"primary,comments"`

// Required: Body of the comment.
Body string `jsonapi:"attr,body"`
}

// List all comments of the given run.
func (s *comments) List(ctx context.Context, runID string) (*CommentList, error) {
if !validStringID(&runID) {
return nil, ErrInvalidRunID
}

u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

cl := &CommentList{}
err = s.client.do(ctx, req, cl)
if err != nil {
return nil, err
}

return cl, nil
}

// Create a new comment with the given options.
func (s *comments) Create(ctx context.Context, runID string, options CommentCreateOptions) (*Comment, error) {
if err := options.valid(); err != nil {
return nil, err
}

if !validStringID(&runID) {
return nil, ErrInvalidRunID
}

u := fmt.Sprintf("runs/%s/comments", url.QueryEscape(runID))
req, err := s.client.newRequest("POST", u, &options)
if err != nil {
return nil, err
}

comm := &Comment{}
err = s.client.do(ctx, req, comm)
if err != nil {
return nil, err
}

return comm, err
}

// Read a comment by its ID.
func (s *comments) Read(ctx context.Context, commentID string) (*Comment, error) {
if !validStringID(&commentID) {
return nil, ErrInvalidCommentID
}

u := fmt.Sprintf("comments/%s", url.QueryEscape(commentID))
req, err := s.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

comm := &Comment{}
err = s.client.do(ctx, req, comm)
if err != nil {
return nil, err
}

return comm, nil
}

func (o CommentCreateOptions) valid() error {
if !validString(&o.Body) {
return ErrInvalidCommentBody
}

return nil
}
77 changes: 77 additions & 0 deletions comment_integration_test.go
@@ -0,0 +1,77 @@
//go:build integration
// +build integration

package tfe

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCommentsList(t *testing.T) {
client := testClient(t)
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()

wTest1, wTest1Cleanup := createWorkspace(t, client, orgTest)
defer wTest1Cleanup()

rTest, rTest1Cleanup := createRun(t, client, wTest1)
defer rTest1Cleanup()
commentBody1 := "1st comment test"
commentBody2 := "2nd comment test"

t.Run("without comments", func(t *testing.T) {
_, err := client.Comments.List(ctx, rTest.ID)
require.NoError(t, err)
})

t.Run("without a valid run", func(t *testing.T) {
cl, err := client.Comments.List(ctx, badIdentifier)
assert.Nil(t, cl)
assert.EqualError(t, err, ErrInvalidRunID.Error())
})

t.Run("create a comment", func(t *testing.T) {
options := CommentCreateOptions{
Body: commentBody1,
}
cl, err := client.Comments.Create(ctx, rTest.ID, options)
require.NoError(t, err)
assert.Equal(t, commentBody1, cl.Body)
})

t.Run("create 2nd comment", func(t *testing.T) {
options := CommentCreateOptions{
Body: commentBody2,
}
cl, err := client.Comments.Create(ctx, rTest.ID, options)
require.NoError(t, err)
assert.Equal(t, commentBody2, cl.Body)
})

t.Run("list comments", func(t *testing.T) {
commentsList, err := client.Comments.List(ctx, rTest.ID)
require.NoError(t, err)
assert.Len(t, commentsList.Items, 2)
assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody1))
assert.Equal(t, true, commentItemsContainsBody(commentsList.Items, commentBody2))
})
}

func commentItemsContainsBody(items []*Comment, body string) bool {
hasBody := false
for _, item := range items {
if item.Body == body {
hasBody = true
break
}
}

return hasBody
}
6 changes: 6 additions & 0 deletions errors.go
Expand Up @@ -145,6 +145,10 @@ var (
ErrInvalidVariableID = errors.New("invalid value for variable ID")

ErrInvalidNotificationTrigger = errors.New("invalid value for notification trigger")

ErrInvalidCommentID = errors.New("invalid value for comment ID")

ErrInvalidCommentBody = errors.New("invalid value for comment body")
)

// Missing values for required field/option
Expand Down Expand Up @@ -248,4 +252,6 @@ var (
ErrRequiredOnlyOneField = errors.New("only one of usernames or organization membership ids can be provided")

ErrRequiredUsernameOrMembershipIds = errors.New("usernames or organization membership ids are required")

ErrCommentBody = errors.New("comment body is required")
)
1 change: 1 addition & 0 deletions run.go
Expand Up @@ -118,6 +118,7 @@ type Run struct {
PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"`
TaskStages []*TaskStage `jsonapi:"relation,task-stages,omitempty"`
Workspace *Workspace `jsonapi:"relation,workspace"`
Comments []*Comment `jsonapi:"relation,comments"`
}

// RunActions represents the run actions.
Expand Down
2 changes: 2 additions & 0 deletions tfe.go
Expand Up @@ -110,6 +110,7 @@ type Client struct {
AgentPools AgentPools
AgentTokens AgentTokens
Applies Applies
Comments Comments
ConfigurationVersions ConfigurationVersions
CostEstimates CostEstimates
NotificationConfigurations NotificationConfigurations
Expand Down Expand Up @@ -251,6 +252,7 @@ func NewClient(cfg *Config) (*Client, error) {
client.AgentPools = &agentPools{client: client}
client.AgentTokens = &agentTokens{client: client}
client.Applies = &applies{client: client}
client.Comments = &comments{client: client}
client.ConfigurationVersions = &configurationVersions{client: client}
client.CostEstimates = &costEstimates{client: client}
client.NotificationConfigurations = &notificationConfigurations{client: client}
Expand Down