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

✨ implement ListIssues and GetCreatedAt for Azure DevOps #4419

Merged
merged 3 commits into from
Dec 3, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions clients/azuredevopsrepo/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package azuredevopsrepo

import (
"context"
"fmt"
"sync"
"time"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7/audit"
)

type auditHandler struct {
auditClient audit.Client
once *sync.Once
ctx context.Context
errSetup error
repourl *Repo
createdAt time.Time
queryLog fnQueryLog
}

func (a *auditHandler) init(ctx context.Context, repourl *Repo) {
a.ctx = ctx
a.errSetup = nil
a.once = new(sync.Once)
a.repourl = repourl
a.queryLog = a.auditClient.QueryLog

Check warning on line 41 in clients/azuredevopsrepo/audit.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/audit.go#L36-L41

Added lines #L36 - L41 were not covered by tests
}

type (
fnQueryLog func(ctx context.Context, args audit.QueryLogArgs) (*audit.AuditLogQueryResult, error)
)

func (a *auditHandler) setup() error {
a.once.Do(func() {
continuationToken := ""
for {
auditLog, err := a.queryLog(a.ctx, audit.QueryLogArgs{
ContinuationToken: &continuationToken,
})
if err != nil {
a.errSetup = fmt.Errorf("error querying audit log: %w", err)
return
}

// Check if Git.CreateRepo event exists for the repository
for i := range *auditLog.DecoratedAuditLogEntries {
entry := &(*auditLog.DecoratedAuditLogEntries)[i]
if *entry.ActionId == "Git.CreateRepo" &&
*entry.ProjectName == a.repourl.project &&
(*entry.Data)["RepoName"] == a.repourl.name {
a.createdAt = entry.Timestamp.Time
return
}
}

if *auditLog.HasMore {
continuationToken = *auditLog.ContinuationToken
} else {
return
}

Check warning on line 75 in clients/azuredevopsrepo/audit.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/audit.go#L71-L75

Added lines #L71 - L75 were not covered by tests
}
})
return a.errSetup
}

func (a *auditHandler) getRepsitoryCreatedAt() (time.Time, error) {
if err := a.setup(); err != nil {
return time.Time{}, fmt.Errorf("error during auditHandler.setup: %w", err)
}

Check warning on line 84 in clients/azuredevopsrepo/audit.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/audit.go#L81-L84

Added lines #L81 - L84 were not covered by tests

return a.createdAt, nil

Check warning on line 86 in clients/azuredevopsrepo/audit.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/audit.go#L86

Added line #L86 was not covered by tests
}
89 changes: 89 additions & 0 deletions clients/azuredevopsrepo/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2024 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package azuredevopsrepo

import (
"context"
"errors"
"sync"
"testing"
"time"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/audit"
)

func Test_auditHandler_setup(t *testing.T) {
t.Parallel()
tests := []struct {
queryLog fnQueryLog
createdAt time.Time
name string
wantErr bool
}{
{
name: "successful setup",
queryLog: func(ctx context.Context, args audit.QueryLogArgs) (*audit.AuditLogQueryResult, error) {
return &audit.AuditLogQueryResult{
HasMore: new(bool),
ContinuationToken: new(string),
DecoratedAuditLogEntries: &[]audit.DecoratedAuditLogEntry{
{
ActionId: strptr("Git.CreateRepo"),
ProjectName: strptr("test-project"),
Data: &map[string]interface{}{"RepoName": "test-repo"},
Timestamp: &azuredevops.Time{Time: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)},
},
},
}, nil
},
wantErr: false,
createdAt: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC),
},
{
name: "query log error",
queryLog: func(ctx context.Context, args audit.QueryLogArgs) (*audit.AuditLogQueryResult, error) {
return nil, errors.New("query log error")
},
wantErr: true,
createdAt: time.Time{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
handler := &auditHandler{
once: new(sync.Once),
queryLog: tt.queryLog,
repourl: &Repo{
project: "test-project",
name: "test-repo",
},
}
err := handler.setup()
if (err != nil) != tt.wantErr {
t.Fatalf("setup() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && !handler.createdAt.Equal(tt.createdAt) {
t.Errorf("setup() createdAt = %v, want %v", handler.createdAt, tt.createdAt)
}
})
}
}

