Skip to content

Commit

Permalink
Merge pull request #73 from chialab/unit-test
Browse files Browse the repository at this point in the history
Unit tests… sort of
  • Loading branch information
fquffio authored Apr 18, 2024
2 parents 9030dc9 + 9f216d4 commit 2c1e415
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 64 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
tags: [ v* ]

permissions:
contents: write

jobs:
build:
name: Build and release
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,21 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
build-mode: manual

- name: Build
run: go build -v ./...

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:go'
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:
permissions:
contents: write


jobs:
go-action-detection:
name: Submit dependencies
Expand Down
37 changes: 8 additions & 29 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ jobs:
with:
version: latest

build:
name: Test build succeeds
unit:
name: Run unit tests
runs-on: ubuntu-latest
timeout-minutes: 5

Expand All @@ -43,31 +43,10 @@ jobs:
- name: Build
run: go build -v ./...

# unit:
# name: Run unit tests
# runs-on: ubuntu-latest
# timeout-minutes: 5
- name: Test
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...

# steps:
# - name: Checkout
# uses: actions/checkout@v4
# with:
# fetch-depth: 2

# - name: Set up Go
# uses: actions/setup-go@v5
# with:
# go-version-file: go.mod

# - name: Build
# run: go build -v ./...

# - name: Test
# run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
# timeout-minutes: 1

# - name: Upload coverage to Codecov
# run: bash <(curl -s https://codecov.io/bash)
# env:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# timeout-minutes: 1
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
72 changes: 72 additions & 0 deletions ecr/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ecr

import (
"context"
"encoding/base64"
"errors"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"strings"
)

// InvalidTokenError is returned when token is not in base64-encoded `username:password` format.
var InvalidTokenError = errors.New("unexpected token format")

// UnexpectedResponseError is returned when ECR client returns zero or more than one types.AuthorizationData in its response.
var UnexpectedResponseError = errors.New("unexpected number of authorization data returned")

// Interface required to obtain an ECR authentication token.
type ecrTokenClient interface {
GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)
}

// getClient returns an ecrTokenClient instantiating a new AWS ECR client using AWS default configuration with options.
func getClient(context context.Context, optsFns ...func(*config.LoadOptions) error) (ecrTokenClient, error) {
if cfg, err := config.LoadDefaultConfig(context, optsFns...); err != nil {
return nil, err
} else {
return ecr.NewFromConfig(cfg), nil
}
}

// Obtain a types.AuthorizationData from an ecrTokenClient.
func getAuthData(context context.Context, client ecrTokenClient) (*types.AuthorizationData, error) {
if token, err := client.GetAuthorizationToken(context, &ecr.GetAuthorizationTokenInput{}); err != nil {
return nil, err
} else if len(token.AuthorizationData) != 1 {
return nil, UnexpectedResponseError
} else {
return &token.AuthorizationData[0], nil
}
}

// AuthorizationToken extends types.AuthorizationData by adding Username and Password explicitly.
type AuthorizationToken struct {
types.AuthorizationData
Username *string
Password *string
}

// NewToken builds an AuthorizationToken from types.AuthorizationData.
func NewToken(authData *types.AuthorizationData) (*AuthorizationToken, error) {
token := *authData.AuthorizationToken
if data, err := base64.StdEncoding.DecodeString(token); err != nil {
return nil, err
} else if parts := strings.SplitN(string(data), ":", 2); len(parts) != 2 {
return nil, InvalidTokenError
} else {
return &AuthorizationToken{*authData, &parts[0], &parts[1]}, nil
}
}

// GetToken obtains an AuthorizationToken from AWS ECR client using the passed context and options
func GetToken(context context.Context, optsFns ...func(*config.LoadOptions) error) (*AuthorizationToken, error) {
if client, err := getClient(context, optsFns...); err != nil {
return nil, err
} else if authData, err := getAuthData(context, client); err != nil {
return nil, err
} else {
return NewToken(authData)
}
}
157 changes: 157 additions & 0 deletions ecr/token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package ecr

import (
"context"
"encoding/base64"
"errors"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"testing"
"time"
)

func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
func ind[T any](val T) *T { return &val }

type mockECRClient func(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error)

func (m mockECRClient) GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
return m(ctx, params, optFns...)
}

