mirror of
https://github.com/ossf/scorecard.git
synced 2024-09-11 08:55:27 +03:00
🌱 add support for parsing azure devops urls
Signed-off-by: Jamie Magee <jamie.magee@gmail.com>
This commit is contained in:
parent
a8eae2d833
commit
8f9e6905bd
116
clients/azuredevops/repo.go
Normal file
116
clients/azuredevops/repo.go
Normal file
@ -0,0 +1,116 @@
|
||||
// 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 azuredevops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ossf/scorecard/v5/clients"
|
||||
sce "github.com/ossf/scorecard/v5/errors"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
scheme string
|
||||
host string
|
||||
organization string
|
||||
project string
|
||||
name string
|
||||
metadata []string
|
||||
}
|
||||
|
||||
// Parses input string into repoURL struct
|
||||
/*
|
||||
Accepted input string formats are as follows:
|
||||
- "dev.azure.com/<organization:string>/<project:string>/_git/<repository:string>"
|
||||
- "https://dev.azure.com/<organization:string>/<project:string>/_git/<repository:string>"
|
||||
*/
|
||||
func (r *Repo) parse(input string) error {
|
||||
u, err := url.Parse(withDefaultScheme(input))
|
||||
if err != nil {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("url.Parse: %v", err))
|
||||
}
|
||||
|
||||
const splitLen = 4
|
||||
split := strings.SplitN(strings.Trim(u.Path, "/"), "/", splitLen)
|
||||
if len(split) != splitLen {
|
||||
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("Azure DevOps repo format is invalid: %s", input))
|
||||
}
|
||||
|
||||
r.scheme, r.host, r.organization, r.project, r.name = u.Scheme, u.Host, split[0], split[1], split[3]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Allow skipping scheme for ease-of-use, default to https.
|
||||
func withDefaultScheme(uri string) string {
|
||||
if strings.Contains(uri, "://") {
|
||||
return uri
|
||||
}
|
||||
return "https://" + uri
|
||||
}
|
||||
|
||||
// URI implements Repo.URI().
|
||||
func (r *Repo) URI() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s/%s", r.host, r.organization, r.project, "_git", r.name)
|
||||
}
|
||||
|
||||
func (r *Repo) Host() string {
|
||||
return r.host
|
||||
}
|
||||
|
||||
// String implements Repo.String.
|
||||
func (r *Repo) String() string {
|
||||
return fmt.Sprintf("%s-%s_%s_%s", r.host, r.organization, r.project, r.name)
|
||||
}
|
||||
|
||||
// IsValid checks if the repoURL is valid.
|
||||
func (r *Repo) IsValid() error {
|
||||
if strings.TrimSpace(r.organization) == "" ||
|
||||
strings.TrimSpace(r.project) == "" ||
|
||||
strings.TrimSpace(r.name) == "" {
|
||||
return sce.WithMessage(sce.ErrInvalidURL, "expected full project url: "+r.URI())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) AppendMetadata(metadata ...string) {
|
||||
r.metadata = append(r.metadata, metadata...)
|
||||
}
|
||||
|
||||
// Metadata implements Repo.Metadata.
|
||||
func (r *Repo) Metadata() []string {
|
||||
return r.metadata
|
||||
}
|
||||
|
||||
// Path() implements RepoClient.Path.
|
||||
func (r *Repo) Path() string {
|
||||
return fmt.Sprintf("%s/%s/%s/%s", r.organization, r.project, "_git", r.name)
|
||||
}
|
||||
|
||||
// MakeAzureDevOpsRepo takes input of forms in parse and returns and implementation
|
||||
// of clients.Repo interface.
|
||||
func MakeAzureDevOpsRepo(input string) (clients.Repo, error) {
|
||||
var repo Repo
|
||||
if err := repo.parse(input); err != nil {
|
||||
return nil, fmt.Errorf("error during parse: %w", err)
|
||||
}
|
||||
if err := repo.IsValid(); err != nil {
|
||||
return nil, fmt.Errorf("error in IsValid: %w", err)
|
||||
}
|
||||
|
||||
return &repo, nil
|
||||
}
|
173
clients/azuredevops/repo_test.go
Normal file
173
clients/azuredevops/repo_test.go
Normal file
@ -0,0 +1,173 @@
|
||||
// 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 azuredevops
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
)
|
||||
|
||||
func TestRepo_parse(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
inputURL string
|
||||
expected Repo
|
||||
wantErr bool
|
||||
flagRequired bool
|
||||
}{
|
||||
{
|
||||
name: "valid azuredevops project with scheme",
|
||||
expected: Repo{
|
||||
host: "dev.azure.com",
|
||||
organization: "dnceng-public",
|
||||
project: "public",
|
||||
name: "public",
|
||||
},
|
||||
inputURL: "https://dev.azure.com/dnceng-public/public/_git/public",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid azuredevops project without scheme",
|
||||
expected: Repo{
|
||||
host: "dev.azure.com",
|
||||
organization: "dnceng-public",
|
||||
project: "public",
|
||||
name: "public",
|
||||
},
|
||||
inputURL: "dev.azure.com/dnceng-public/public/_git/public",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid azuredevops project missing repo",
|
||||
expected: Repo{},
|
||||
inputURL: "https://dev.azure.com/dnceng-public/public",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := Repo{}
|
||||
if err := r.parse(tt.inputURL); (err != nil) != tt.wantErr {
|
||||
t.Errorf("repoURL.parse() error = %v", err)
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
t.Log(r.URI())
|
||||
if !tt.wantErr && !cmp.Equal(tt.expected, r, cmpopts.IgnoreUnexported(Repo{})) {
|
||||
t.Logf("expected: %s GOT: %s", tt.expected.host, r.host)
|
||||
t.Logf("expected: %s GOT: %s", tt.expected.organization, r.organization)
|
||||
t.Logf("expected: %s GOT: %s", tt.expected.project, r.project)
|
||||
t.Logf("expected: %s GOT: %s", tt.expected.name, r.name)
|
||||
t.Errorf("Got diff: %s", cmp.Diff(tt.expected, r))
|
||||
}
|
||||
if !cmp.Equal(r.Host(), tt.expected.host) {
|
||||
t.Errorf("%s expected host: %s got host %s", tt.inputURL, tt.expected.host, r.Host())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepo_IsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
inputURL string
|
||||
repo Repo
|
||||
wantErr bool
|
||||
flagRequired bool
|
||||
}{
|
||||
{
|
||||
name: "valid azuredevops project",
|
||||
repo: Repo{
|
||||
host: "dev.azure.com",
|
||||
organization: "dnceng-public",
|
||||
project: "public",
|
||||
name: "public",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid azuredevops project",
|
||||
repo: Repo{
|
||||
host: "dev.azure.com",
|
||||
organization: "dnceng-public",
|
||||
project: "public",
|
||||
name: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tt.repo.IsValid(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("repoURL.IsValid() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepo_MakeAzureDevOpsRepo(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
repouri string
|
||||
expected bool
|
||||
flagRequired bool
|
||||
}{
|
||||
{
|
||||
repouri: "github.com/ossf/scorecard",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
repouri: "ossf/scorecard",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
repouri: "https://github.com/ossf/scorecard",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
repouri: "https://dev.azure.com/dnceng-public/public/_git/public",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
repouri: "dev.azure.com/dnceng-public/public/_git/public",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
g, err := MakeAzureDevOpsRepo(tt.repouri)
|
||||
if (g != nil) != (err == nil) {
|
||||
t.Errorf("got azuredevopsrepo: %s with err %s", g, err)
|
||||
}
|
||||
isAzureDevOps := g != nil && err == nil
|
||||
if isAzureDevOps != tt.expected {
|
||||
t.Errorf("got %s isazuredevops: %t expected %t", tt.repouri, isAzureDevOps, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user