diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d64b772..816629362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Enhancements +* Add `Projects` interface with `list`, `read`, `update`, `delete` methods and integration tests by @hs26gill and @mwudka [#564](https://github.com/hashicorp/go-tfe/pull/564) * Add `NotificationTriggerAssessmentCheckFailed` notification trigger type by @rexredinger [#549](https://github.com/hashicorp/go-tfe/pull/549) * Add `RemoteTFEVersion()` to the `Client` interface, which exposes the `X-TFE-Version` header set by a remote TFE instance by @sebasslash [#563](https://github.com/hashicorp/go-tfe/pull/563) * Validate the module version as a version instead of an ID [#409](https://github.com/hashicorp/go-tfe/pull/409) diff --git a/README.md b/README.md index 317dd0fde..5263a6471 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ This API client covers most of the existing Terraform Cloud API calls and is upd - [x] Providers - [x] Provider Provider Versions and Platforms - [x] GPG Keys +- [x] Projects - [x] Runs - [x] Run Tasks - [ ] Run Tasks Integration diff --git a/errors.go b/errors.go index 8a4372562..d57f5840b 100644 --- a/errors.go +++ b/errors.go @@ -84,6 +84,8 @@ var ( ErrInvalidProjectID = errors.New("invalid value for project ID") + ErrInvalidPagination = errors.New("invalid value for page size or number") + ErrInvalidRunTaskCategory = errors.New(`category must be "task"`) ErrInvalidRunTaskID = errors.New("invalid value for run task ID") diff --git a/helper_test.go b/helper_test.go index 0524b9ec1..287b9c543 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1874,7 +1874,7 @@ func createProject(t *testing.T, client *Client, org *Organization) (*Project, f ctx := context.Background() p, err := client.Projects.Create(ctx, org.Name, ProjectCreateOptions{ - Name: String("test_project"), + Name: String(randomStringWithoutSpecialChar(t)), }) if err != nil { t.Fatal(err) @@ -1997,6 +1997,24 @@ func randomString(t *testing.T) string { return v } +func randomStringWithoutSpecialChar(t *testing.T) string { + v, err := uuid.GenerateUUID() + if err != nil { + t.Fatal(err) + } + uuidWithoutHyphens := strings.Replace(v, "-", "", -1) + return uuidWithoutHyphens +} + +func containsProject(pl []*Project, str string) bool { + for _, p := range pl { + if p.Name == str { + return true + } + } + return false +} + func randomSemver(t *testing.T) string { return fmt.Sprintf("%d.%d.%d", rand.Intn(99)+3, rand.Intn(99)+1, rand.Intn(99)+1) } diff --git a/projects.go b/projects.go index 5de5ab62c..73ba6cd26 100644 --- a/projects.go +++ b/projects.go @@ -3,7 +3,6 @@ package tfe import ( "context" "fmt" - "log" "net/url" ) @@ -16,7 +15,7 @@ var _ Projects = (*projects)(nil) // TFE API docs: (TODO: ADD DOCS URL) type Projects interface { // List all projects in the given organization - //List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error) + List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error) // Create a new project. Create(ctx context.Context, organization string, options ProjectCreateOptions) (*Project, error) @@ -51,12 +50,10 @@ type Project struct { Organization *Organization `jsonapi:"relation,organization"` } -//// ProjectListOptions represents the options for listing projects -//type ProjectListOptions struct { -// ListOptions -// -// // Add more list options here -//} +// ProjectListOptions represents the options for listing projects +type ProjectListOptions struct { + ListOptions +} // ProjectCreateOptions represents the options for creating a project type ProjectCreateOptions struct { @@ -83,9 +80,37 @@ type ProjectUpdateOptions struct { } // List all projects. -//func List(ctx context.Context, options *ProjectListOptions) (*ProjectList, error) { -// panic("not yet implemented") -//} +func (s *projects) List(ctx context.Context, organization string, options *ProjectListOptions) (*ProjectList, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("organizations/%s/projects", url.QueryEscape(organization)) + req, err := s.client.NewRequest("GET", u, options) + if err != nil { + return nil, err + } + + p := &ProjectList{} + err = req.Do(ctx, p) + if err != nil { + return nil, err + } + + return p, nil +} + +func (o *ProjectListOptions) valid() error { + if o == nil || o.PageNumber == 0 || o.PageSize == 0 { + return ErrInvalidPagination + } + + return nil +} // Create a project with the given options func (s *projects) Create(ctx context.Context, organization string, options ProjectCreateOptions) (*Project, error) { @@ -105,8 +130,6 @@ func (s *projects) Create(ctx context.Context, organization string, options Proj p := &Project{} err = req.Do(ctx, p) - log.Println(ctx) - log.Println(p) if err != nil { return nil, err } diff --git a/projects_integration_test.go b/projects_integration_test.go index c71cbd5d3..8bc4ebe48 100644 --- a/projects_integration_test.go +++ b/projects_integration_test.go @@ -11,20 +11,52 @@ import ( "github.com/stretchr/testify/require" ) -//func TestProjectsList(t *testing.T) { -// skipIfNotCINode(t) -// -// client := testClient(t) -// ctx := context.Background() -// -// // Create your test helper resources here -// t.Run("test not yet implemented", func(t *testing.T) { -// require.NotNil(t, nil) -// }) -//} +func TestProjectsList(t *testing.T) { + skipIfNotCINode(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + pTest1, pTestCleanup := createProject(t, client, orgTest) + defer pTestCleanup() + + pTest2, pTestCleanup := createProject(t, client, orgTest) + defer pTestCleanup() + + t.Run("with invalid options", func(t *testing.T) { + pl, err := client.Projects.List(ctx, orgTest.Name, nil) + assert.Nil(t, pl) + assert.EqualError(t, err, ErrInvalidPagination.Error()) + }) + + t.Run("with list options", func(t *testing.T) { + pl, err := client.Projects.List(ctx, orgTest.Name, &ProjectListOptions{ + ListOptions: ListOptions{ + PageNumber: 1, + PageSize: 100, + }, + }) + require.NoError(t, err) + assert.Contains(t, pl.Items, pTest1) + assert.Contains(t, pl.Items, pTest2) + assert.Equal(t, true, containsProject(pl.Items, "Default Project")) + assert.Equal(t, 3, len(pl.Items)) + }) + + t.Run("without a valid organization", func(t *testing.T) { + pl, err := client.Projects.List(ctx, badIdentifier, nil) + assert.Nil(t, pl) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) +} func TestProjectsRead(t *testing.T) { skipIfNotCINode(t) + skipIfBeta(t) client := testClient(t) ctx := context.Background() @@ -57,6 +89,7 @@ func TestProjectsRead(t *testing.T) { func TestProjectsCreate(t *testing.T) { skipIfNotCINode(t) + skipIfBeta(t) client := testClient(t) ctx := context.Background() @@ -64,7 +97,6 @@ func TestProjectsCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - // Create your test helper resources here t.Run("with valid options", func(t *testing.T) { options := ProjectCreateOptions{ Name: String("foo"), @@ -73,7 +105,6 @@ func TestProjectsCreate(t *testing.T) { w, err := client.Projects.Create(ctx, orgTest.Name, options) require.NoError(t, err) - // Get a refreshed view from the API. refreshed, err := client.Projects.Read(ctx, w.ID) require.NoError(t, err) @@ -87,13 +118,13 @@ func TestProjectsCreate(t *testing.T) { }) t.Run("when options is missing name", func(t *testing.T) { - w, err := client.Projects.Create(ctx, "foo", ProjectCreateOptions{}) + w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{}) assert.Nil(t, w) assert.EqualError(t, err, ErrRequiredName.Error()) }) t.Run("when options has an invalid name", func(t *testing.T) { - w, err := client.Projects.Create(ctx, "foo", ProjectCreateOptions{ + w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{ Name: String(badIdentifier), }) assert.Nil(t, w) @@ -111,6 +142,7 @@ func TestProjectsCreate(t *testing.T) { func TestProjectsUpdate(t *testing.T) { skipIfNotCINode(t) + skipIfBeta(t) client := testClient(t) ctx := context.Background() @@ -148,3 +180,36 @@ func TestProjectsUpdate(t *testing.T) { assert.EqualError(t, err, ErrInvalidProjectID.Error()) }) } + +func TestProjectsDelete(t *testing.T) { + skipIfNotCINode(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + pTest, pTestCleanup := createProject(t, client, orgTest) + defer pTestCleanup() + + t.Run("with valid options", func(t *testing.T) { + err := client.Projects.Delete(ctx, pTest.ID) + require.NoError(t, err) + + // Try loading the project - it should fail. + _, err = client.Projects.Read(ctx, pTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the project does not exist", func(t *testing.T) { + err := client.Projects.Delete(ctx, pTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the project ID is invalid", func(t *testing.T) { + err := client.Projects.Delete(ctx, badIdentifier) + assert.EqualError(t, err, ErrInvalidProjectID.Error()) + }) +}