func TestGetAuthData(t *testing.T) {
t.Run("Success", func(t *testing.T) {
expected := types.AuthorizationData{AuthorizationToken: ind("Zm9vOmJhcg==")}
client := mockECRClient(func(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
return &ecr.GetAuthorizationTokenOutput{AuthorizationData: []types.AuthorizationData{expected}}, nil
})

authData, err := getAuthData(context.TODO(), client)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if authData == nil {
t.Fatalf("expected authorization data, got nil")
}

if expected.AuthorizationToken != authData.AuthorizationToken {
t.Errorf("expected authorization token %v, got %v", expected.AuthorizationToken, authData.AuthorizationToken)
}
})

testError := errors.New("my error")
fail := map[string]struct {
client ecrTokenClient
expected error
}{
"Error": {
mockECRClient(func(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
return nil, testError
}),
testError,
},
"Invalid(EmptyAuthorizationDataArray)": {
mockECRClient(func(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
return &ecr.GetAuthorizationTokenOutput{AuthorizationData: []types.AuthorizationData{}}, nil
}),
UnexpectedResponseError,
},
"Invalid(AuthorizationDataArrayTooLong)": {
mockECRClient(func(context.Context, *ecr.GetAuthorizationTokenInput, ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) {
return &ecr.GetAuthorizationTokenOutput{AuthorizationData: []types.AuthorizationData{{AuthorizationToken: ind("Zm9vOmJhcg==")}, {AuthorizationToken: ind("Zm9vOmJhcjpiYXo=")}}}, nil
}),
UnexpectedResponseError,
},
}
for name, tt := range fail {
t.Run(name, func(t *testing.T) {
client, expected := tt.client, tt.expected
authData, err := getAuthData(context.TODO(), client)
if authData != nil {
t.Fatalf("expected nil authorization data, got %v", authData)
}
if !errors.Is(err, expected) {
t.Errorf("expected error %v, got %v", expected, err)
}
})
}
}

func TestNewToken(t *testing.T) {
success := map[string]struct {
input *types.AuthorizationData
expected [2]string
}{
"Success(foo:bar)": {
&types.AuthorizationData{AuthorizationToken: ind("Zm9vOmJhcg=="), ExpiresAt: ind(must(time.Parse(time.RFC3339, "2024-04-18T13:18:00Z")))},
[2]string{"foo", "bar"},
},
"Success(foo:bar:baz)": {
&types.AuthorizationData{AuthorizationToken: ind("Zm9vOmJhcjpiYXo="), ExpiresAt: ind(must(time.Parse(time.RFC3339, "2024-04-18T13:18:00Z")))},
[2]string{"foo", "bar:baz"},
},
}

for name, tt := range success {
t.Run(name, func(t *testing.T) {
input, username, password := tt.input, tt.expected[0], tt.expected[1]
token, err := NewToken(input)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if token == nil {
t.Fatalf("expected token, got nil")
}

if input.AuthorizationToken != token.AuthorizationToken {
t.Errorf("expected authorization token %v, got %v", input.AuthorizationToken, token.AuthorizationToken)
}
if input.ExpiresAt != token.ExpiresAt {
t.Errorf("expected expiration %s, got %s", input.ExpiresAt, token.ExpiresAt)
}
if input.ProxyEndpoint != token.ProxyEndpoint {
t.Errorf("expected proxy endpoint %v, got %v", input.ProxyEndpoint, token.ProxyEndpoint)
}
if input.AuthorizationToken != token.AuthorizationToken {
t.Errorf("expected authorization token %v, got %v", input.AuthorizationToken, token.AuthorizationToken)
}
if username != *token.Username {
t.Errorf("expected username %s, got %s", username, *token.Username)
}
if password != *token.Password {
t.Errorf("expected password %s, got %s", password, *token.Password)
}
})
}

fail := map[string]struct {
input *types.AuthorizationData
expected error
}{
"Invalid(CorruptInput)": {
&types.AuthorizationData{AuthorizationToken: ind("__NOT_A_BASE64_STRING!"), ExpiresAt: ind(must(time.Parse(time.RFC3339, "2024-04-18T13:18:00Z")))},
base64.CorruptInputError(0),
},
"Invalid(`foo`)": {
&types.AuthorizationData{AuthorizationToken: ind("Zm9v"), ExpiresAt: ind(must(time.Parse(time.RFC3339, "2024-04-18T13:18:00Z")))},
InvalidTokenError,
},
}
for name, tt := range fail {
t.Run(name, func(t *testing.T) {
input, expected := tt.input, tt.expected
token, err := NewToken(input)
if token != nil {
t.Fatalf("expected nil token, got %v", token)
}
if !errors.Is(err, expected) {
t.Errorf("expected error %v, got %v", expected, err)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/chialab/aws-ecr-get-login-password

go 1.21
go 1.22

require (
github.com/aws/aws-sdk-go-v2/config v1.27.11
Expand Down
36 changes: 3 additions & 33 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,15 @@ package main

import (
"context"
"encoding/base64"
"fmt"
"github.com/chialab/aws-ecr-get-login-password/ecr"
"log"
"os"
"strings"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ecr"
)

func main() {
if token, err := getToken(); err != nil {
if token, err := ecr.GetToken(context.Background()); err != nil {
log.Fatal(err)
} else {
fmt.Println(token)
}
}

// Retrieve token for authentication against ECR registries.
func getToken() (string, error) {
os.Setenv("AWS_SDK_LOAD_CONFIG", "1")
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return "", err
}

svc := ecr.NewFromConfig(cfg)
token, err := svc.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{})
if err != nil {
return "", err
}

authData := token.AuthorizationData[0].AuthorizationToken
data, err := base64.StdEncoding.DecodeString(*authData)
if err != nil {
return "", err
fmt.Println(*token.Password)
}

parts := strings.SplitN(string(data), ":", 2)

return parts[1], nil
}

0 comments on commit 2c1e415

Please sign in to comment.