func strptr(s string) *string {
return &s
}
37 changes: 35 additions & 2 deletions clients/azuredevopsrepo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"time"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/audit"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"

"github.com/ossf/scorecard/v5/clients"
)
Expand All @@ -40,8 +42,10 @@
ctx context.Context
repourl *Repo
repo *git.GitRepository
audit *auditHandler
branches *branchesHandler
commits *commitsHandler
workItems *workItemsHandler
zip *zipHandler
commitDepth int
}
Expand Down Expand Up @@ -81,10 +85,14 @@
commitSHA: commitSHA,
}

c.audit.init(c.ctx, c.repourl)

Check warning on line 89 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L88-L89

Added lines #L88 - L89 were not covered by tests
c.branches.init(c.ctx, c.repourl)

c.commits.init(c.ctx, c.repourl, c.commitDepth)

c.workItems.init(c.ctx, c.repourl)

Check warning on line 95 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L94-L95

Added lines #L94 - L95 were not covered by tests
c.zip.init(c.ctx, c.repourl)

return nil
Expand Down Expand Up @@ -115,7 +123,16 @@
}

func (c *Client) GetCreatedAt() (time.Time, error) {
return time.Time{}, clients.ErrUnsupportedFeature
createdAt, err := c.audit.getRepsitoryCreatedAt()
if err != nil {
return time.Time{}, err
}

Check warning on line 129 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L126-L129

Added lines #L126 - L129 were not covered by tests

// The audit log may not be enabled on the repository
if createdAt.IsZero() {
return c.commits.getFirstCommitCreatedAt()
}
return createdAt, nil

Check warning on line 135 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L132-L135

Added lines #L132 - L135 were not covered by tests
}

func (c *Client) GetDefaultBranchName() (string, error) {
Expand All @@ -139,7 +156,7 @@
}

func (c *Client) ListIssues() ([]clients.Issue, error) {
return nil, clients.ErrUnsupportedFeature
return c.workItems.listIssues()

Check warning on line 159 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L159

Added line #L159 was not covered by tests
}

