400 lines
10 KiB
Go
400 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
|
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testRepo = "github/example"
|
|
testOwner = "github"
|
|
testDigest = "sha256:12313213"
|
|
)
|
|
|
|
func NewClientWithMockGHClient(hasNextPage bool) Client {
|
|
fetcher := mockDataGenerator{
|
|
NumUserAttestations: 5,
|
|
NumGitHubAttestations: 4,
|
|
}
|
|
l := io.NewTestHandler()
|
|
|
|
httpClient := &mockHttpClient{}
|
|
|
|
if hasNextPage {
|
|
return &LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage,
|
|
},
|
|
httpClient: httpClient,
|
|
logger: l,
|
|
}
|
|
}
|
|
|
|
return &LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.OnRESTSuccess,
|
|
},
|
|
httpClient: httpClient,
|
|
logger: l,
|
|
}
|
|
}
|
|
|
|
var testFetchParamsWithOwner = FetchParams{
|
|
Digest: testDigest,
|
|
Limit: DefaultLimit,
|
|
Owner: testOwner,
|
|
PredicateType: "https://slsa.dev/provenance/v1",
|
|
Initiator: "user",
|
|
}
|
|
var testFetchParamsWithRepo = FetchParams{
|
|
Digest: testDigest,
|
|
Limit: DefaultLimit,
|
|
Repo: testRepo,
|
|
PredicateType: "https://slsa.dev/provenance/v1",
|
|
Initiator: "user",
|
|
}
|
|
|
|
var testFetchParamsWithRepoWithGitHubInitiator = FetchParams{
|
|
Digest: testDigest,
|
|
Limit: DefaultLimit,
|
|
Repo: testRepo,
|
|
Initiator: "github",
|
|
}
|
|
|
|
type getByTestCase struct {
|
|
name string
|
|
params FetchParams
|
|
limit int
|
|
expectedAttestations int
|
|
hasNextPage bool
|
|
}
|
|
|
|
var getByTestCases = []getByTestCase{
|
|
{
|
|
name: "get by digest with owner",
|
|
params: testFetchParamsWithOwner,
|
|
expectedAttestations: 5,
|
|
},
|
|
{
|
|
name: "get by digest with repo",
|
|
params: testFetchParamsWithRepo,
|
|
expectedAttestations: 5,
|
|
},
|
|
{
|
|
name: "get by digest with attestations greater than limit",
|
|
params: testFetchParamsWithRepo,
|
|
limit: 3,
|
|
expectedAttestations: 3,
|
|
},
|
|
{
|
|
name: "get by digest with next page",
|
|
params: testFetchParamsWithRepo,
|
|
expectedAttestations: 10,
|
|
hasNextPage: true,
|
|
},
|
|
{
|
|
name: "greater than limit with next page",
|
|
params: testFetchParamsWithRepo,
|
|
limit: 7,
|
|
expectedAttestations: 7,
|
|
hasNextPage: true,
|
|
},
|
|
{
|
|
name: "get by digest with repo and GitHub initiator",
|
|
params: testFetchParamsWithRepoWithGitHubInitiator,
|
|
expectedAttestations: 4,
|
|
},
|
|
}
|
|
|
|
func TestGetByDigest(t *testing.T) {
|
|
for _, tc := range getByTestCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := NewClientWithMockGHClient(tc.hasNextPage)
|
|
|
|
if tc.limit > 0 {
|
|
tc.params.Limit = tc.limit
|
|
}
|
|
attestations, err := c.GetByDigest(tc.params)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.expectedAttestations, len(attestations))
|
|
bundle := (attestations)[0].Bundle
|
|
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetByDigest_NoAttestationsFound(t *testing.T) {
|
|
fetcher := mockDataGenerator{
|
|
NumUserAttestations: 5,
|
|
}
|
|
|
|
httpClient := &mockHttpClient{}
|
|
c := LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations,
|
|
},
|
|
httpClient: httpClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
|
require.Error(t, err)
|
|
require.IsType(t, ErrNoAttestationsFound, err)
|
|
require.Nil(t, attestations)
|
|
}
|
|
|
|
func TestGetByDigest_Error(t *testing.T) {
|
|
fetcher := mockDataGenerator{
|
|
NumUserAttestations: 5,
|
|
}
|
|
|
|
c := LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.OnRESTWithNextError,
|
|
},
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
|
require.Error(t, err)
|
|
require.Nil(t, attestations)
|
|
}
|
|
|
|
func TestFetchBundleFromAttestations_BundleURL(t *testing.T) {
|
|
httpClient := &mockHttpClient{}
|
|
client := LiveClient{
|
|
httpClient: httpClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
att1 := makeTestAttestation()
|
|
att2 := makeTestAttestation()
|
|
attestations := []*Attestation{&att1, &att2}
|
|
fetched, err := client.fetchBundleFromAttestations(attestations)
|
|
require.NoError(t, err)
|
|
require.Len(t, fetched, 2)
|
|
require.NotNil(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
|
|
httpClient.AssertNumberOfCalls(t, "OnGetSuccess", 2)
|
|
}
|
|
|
|
func TestFetchBundleFromAttestations_MissingBundleAndBundleURLFields(t *testing.T) {
|
|
httpClient := &mockHttpClient{}
|
|
client := LiveClient{
|
|
httpClient: httpClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
// If both the BundleURL and Bundle fields are empty, the function should
|
|
// return an error indicating that
|
|
att1 := Attestation{}
|
|
attestations := []*Attestation{&att1}
|
|
bundles, err := client.fetchBundleFromAttestations(attestations)
|
|
require.ErrorContains(t, err, "attestation has no bundle or bundle URL")
|
|
require.Nil(t, bundles, 2)
|
|
}
|
|
|
|
func TestFetchBundleFromAttestations_FailOnTheSecondAttestation(t *testing.T) {
|
|
mockHTTPClient := &failAfterNCallsHttpClient{
|
|
// the initial HTTP request will succeed, which returns a bundle for the first attestation
|
|
// all following HTTP requests will fail, which means the function fails to fetch a bundle
|
|
// for the second attestation and the function returns an error
|
|
FailOnCallN: 2,
|
|
FailOnAllSubsequentCalls: true,
|
|
}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
att1 := makeTestAttestation()
|
|
att2 := makeTestAttestation()
|
|
attestations := []*Attestation{&att1, &att2}
|
|
bundles, err := c.fetchBundleFromAttestations(attestations)
|
|
require.Error(t, err)
|
|
require.Nil(t, bundles)
|
|
}
|
|
|
|
func TestFetchBundleFromAttestations_FailAfterRetrying(t *testing.T) {
|
|
mockHTTPClient := &reqFailHttpClient{}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
a := makeTestAttestation()
|
|
attestations := []*Attestation{&a}
|
|
bundle, err := c.fetchBundleFromAttestations(attestations)
|
|
require.Error(t, err)
|
|
require.Nil(t, bundle)
|
|
mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4)
|
|
}
|
|
|
|
func TestFetchBundleFromAttestations_FallbackToBundleField(t *testing.T) {
|
|
mockHTTPClient := &mockHttpClient{}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
// If the bundle URL is empty, the code will fallback to the bundle field
|
|
a := Attestation{Bundle: data.SigstoreBundle(t)}
|
|
attestations := []*Attestation{&a}
|
|
fetched, err := c.fetchBundleFromAttestations(attestations)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", fetched[0].Bundle.GetMediaType())
|
|
mockHTTPClient.AssertNotCalled(t, "OnGetSuccess")
|
|
}
|
|
|
|
// getBundle successfully fetches a bundle on the first HTTP request attempt
|
|
func TestGetBundle(t *testing.T) {
|
|
mockHTTPClient := &mockHttpClient{}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
b, err := c.getBundle("https://mybundleurl.com")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType())
|
|
mockHTTPClient.AssertNumberOfCalls(t, "OnGetSuccess", 1)
|
|
}
|
|
|
|
// getBundle retries successfully when the initial HTTP request returns
|
|
// a 5XX status code
|
|
func TestGetBundle_SuccessfulRetry(t *testing.T) {
|
|
mockHTTPClient := &failAfterNCallsHttpClient{
|
|
FailOnCallN: 1,
|
|
FailOnAllSubsequentCalls: false,
|
|
}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
b, err := c.getBundle("mybundleurl")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "application/vnd.dev.sigstore.bundle.v0.3+json", b.GetMediaType())
|
|
mockHTTPClient.AssertNumberOfCalls(t, "OnGetFailAfterNCalls", 2)
|
|
}
|
|
|
|
// getBundle does not retry when the function fails with a permanent backoff error condition
|
|
func TestGetBundle_PermanentBackoffFail(t *testing.T) {
|
|
mockHTTPClient := &invalidBundleClient{}
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
b, err := c.getBundle("mybundleurl")
|
|
// var permanent *backoff.PermanentError
|
|
//require.IsType(t, &backoff.PermanentError{}, err)
|
|
require.Error(t, err)
|
|
require.Nil(t, b)
|
|
mockHTTPClient.AssertNumberOfCalls(t, "OnGetInvalidBundle", 1)
|
|
}
|
|
|
|
// getBundle retries when the HTTP request fails
|
|
func TestGetBundle_RequestFail(t *testing.T) {
|
|
mockHTTPClient := &reqFailHttpClient{}
|
|
|
|
c := &LiveClient{
|
|
httpClient: mockHTTPClient,
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
b, err := c.getBundle("mybundleurl")
|
|
require.Error(t, err)
|
|
require.Nil(t, b)
|
|
mockHTTPClient.AssertNumberOfCalls(t, "OnGetReqFail", 4)
|
|
}
|
|
|
|
func TestGetTrustDomain(t *testing.T) {
|
|
fetcher := mockMetaGenerator{
|
|
TrustDomain: "foo",
|
|
}
|
|
|
|
t.Run("with returned trust domain", func(t *testing.T) {
|
|
c := LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnREST: fetcher.OnREST,
|
|
},
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
td, err := c.GetTrustDomain()
|
|
require.Nil(t, err)
|
|
require.Equal(t, "foo", td)
|
|
|
|
})
|
|
|
|
t.Run("with error", func(t *testing.T) {
|
|
c := LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnREST: fetcher.OnRESTError,
|
|
},
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
td, err := c.GetTrustDomain()
|
|
require.Equal(t, "", td)
|
|
require.ErrorContains(t, err, "test error")
|
|
})
|
|
|
|
}
|
|
|
|
func TestGetAttestationsRetries(t *testing.T) {
|
|
getAttestationRetryInterval = 0
|
|
|
|
fetcher := mockDataGenerator{
|
|
NumUserAttestations: 5,
|
|
}
|
|
|
|
c := &LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.FlakyOnRESTSuccessWithNextPageHandler(),
|
|
},
|
|
httpClient: &mockHttpClient{},
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
testFetchParamsWithRepo.Limit = 30
|
|
attestations, err := c.GetByDigest(testFetchParamsWithRepo)
|
|
require.NoError(t, err)
|
|
|
|
// assert the error path was executed; because this is a paged
|
|
// request, it should have errored twice
|
|
fetcher.AssertNumberOfCalls(t, "FlakyOnRESTSuccessWithNextPage:error", 2)
|
|
|
|
// but we still successfully got the right data
|
|
require.Equal(t, len(attestations), 10)
|
|
bundle := (attestations)[0].Bundle
|
|
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle.v0.3+json")
|
|
}
|
|
|
|
// test total retries
|
|
func TestGetAttestationsMaxRetries(t *testing.T) {
|
|
getAttestationRetryInterval = 0
|
|
|
|
fetcher := mockDataGenerator{
|
|
NumUserAttestations: 5,
|
|
}
|
|
|
|
c := &LiveClient{
|
|
githubAPI: mockAPIClient{
|
|
OnRESTWithNext: fetcher.OnREST500ErrorHandler(),
|
|
},
|
|
logger: io.NewTestHandler(),
|
|
}
|
|
|
|
_, err := c.GetByDigest(testFetchParamsWithRepo)
|
|
require.Error(t, err)
|
|
|
|
fetcher.AssertNumberOfCalls(t, "OnREST500Error", 4)
|
|
}
|