Skip to content

Commit

Permalink
add integration test that directly invokes credential helper
Browse files Browse the repository at this point in the history
  • Loading branch information
malt3 committed Feb 17, 2025
1 parent 1a03d27 commit 3754abe
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 3 deletions.
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use_repo(
"com_github_aws_aws_sdk_go_v2",
"com_github_aws_aws_sdk_go_v2_config",
"com_github_aws_aws_sdk_go_v2_credentials",
"com_github_bazelbuild_rules_go",
"com_github_stretchr_testify",
"com_github_zalando_go_keyring",
"io_k8s_sigs_yaml",
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.32.5
github.com/aws/aws-sdk-go-v2/config v1.28.5
github.com/aws/aws-sdk-go-v2/credentials v1.17.46
github.com/bazelbuild/rules_go v0.53.0
github.com/stretchr/testify v1.10.0
github.com/zalando/go-keyring v0.2.6
golang.org/x/oauth2 v0.24.0
Expand All @@ -29,6 +30,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLb
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bazelbuild/rules_go v0.53.0 h1:u160DT+RRb+Xb2aSO4piN8xhs4aZvWz2UDXCq48F4ao=
github.com/bazelbuild/rules_go v0.53.0/go.mod h1:xB1jfsYHWlnZyPPxzlOSst4q2ZAwS251Mp9Iw6TPuBc=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -48,8 +50,8 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
19 changes: 19 additions & 0 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "tests",
srcs = ["framework.go"],
importpath = "github.com/tweag/credential-helper/tests",
visibility = ["//visibility:public"],
deps = [
"//api",
"@com_github_bazelbuild_rules_go//go/runfiles",
],
)

go_test(
name = "tests_test",
srcs = ["oci_test.go"],
embed = [":tests"],
data = ["//:tweag-credential-helper"],
)
192 changes: 192 additions & 0 deletions tests/framework.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package framework

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"os/exec"
"testing"

"github.com/bazelbuild/rules_go/go/runfiles"
"github.com/tweag/credential-helper/api"
)

type response struct{}

type TestBuilder struct {
credentialHelperBinary string
t *testing.T
}

func (builder *TestBuilder) WithHelperBinary(credentialHelperBinary string) *TestBuilder {
builder.credentialHelperBinary = credentialHelperBinary
return builder
}

func (builder *TestBuilder) WithDefaultHelperBinary() *TestBuilder {
if os.Getenv("BAZEL_TEST") == "1" {
runfileLocation, err := runfiles.Rlocation("_main/tweag-credential-helper_/tweag-credential-helper")
if err != nil {
panic(err)
}
builder.credentialHelperBinary = runfileLocation
} else {
builder.credentialHelperBinary = "tweag-credential-helper"
}
return builder
}

func (builder *TestBuilder) WithT(t *testing.T) *TestBuilder {
builder.t = t
return builder
}

func (builder *TestBuilder) Build() *CredentialHelperTest {
return &CredentialHelperTest{
credentialHelperBinary: builder.credentialHelperBinary,
t: builder.t,
}
}

type CredentialHelperTest struct {
credentialHelperBinary string
t *testing.T
}

func (test *CredentialHelperTest) invoke(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, test.credentialHelperBinary, args...)
cmd.Stdin = bytes.NewReader(stdin)
out, err := cmd.Output()
return out, err
}

func (test *CredentialHelperTest) getCommand(ctx context.Context, uri string) (api.GetCredentialsResponse, error) {
stdin, err := json.Marshal(api.GetCredentialsRequest{URI: uri})
if err != nil {
return api.GetCredentialsResponse{}, err
}

var response api.GetCredentialsResponse
stdout, err := test.invoke(ctx, stdin, "get")
if err != nil {
return api.GetCredentialsResponse{}, err
}
if err := json.Unmarshal(stdout, &response); err != nil {
return api.GetCredentialsResponse{}, err
}
return response, nil
}

func (test *CredentialHelperTest) Fetch(ctx context.Context, uri string) (*FetchResult, error) {
helperResp, err := test.getCommand(ctx, uri)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, http.NoBody)
if err != nil {
return nil, err
}

for key, values := range helperResp.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}

client := http.Client{}

httpResp, err := client.Do(req)
if err != nil {
return nil, err
}

defer httpResp.Body.Close()

body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}

return &FetchResult{
helperResp: &helperResp,
httpResp: httpResp,
body: body,
t: test.t,
}, nil
}

type FetchResult struct {
helperResp *api.GetCredentialsResponse
httpResp *http.Response
body []byte
t *testing.T
}

func (result *FetchResult) ExpectStatusCode(expected int) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
if result.httpResp.StatusCode != expected {
result.t.Errorf("expected status code %d, got %d", expected, result.httpResp.StatusCode)
}
}

func (result *FetchResult) ExpectHeader(key, value string) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
actual := result.httpResp.Header.Get(key)
if actual != value {
result.t.Errorf("expected header %q to be %q, got %q", key, value, actual)
}
}

func (result *FetchResult) ExpectHelperToReturnHeader(headerName string, validations ...func([]string) error) {
if result.helperResp == nil {
result.t.Error("helper response is nil")
return
}
actual := result.helperResp.Headers[headerName]
for _, validation := range validations {
if err := validation(actual); err != nil {
result.t.Error(err)
}
}
}

func (result *FetchResult) ExpectBody(expected []byte) {
if !bytes.Equal(result.body, expected) {
result.t.Errorf("expected body to match %v, got %v", string(expected), string(result.body))
}
}

func (result *FetchResult) ExpectBodySHA256(expected [sha256.Size]byte) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
hash := sha256.New()
hash.Write(result.body)
actual := hash.Sum(nil)
if !bytes.Equal(actual, expected[:]) {
result.t.Errorf("expected body sha256 to match %x, got %x", expected, actual)
}
}

func HexToSHA256(t *testing.T, hexStr string) [sha256.Size]byte {
hash, err := hex.DecodeString(hexStr)
if err != nil {
t.Fatalf("failed to decode hex: %v", err)
}
var hash32 [sha256.Size]byte
copy(hash32[:], hash)
return hash32
}
28 changes: 28 additions & 0 deletions tests/oci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package framework

import (
"context"
"strings"
"testing"
)

func TestDockerHub(t *testing.T) {
builder := &TestBuilder{}
test := builder.WithDefaultHelperBinary().WithT(t).Build()
ctx := context.Background()
fetchResult, err := test.Fetch(ctx, "https://index.docker.io/v2/library/hello-world/blobs/sha256:d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a")
if err != nil {
t.Fatalf("failed to fetch credentials: %v", err)
}
fetchResult.ExpectHelperToReturnHeader("Authorization", func(s []string) error {
if len(s) != 1 {
t.Errorf("expected exactly one Authorization header, got %d", len(s))
}
if len(s) == 1 && !strings.HasPrefix(s[0], "Bearer ") {
t.Errorf("expected Authorization header to start with 'Bearer '")
}
return nil
})
fetchResult.ExpectStatusCode(200)
fetchResult.ExpectBodySHA256(HexToSHA256(t, "d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a"))
}

0 comments on commit 3754abe

Please sign in to comment.