Skip to content

Commit ebd5230

Browse files
author
Nicholas M. Iodice
authored
Add testing for project Create & Read APIs, as well as expand/flatten project code (#57)
* Adding more comprehensive tests for project create API * Adding tests for projectRead(), flattenProject() and expandProject() * Adding another test & refactoring the async wait code
1 parent cda69b0 commit ebd5230

File tree

3 files changed

+283
-45
lines changed

3 files changed

+283
-45
lines changed

scripts/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function unittest() {
1414
info "Executing unit tests"
1515
(
1616
cd "$SOURCE_DIR"
17-
go test ./... || fatal "Build finished in error due to failed unit tests"
17+
go test -v ./... || fatal "Build finished in error due to failed unit tests"
1818
)
1919
}
2020

src/resource_project.go

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/microsoft/azure-devops-go-api/azuredevops/operations"
1515
)
1616

17+
var projectCreateTimeoutSeconds time.Duration = 30
18+
1719
func resourceProject() *schema.Resource {
1820
return &schema.Resource{
1921
Create: resourceProjectCreate,
@@ -66,7 +68,7 @@ func resourceProjectCreate(d *schema.ResourceData, m interface{}) error {
6668
return fmt.Errorf("Error converting terraform data model to AzDO project reference: %+v", err)
6769
}
6870

69-
err = createProject(clients, project)
71+
err = createProject(clients, project, projectCreateTimeoutSeconds)
7072
if err != nil {
7173
return fmt.Errorf("Error creating project in Azure DevOps: %+v", err)
7274
}
@@ -76,63 +78,55 @@ func resourceProjectCreate(d *schema.ResourceData, m interface{}) error {
7678
}
7779

7880
// Make API call to create the project and wait for an async success/fail response from the service
79-
func createProject(clients *aggregatedClient, project *core.TeamProject) error {
81+
func createProject(clients *aggregatedClient, project *core.TeamProject, timeoutSeconds time.Duration) error {
8082
operationRef, err := clients.CoreClient.QueueCreateProject(clients.ctx, core.QueueCreateProjectArgs{ProjectToCreate: project})
8183
if err != nil {
8284
return err
8385
}
8486

85-
err = waitForAsyncOperationSuccess(clients, operationRef)
87+
err = waitForAsyncOperationSuccess(clients, operationRef, timeoutSeconds)
8688
if err != nil {
8789
return err
8890
}
8991

9092
return nil
9193
}
9294

93-
func waitForAsyncOperationSuccess(clients *aggregatedClient, operationRef *operations.OperationReference) error {
94-
maxAttempts := 30
95-
currentAttempt := 1
96-
97-
for currentAttempt <= maxAttempts {
98-
result, err := clients.OperationsClient.GetOperation(clients.ctx, operations.GetOperationArgs{
99-
OperationId: operationRef.Id,
100-
PluginId: operationRef.PluginId,
101-
})
102-
103-
if err != nil {
104-
return err
105-
}
106-
107-
if *result.Status == operations.OperationStatusValues.Succeeded {
108-
// Sometimes without the sleep, the subsequent operations won't find the project...
109-
time.Sleep(2 * time.Second)
110-
return nil
95+
func waitForAsyncOperationSuccess(clients *aggregatedClient, operationRef *operations.OperationReference, timeoutSeconds time.Duration) error {
96+
timeout := time.After(timeoutSeconds * time.Second)
97+
ticker := time.NewTicker(1 * time.Second)
98+
defer ticker.Stop()
99+
100+
for {
101+
select {
102+
case <- ticker.C:
103+
result, err := clients.OperationsClient.GetOperation(clients.ctx, operations.GetOperationArgs{
104+
OperationId: operationRef.Id,
105+
PluginId: operationRef.PluginId,
106+
})
107+
108+
if err != nil {
109+
return err
110+
}
111+
112+
if *result.Status == operations.OperationStatusValues.Succeeded {
113+
return nil
114+
}
115+
case <- timeout:
116+
return fmt.Errorf("Operation was not successful after %d seconds", timeoutSeconds)
111117
}
112-
113-
currentAttempt++
114-
time.Sleep(1 * time.Second)
115118
}
116-
117-
return fmt.Errorf("Operation was not successful after %d attempts", maxAttempts)
118119
}
119120

121+
120122
func resourceProjectRead(d *schema.ResourceData, m interface{}) error {
121123
clients := m.(*aggregatedClient)
122124

123-
projectID := d.Id()
124-
if projectID == "" {
125-
// project name can be used as an identifier for the core.Projects API:
126-
// https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/get?view=azure-devops-rest-5.0
127-
projectID = d.Get("project_name").(string)
128-
}
129-
project, err := clients.CoreClient.GetProject(clients.ctx, core.GetProjectArgs{
130-
ProjectId: &projectID,
131-
IncludeCapabilities: converter.Bool(true),
132-
IncludeHistory: converter.Bool(false),
133-
})
125+
id := d.Id()
126+
name := d.Get("project_name").(string)
127+
project, err := projectRead(clients, id, name)
134128
if err != nil {
135-
return fmt.Errorf("Error looking up project given ID: %v %v", projectID, err)
129+
return fmt.Errorf("Error looking up project with ID %s and Name %s", id, name)
136130
}
137131

138132
err = flattenProject(clients, d, project)
@@ -142,6 +136,22 @@ func resourceProjectRead(d *schema.ResourceData, m interface{}) error {
142136
return nil
143137
}
144138

139+
// Lookup a project using the ID, or name if the ID is not set. Note, usage of the name in place
140+
// of the ID is an explicitly stated supported behavior:
141+
// https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/get?view=azure-devops-rest-5.0
142+
func projectRead(clients *aggregatedClient, projectID string, projectName string) (*core.TeamProject, error) {
143+
identifier := projectID
144+
if identifier == "" {
145+
identifier = projectName
146+
}
147+
148+
return clients.CoreClient.GetProject(clients.ctx, core.GetProjectArgs{
149+
ProjectId: &identifier,
150+
IncludeCapabilities: converter.Bool(true),
151+
IncludeHistory: converter.Bool(false),
152+
})
153+
}
154+
145155
func resourceProjectUpdate(d *schema.ResourceData, m interface{}) error {
146156
return resourceProjectRead(d, m)
147157
}

0 commit comments

Comments
 (0)