func (c *Client) ListLicenses() ([]clients.License, error) {
Expand Down Expand Up @@ -198,20 +215,36 @@

client := connection.GetClientByUrl(url)

auditClient, err := audit.NewClient(ctx, connection)
if err != nil {
return nil, fmt.Errorf("could not create azure devops audit client with error: %w", err)
}

Check warning on line 221 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L218-L221

Added lines #L218 - L221 were not covered by tests

gitClient, err := git.NewClient(ctx, connection)
if err != nil {
return nil, fmt.Errorf("could not create azure devops git client with error: %w", err)
}

workItemsClient, err := workitemtracking.NewClient(ctx, connection)
if err != nil {
return nil, fmt.Errorf("could not create azure devops work item tracking client with error: %w", err)
}

Check warning on line 231 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L228-L231

Added lines #L228 - L231 were not covered by tests

return &Client{
ctx: ctx,
azdoClient: gitClient,
audit: &auditHandler{
auditClient: auditClient,
},

Check warning on line 238 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L236-L238

Added lines #L236 - L238 were not covered by tests
branches: &branchesHandler{
gitClient: gitClient,
},
commits: &commitsHandler{
gitClient: gitClient,
},
workItems: &workItemsHandler{
workItemsClient: workItemsClient,
},

Check warning on line 247 in clients/azuredevopsrepo/client.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/client.go#L245-L247

Added lines #L245 - L247 were not covered by tests
zip: &zipHandler{
client: client,
},
Expand Down
58 changes: 48 additions & 10 deletions clients/azuredevopsrepo/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"errors"
"fmt"
"sync"
"time"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"

Expand All @@ -28,16 +29,18 @@
var errMultiplePullRequests = errors.New("expected 1 pull request for commit, got multiple")

type commitsHandler struct {
gitClient git.Client
ctx context.Context
errSetup error
once *sync.Once
repourl *Repo
commitsRaw *[]git.GitCommitRef
pullRequestsRaw *git.GitPullRequestQuery
getCommits fnGetCommits
getPullRequestQuery fnGetPullRequestQuery
commitDepth int
gitClient git.Client
ctx context.Context
errSetup error
once *sync.Once
repourl *Repo
commitsRaw *[]git.GitCommitRef
pullRequestsRaw *git.GitPullRequestQuery
firstCommitCreatedAt time.Time
getCommits fnGetCommits
getPullRequestQuery fnGetPullRequestQuery
getFirstCommit fnGetFirstCommit
commitDepth int
}

func (handler *commitsHandler) init(ctx context.Context, repourl *Repo, commitDepth int) {
Expand All @@ -48,11 +51,13 @@
handler.commitDepth = commitDepth
handler.getCommits = handler.gitClient.GetCommits
handler.getPullRequestQuery = handler.gitClient.GetPullRequestQuery
handler.getFirstCommit = handler.gitClient.GetCommits

Check warning on line 54 in clients/azuredevopsrepo/commits.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/commits.go#L54

Added line #L54 was not covered by tests
}

type (
fnGetCommits func(ctx context.Context, args git.GetCommitsArgs) (*[]git.GitCommitRef, error)
fnGetPullRequestQuery func(ctx context.Context, args git.GetPullRequestQueryArgs) (*git.GitPullRequestQuery, error)
fnGetFirstCommit func(ctx context.Context, args git.GetCommitsArgs) (*[]git.GitCommitRef, error)
)

func (handler *commitsHandler) setup() error {
Expand Down Expand Up @@ -106,6 +111,31 @@
return
}

switch {
case len(*commits) == 0:
handler.firstCommitCreatedAt = time.Time{}
case len(*commits) < handler.commitDepth:
handler.firstCommitCreatedAt = (*commits)[len(*commits)-1].Committer.Date.Time
default:
firstCommit, err := handler.getFirstCommit(handler.ctx, git.GetCommitsArgs{
RepositoryId: &handler.repourl.id,
SearchCriteria: &git.GitQueryCommitsCriteria{
Top: &[]int{1}[0],
ShowOldestCommitsFirst: &[]bool{true}[0],
ItemVersion: &git.GitVersionDescriptor{
VersionType: &git.GitVersionTypeValues.Branch,
Version: &handler.repourl.defaultBranch,
},
},
})
if err != nil {
handler.errSetup = fmt.Errorf("request for first commit failed with %w", err)
return
}

Check warning on line 134 in clients/azuredevopsrepo/commits.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/commits.go#L114-L134

Added lines #L114 - L134 were not covered by tests

handler.firstCommitCreatedAt = (*firstCommit)[0].Committer.Date.Time

Check warning on line 136 in clients/azuredevopsrepo/commits.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/commits.go#L136

Added line #L136 was not covered by tests
}

handler.commitsRaw = commits
handler.pullRequestsRaw = pullRequests

Expand Down Expand Up @@ -182,3 +212,11 @@

return pullRequests, nil
}

func (handler *commitsHandler) getFirstCommitCreatedAt() (time.Time, error) {
if err := handler.setup(); err != nil {
return time.Time{}, fmt.Errorf("error during commitsHandler.setup: %w", err)
}

Check warning on line 219 in clients/azuredevopsrepo/commits.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/commits.go#L216-L219

Added lines #L216 - L219 were not covered by tests

return handler.firstCommitCreatedAt, nil

Check warning on line 221 in clients/azuredevopsrepo/commits.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/commits.go#L221

Added line #L221 was not covered by tests
}
Loading
Loading