Skip to content

Commit

Permalink
add test for internal/colonyapi (#9)
Browse files Browse the repository at this point in the history
* add test

* fix: add code review

* refactor httptest server

* add test for server error

* refactor: use const

---------

Co-authored-by: Patrick D'appollonio <[email protected]>
  • Loading branch information
muse-sisay and patrickdappollonio authored Sep 19, 2024
1 parent 34b3565 commit a169c4b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 73 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require (
k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
sigs.k8s.io/yaml v1.4.0
)

require (
Expand Down Expand Up @@ -52,4 +51,5 @@ require (
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
10 changes: 7 additions & 3 deletions internal/colony/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
Expand All @@ -15,8 +16,11 @@ type API struct {
token string
}

var errInvalidKey = errors.New("invalid Colony API key")

const (
validateAPIKeyURL = "/api/v1/token/validate"
templateEndpoint = "/api/v1/templates/all/system"
validateEndpoint = "/api/v1/token/validate"
)

// New creates a new colony API client
Expand All @@ -37,7 +41,7 @@ func New(baseURL, token string) *API {
}

func (a *API) ValidateAPIKey(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+validateAPIKeyURL, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.baseURL+validateEndpoint, nil)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}
Expand All @@ -64,7 +68,7 @@ func (a *API) ValidateAPIKey(ctx context.Context) error {
}

if !r.IsValid {
return fmt.Errorf("invalid api key")
return errInvalidKey
}

return nil
Expand Down
145 changes: 80 additions & 65 deletions internal/colony/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,103 +2,118 @@ package colony

import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)

func createServer(t *testing.T, path string, fn http.HandlerFunc) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc(path, fn)
return httptest.NewServer(mux)
}
const (
testValidToken = "super-duper-valid-token"
)

func Test_ValidateAPIKey(t *testing.T) {
t.Run("full request flow", func(t *testing.T) {
fakeToken := "my-super-secret"
func TestAPI_ValidateApiKey(t *testing.T) {
t.Run("valid API key", func(t *testing.T) {
response := map[string]interface{}{
"isValid": true,
}

srv := createServer(t, validateAPIKeyURL, func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer "+fakeToken {
t.Fatalf("unexpected Authorization header: %s", r.Header.Get("Authorization"))
}
mockServer := createServer(t, response, validateEndpoint)

w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"isValid": true}`))
})
defer srv.Close()
defer mockServer.Close()

ctx := context.Background()
api := New(srv.URL, fakeToken)
api := New(mockServer.URL, testValidToken)

if err := api.ValidateAPIKey(ctx); err != nil {
t.Fatalf("unexpected error: %v", err)
err := api.ValidateAPIKey(context.TODO())
if err != nil {
t.Fatalf("expected nil but got: %s", err)
}
})

t.Run("malformed URL", func(t *testing.T) {
baseURL := ":"
api := New(baseURL, "token")
t.Run("invalid API key", func(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"isValid": false}`)
}))

err := api.ValidateAPIKey(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
defer mockServer.Close()

api := New(mockServer.URL, testValidToken)

err := api.ValidateAPIKey(context.TODO())
if !errors.Is(err, errInvalidKey) {
t.Fatalf("expected %s, but got: %s", errInvalidKey, err)
}
})
}

t.Run("unable to connect", func(t *testing.T) {
srv := httptest.NewServer(nil)
srv.Close()
func TestAPI_GetSystemTemplates(t *testing.T) {
t.Run("valid response", func(t *testing.T) {
response := []Template{{
ID: "k1",
Name: "name",
Label: "label",
IsTinkTemplate: true,
IsSystem: true,
Template: "template_data",
}}

api := New(srv.URL, "token")
mockServer := createServer(t, response, templateEndpoint)

err := api.ValidateAPIKey(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
}
})
defer mockServer.Close()

t.Run("invalid status code", func(t *testing.T) {
srv := createServer(t, validateAPIKeyURL, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
defer srv.Close()
api := New(mockServer.URL, testValidToken)

api := New(srv.URL, "token")
templates, err := api.GetSystemTemplates(context.TODO())
if err != nil {
t.Fatalf("expected nil but got: %s", err)
}

err := api.ValidateAPIKey(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
if !reflect.DeepEqual(response, templates) {
t.Fatalf("expected %#v got %#v", response, templates)
}
})

t.Run("invalid response body", func(t *testing.T) {
srv := createServer(t, validateAPIKeyURL, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`not JSON`))
})
defer srv.Close()
t.Run("connection reset by peer", func(t *testing.T) {
myListener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("error creating listener %s", err)
}
address := myListener.Addr().String()

go func() {
for {
con, err := myListener.Accept()
if err != nil {
t.Log(err)
}
con.Close()
}
}()

api := New(srv.URL, "token")
api := New(address, testValidToken)

err := api.ValidateAPIKey(context.Background())
_, err = api.GetSystemTemplates(context.TODO())
if err == nil {
t.Fatal("expected error, got nil")
t.Fatal("was expecting error but got none")
}
})
}

t.Run("invalid API key", func(t *testing.T) {
srv := createServer(t, validateAPIKeyURL, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"isValid": false}`))
})
defer srv.Close()

api := New(srv.URL, "token")
func createServer(t *testing.T, response interface{}, apiEndpoint string) *httptest.Server {
t.Helper()

err := api.ValidateAPIKey(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
mux := http.NewServeMux()
mux.HandleFunc("GET "+apiEndpoint, func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", testValidToken) {
t.Fatalf("expected to get a bearer token %s but got: %s", fmt.Sprintf("Bearer %s", testValidToken), r.Header.Get("Authorization"))
}

json.NewEncoder(w).Encode(response)
})

return httptest.NewServer(mux)
}
6 changes: 2 additions & 4 deletions internal/k8s/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import (
"context"
"testing"

"github.com/konstructio/colony/internal/logger"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
fakeServer "k8s.io/client-go/kubernetes/fake"
)

func TestClient_CreateAPIKeySecret(t *testing.T) {

t.Run("successful creation", func(tt *testing.T) {

var (
secretNamespace = "tink-system"
secretName = "colony-api"
Expand All @@ -24,6 +23,7 @@ func TestClient_CreateAPIKeySecret(t *testing.T) {

client := &Client{
clientSet: mockServer,
logger: logger.NOOPLogger,
}

ctx := context.TODO()
Expand All @@ -35,7 +35,6 @@ func TestClient_CreateAPIKeySecret(t *testing.T) {

// check secret exists
secret, err := client.clientSet.CoreV1().Secrets(secretNamespace).Get(ctx, secretName, v1.GetOptions{})

if err != nil {
if errors.IsNotFound(err) {
tt.Fatalf("can't find the secret %q", secretName)
Expand All @@ -52,5 +51,4 @@ func TestClient_CreateAPIKeySecret(t *testing.T) {
tt.Fatalf("expected key value %q but got %q", secretValue, string(data))
}
})

}

0 comments on commit a169c4b

Please sign in to comment.