tests(openstack): cover GetApiIngressStatus

This commit is contained in:
Michael Wagner 2019-12-15 19:08:52 +01:00
parent de3bdc1be6
commit 0e5632c4c5
11 changed files with 989 additions and 1 deletions

View File

@ -1,4 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
@ -66,3 +66,19 @@ go_library(
"//vendor/k8s.io/klog:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["cloud_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/kops:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/testhelper:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/testhelper/client:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/testhelper/fixture:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
],
)

View File

@ -0,0 +1,439 @@
/*
Copyright 2019 The Kubernetes 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 openstack
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"sort"
"testing"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
"github.com/gophercloud/gophercloud/testhelper"
"github.com/gophercloud/gophercloud/testhelper/client"
"github.com/gophercloud/gophercloud/testhelper/fixture"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kops/pkg/apis/kops"
)
func Test_OpenstackCloud_GetApiIngressStatus(t *testing.T) {
tests := []struct {
desc string
cluster *kops.Cluster
loadbalancers []loadbalancers.LoadBalancer
floatingIPs []floatingips.FloatingIP
instances serverList
cloudFloatingEnabled bool
expectedAPIIngress []kops.ApiIngressStatus
expectedError error
}{
{
desc: "Loadbalancer configured master public name set",
cluster: &kops.Cluster{
Spec: kops.ClusterSpec{
MasterPublicName: "master.k8s.local",
CloudConfig: &kops.CloudConfiguration{
Openstack: &kops.OpenstackConfiguration{
Loadbalancer: &kops.OpenstackLoadbalancerConfig{},
},
},
},
},
loadbalancers: []loadbalancers.LoadBalancer{
{
ID: "lb_id",
Name: "name",
VipAddress: "10.1.2.3",
VipPortID: "vip_port_id",
VipSubnetID: "vip_subnet_id",
VipNetworkID: "vip_network_id",
},
},
floatingIPs: []floatingips.FloatingIP{
{
ID: "id",
FixedIP: "10.1.2.3",
InstanceID: "lb_id",
IP: "8.8.8.8",
},
},
expectedAPIIngress: []kops.ApiIngressStatus{
{
IP: "8.8.8.8",
},
},
},
{
desc: "Loadbalancer configured master public name set multiple IPs match",
cluster: &kops.Cluster{
Spec: kops.ClusterSpec{
MasterPublicName: "master.k8s.local",
CloudConfig: &kops.CloudConfiguration{
Openstack: &kops.OpenstackConfiguration{
Loadbalancer: &kops.OpenstackLoadbalancerConfig{},
},
},
},
},
loadbalancers: []loadbalancers.LoadBalancer{
{
ID: "lb_id",
Name: "name",
VipAddress: "10.1.2.3",
VipPortID: "vip_port_id",
VipSubnetID: "vip_subnet_id",
VipNetworkID: "vip_network_id",
},
},
floatingIPs: []floatingips.FloatingIP{
{
ID: "cluster",
FixedIP: "10.1.2.3",
InstanceID: "lb_id",
IP: "8.8.8.8",
},
{
ID: "something_else",
FixedIP: "192.168.2.3",
InstanceID: "xx_id",
IP: "2.2.2.2",
},
{
ID: "yet_another",
FixedIP: "10.1.2.3",
InstanceID: "yy_id",
IP: "9.9.9.9",
},
},
expectedAPIIngress: []kops.ApiIngressStatus{
{IP: "8.8.8.8"},
{IP: "9.9.9.9"},
},
},
{
desc: "Loadbalancer configured master public name not set",
cluster: &kops.Cluster{
Spec: kops.ClusterSpec{
CloudConfig: &kops.CloudConfiguration{
Openstack: &kops.OpenstackConfiguration{
Loadbalancer: &kops.OpenstackLoadbalancerConfig{},
},
},
},
},
expectedAPIIngress: nil,
},
{
desc: "No Loadbalancer configured no floating enabled",
cluster: &kops.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster.k8s.local",
},
Spec: kops.ClusterSpec{
CloudConfig: &kops.CloudConfiguration{
Openstack: &kops.OpenstackConfiguration{},
},
},
},
instances: []servers.Server{
{
ID: "master1_no_lb_no_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Master",
},
Addresses: map[string]interface{}{
"1": []Address{
{Addr: "1.2.3.4"},
{Addr: "2.3.4.5"},
},
"2": []Address{
{Addr: "3.4.5.6"},
{Addr: "4.5.6.7"},
},
},
},
{
ID: "master2_no_lb_no_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Master",
},
Addresses: map[string]interface{}{
"1": []Address{
{Addr: "10.20.30.40"},
{Addr: "20.30.40.50"},
},
"2": []Address{
{Addr: "30.40.50.60"},
{Addr: "40.50.60.70"},
},
},
},
{
ID: "node_no_lb_no_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Node",
},
Addresses: map[string]interface{}{
"1": []Address{
{Addr: "110.120.130.140", IPType: "floating"},
{Addr: "120.130.140.150", IPType: "private"},
},
},
},
},
expectedAPIIngress: []kops.ApiIngressStatus{
{IP: "1.2.3.4"},
{IP: "2.3.4.5"},
{IP: "3.4.5.6"},
{IP: "4.5.6.7"},
{IP: "10.20.30.40"},
{IP: "20.30.40.50"},
{IP: "30.40.50.60"},
{IP: "40.50.60.70"},
},
},
{
desc: "No Loadbalancer configured with floating enabled",
cluster: &kops.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster.k8s.local",
},
Spec: kops.ClusterSpec{
CloudConfig: &kops.CloudConfiguration{
Openstack: &kops.OpenstackConfiguration{},
},
},
},
instances: []servers.Server{
{
ID: "master1_no_lb_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Master",
},
Addresses: map[string]interface{}{
"1": []map[string]interface{}{
{"Addr": "1.2.3.4", "OS-EXT-IPS:type": "floating"},
{"Addr": "2.3.4.5", "OS-EXT-IPS:type": "private"},
},
"2": []map[string]string{
{"Addr": "3.4.5.6", "OS-EXT-IPS:type": "private"},
{"Addr": "4.5.6.7", "OS-EXT-IPS:type": "floating"},
},
},
},
{
ID: "master2_no_lb_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Master",
},
Addresses: map[string]interface{}{
"1": []map[string]string{
{"Addr": "10.20.30.40", "OS-EXT-IPS:type": "private"},
{"Addr": "20.30.40.50", "OS-EXT-IPS:type": "private"},
},
"2": []map[string]string{
{"Addr": "30.40.50.60", "OS-EXT-IPS:type": "private"},
{"Addr": "40.50.60.70", "OS-EXT-IPS:type": "floating"},
},
},
},
{
ID: "node_no_lb_floating",
Metadata: map[string]string{
"k8s": "cluster.k8s.local",
"KopsRole": "Node",
},
Addresses: map[string]interface{}{
"1": []map[string]string{
{"Addr": "110.120.130.140", "OS-EXT-IPS:type": "floating"},
{"Addr": "120.130.140.150", "OS-EXT-IPS:type": "private"},
},
},
},
},
cloudFloatingEnabled: true,
expectedAPIIngress: []kops.ApiIngressStatus{
{IP: "1.2.3.4"},
{IP: "4.5.6.7"},
{IP: "40.50.60.70"},
},
},
}
for _, testCase := range tests {
t.Run(testCase.desc, func(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
fixture.SetupHandler(
t,
"/servers/detail",
http.MethodGet,
"",
string(mustJSONMarshal(json.Marshal(
struct {
Servers []servers.Server `json:"servers"`
}{
Servers: testCase.instances,
},
))),
http.StatusOK,
)
for _, server := range testCase.instances {
fixture.SetupHandler(
t,
fmt.Sprintf("/servers/%s", server.ID),
http.MethodGet,
"",
string(mustJSONMarshal(json.Marshal(
struct {
Server servers.Server `json:"server"`
}{
Server: server,
},
))),
http.StatusOK,
)
}
fixture.SetupHandler(
t,
"/lbaas/loadbalancers",
http.MethodGet,
"",
string(mustJSONMarshal(json.Marshal(
struct{ LoadBalancers []loadbalancers.LoadBalancer }{
LoadBalancers: testCase.loadbalancers,
},
))),
http.StatusOK,
)
fixture.SetupHandler(
t,
"/os-floating-ips",
http.MethodGet,
"",
string(mustJSONMarshal(json.Marshal(
struct {
FloatingIPs []floatingips.FloatingIP `json:"floating_ips"`
}{
FloatingIPs: testCase.floatingIPs,
},
))),
http.StatusOK,
)
testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Unexpected request for `%v`", r.URL)
})
cloud := &openstackCloud{
floatingEnabled: testCase.cloudFloatingEnabled,
lbClient: client.ServiceClient(),
novaClient: client.ServiceClient(),
}
ingress, err := cloud.GetApiIngressStatus(testCase.cluster)
compareErrors(t, testCase.expectedError, err)
sortedExpected := sortByIP(testCase.expectedAPIIngress)
sortedActual := sortByIP(ingress)
sort.Sort(sortedExpected)
sort.Sort(sortedActual)
if !reflect.DeepEqual(sortedActual, sortedExpected) {
t.Errorf("Ingress status differ: expected\n%+#v\n\tgot:\n%+#v\n", testCase.expectedAPIIngress, ingress)
}
})
}
}
type sortByIP []kops.ApiIngressStatus
// Len is the number of elements in the collection.
func (s sortByIP) Len() int {
return len(s)
}
// Less reports whether the element with
// index i should sort before the element with index j.
func (s sortByIP) Less(i int, j int) bool {
return s[i].IP < s[j].IP
}
// Swap swaps the elements with indexes i and j.
func (s sortByIP) Swap(i int, j int) {
s[i], s[j] = s[j], s[i]
}
type serverList []servers.Server
func (s serverList) Get(id string) *servers.Server {
for _, server := range s {
if server.ID == id {
return &server
}
}
return nil
}
func mustJSONMarshal(data []byte, err error) []byte {
if err != nil {
panic(err)
}
return data
}
func compareErrors(t *testing.T, actual, expected error) {
t.Helper()
if pointersAreBothNil(t, "errors", actual, expected) {
return
}
a := fmt.Sprintf("%v", actual)
e := fmt.Sprintf("%v", expected)
if a != e {
t.Errorf("error differs: %+v instead of %+v", actual, expected)
}
}
func pointersAreBothNil(t *testing.T, name string, actual, expected interface{}) bool {
t.Helper()
if actual == nil && expected == nil {
return true
}
if !reflect.ValueOf(actual).IsValid() {
return false
}
if reflect.ValueOf(actual).IsNil() && reflect.ValueOf(expected).IsNil() {
return true
}
if actual == nil && expected != nil {
t.Fatalf("%s differ: actual is nil, expected is not", name)
}
if actual != nil && expected == nil {
t.Fatalf("%s differ: expected is nil, actual is not", name)
}
return false
}

View File

@ -0,0 +1,13 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"convenience.go",
"doc.go",
"http_responses.go",
],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/testhelper",
importpath = "github.com/gophercloud/gophercloud/testhelper",
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,13 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["fake.go"],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/testhelper/client",
importpath = "github.com/gophercloud/gophercloud/testhelper/client",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/testhelper:go_default_library",
],
)

View File

@ -0,0 +1,17 @@
package client
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/testhelper"
)
// Fake token to use.
const TokenID = "cbc36478b0bd8e67e89469c7749d4127"
// ServiceClient returns a generic service client for use in tests.
func ServiceClient() *gophercloud.ServiceClient {
return &gophercloud.ServiceClient{
ProviderClient: &gophercloud.ProviderClient{TokenID: TokenID},
Endpoint: testhelper.Endpoint(),
}
}

View File

@ -0,0 +1,348 @@
package testhelper
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
const (
logBodyFmt = "\033[1;31m%s %s\033[0m"
greenCode = "\033[0m\033[1;32m"
yellowCode = "\033[0m\033[1;33m"
resetCode = "\033[0m\033[1;31m"
)
func prefix(depth int) string {
_, file, line, _ := runtime.Caller(depth)
return fmt.Sprintf("Failure in %s, line %d:", filepath.Base(file), line)
}
func green(str interface{}) string {
return fmt.Sprintf("%s%#v%s", greenCode, str, resetCode)
}
func yellow(str interface{}) string {
return fmt.Sprintf("%s%#v%s", yellowCode, str, resetCode)
}
func logFatal(t *testing.T, str string) {
t.Fatalf(logBodyFmt, prefix(3), str)
}
func logError(t *testing.T, str string) {
t.Errorf(logBodyFmt, prefix(3), str)
}
type diffLogger func([]string, interface{}, interface{})
type visit struct {
a1 uintptr
a2 uintptr
typ reflect.Type
}
// Recursively visits the structures of "expected" and "actual". The diffLogger function will be
// invoked with each different value encountered, including the reference path that was followed
// to get there.
func deepDiffEqual(expected, actual reflect.Value, visited map[visit]bool, path []string, logDifference diffLogger) {
defer func() {
// Fall back to the regular reflect.DeepEquals function.
if r := recover(); r != nil {
var e, a interface{}
if expected.IsValid() {
e = expected.Interface()
}
if actual.IsValid() {
a = actual.Interface()
}
if !reflect.DeepEqual(e, a) {
logDifference(path, e, a)
}
}
}()
if !expected.IsValid() && actual.IsValid() {
logDifference(path, nil, actual.Interface())
return
}
if expected.IsValid() && !actual.IsValid() {
logDifference(path, expected.Interface(), nil)
return
}
if !expected.IsValid() && !actual.IsValid() {
return
}
hard := func(k reflect.Kind) bool {
switch k {
case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
return true
}
return false
}
if expected.CanAddr() && actual.CanAddr() && hard(expected.Kind()) {
addr1 := expected.UnsafeAddr()
addr2 := actual.UnsafeAddr()
if addr1 > addr2 {
addr1, addr2 = addr2, addr1
}
if addr1 == addr2 {
// References are identical. We can short-circuit
return
}
typ := expected.Type()
v := visit{addr1, addr2, typ}
if visited[v] {
// Already visited.
return
}
// Remember this visit for later.
visited[v] = true
}
switch expected.Kind() {
case reflect.Array:
for i := 0; i < expected.Len(); i++ {
hop := append(path, fmt.Sprintf("[%d]", i))
deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
}
return
case reflect.Slice:
if expected.IsNil() != actual.IsNil() {
logDifference(path, expected.Interface(), actual.Interface())
return
}
if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
return
}
for i := 0; i < expected.Len(); i++ {
hop := append(path, fmt.Sprintf("[%d]", i))
deepDiffEqual(expected.Index(i), actual.Index(i), visited, hop, logDifference)
}
return
case reflect.Interface:
if expected.IsNil() != actual.IsNil() {
logDifference(path, expected.Interface(), actual.Interface())
return
}
deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
return
case reflect.Ptr:
deepDiffEqual(expected.Elem(), actual.Elem(), visited, path, logDifference)
return
case reflect.Struct:
for i, n := 0, expected.NumField(); i < n; i++ {
field := expected.Type().Field(i)
hop := append(path, "."+field.Name)
deepDiffEqual(expected.Field(i), actual.Field(i), visited, hop, logDifference)
}
return
case reflect.Map:
if expected.IsNil() != actual.IsNil() {
logDifference(path, expected.Interface(), actual.Interface())
return
}
if expected.Len() == actual.Len() && expected.Pointer() == actual.Pointer() {
return
}
var keys []reflect.Value
if expected.Len() >= actual.Len() {
keys = expected.MapKeys()
} else {
keys = actual.MapKeys()
}
for _, k := range keys {
expectedValue := expected.MapIndex(k)
actualValue := actual.MapIndex(k)
if !expectedValue.IsValid() {
logDifference(path, nil, actual.Interface())
return
}
if !actualValue.IsValid() {
logDifference(path, expected.Interface(), nil)
return
}
hop := append(path, fmt.Sprintf("[%v]", k))
deepDiffEqual(expectedValue, actualValue, visited, hop, logDifference)
}
return
case reflect.Func:
if expected.IsNil() != actual.IsNil() {
logDifference(path, expected.Interface(), actual.Interface())
}
return
default:
if expected.Interface() != actual.Interface() {
logDifference(path, expected.Interface(), actual.Interface())
}
}
}
func deepDiff(expected, actual interface{}, logDifference diffLogger) {
if expected == nil || actual == nil {
logDifference([]string{}, expected, actual)
return
}
expectedValue := reflect.ValueOf(expected)
actualValue := reflect.ValueOf(actual)
if expectedValue.Type() != actualValue.Type() {
logDifference([]string{}, expected, actual)
return
}
deepDiffEqual(expectedValue, actualValue, map[visit]bool{}, []string{}, logDifference)
}
// AssertEquals compares two arbitrary values and performs a comparison. If the
// comparison fails, a fatal error is raised that will fail the test
func AssertEquals(t *testing.T, expected, actual interface{}) {
if expected != actual {
logFatal(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
// CheckEquals is similar to AssertEquals, except with a non-fatal error
func CheckEquals(t *testing.T, expected, actual interface{}) {
if expected != actual {
logError(t, fmt.Sprintf("expected %s but got %s", green(expected), yellow(actual)))
}
}
// AssertDeepEquals - like Equals - performs a comparison - but on more complex
// structures that requires deeper inspection
func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
pre := prefix(2)
differed := false
deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
differed = true
t.Errorf("\033[1;31m%sat %s expected %s, but got %s\033[0m",
pre,
strings.Join(path, ""),
green(expected),
yellow(actual))
})
if differed {
logFatal(t, "The structures were different.")
}
}
// CheckDeepEquals is similar to AssertDeepEquals, except with a non-fatal error
func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
pre := prefix(2)
deepDiff(expected, actual, func(path []string, expected, actual interface{}) {
t.Errorf("\033[1;31m%s at %s expected %s, but got %s\033[0m",
pre,
strings.Join(path, ""),
green(expected),
yellow(actual))
})
}
func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool {
return bytes.Equal(expectedBytes, actualBytes)
}
// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal
func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
if !isByteArrayEquals(t, expectedBytes, actualBytes) {
logFatal(t, "The bytes differed.")
}
}
// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal
func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
if !isByteArrayEquals(t, expectedBytes, actualBytes) {
logError(t, "The bytes differed.")
}
}
// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and
// CheckJSONEquals.
func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool {
var parsedExpected, parsedActual interface{}
err := json.Unmarshal([]byte(expectedJSON), &parsedExpected)
if err != nil {
t.Errorf("Unable to parse expected value as JSON: %v", err)
return false
}
jsonActual, err := json.Marshal(actual)
AssertNoErr(t, err)
err = json.Unmarshal(jsonActual, &parsedActual)
AssertNoErr(t, err)
if !reflect.DeepEqual(parsedExpected, parsedActual) {
prettyExpected, err := json.MarshalIndent(parsedExpected, "", " ")
if err != nil {
t.Logf("Unable to pretty-print expected JSON: %v\n%s", err, expectedJSON)
} else {
// We can't use green() here because %#v prints prettyExpected as a byte array literal, which
// is... unhelpful. Converting it to a string first leaves "\n" uninterpreted for some reason.
t.Logf("Expected JSON:\n%s%s%s", greenCode, prettyExpected, resetCode)
}
prettyActual, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Logf("Unable to pretty-print actual JSON: %v\n%#v", err, actual)
} else {
// We can't use yellow() for the same reason.
t.Logf("Actual JSON:\n%s%s%s", yellowCode, prettyActual, resetCode)
}
return false
}
return true
}
// AssertJSONEquals serializes a value as JSON, parses an expected string as JSON, and ensures that
// both are consistent. If they aren't, the expected and actual structures are pretty-printed and
// shown for comparison.
//
// This is useful for comparing structures that are built as nested map[string]interface{} values,
// which are a pain to construct as literals.
func AssertJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
if !isJSONEquals(t, expectedJSON, actual) {
logFatal(t, "The generated JSON structure differed.")
}
}
// CheckJSONEquals is similar to AssertJSONEquals, but nonfatal.
func CheckJSONEquals(t *testing.T, expectedJSON string, actual interface{}) {
if !isJSONEquals(t, expectedJSON, actual) {
logError(t, "The generated JSON structure differed.")
}
}
// AssertNoErr is a convenience function for checking whether an error value is
// an actual error
func AssertNoErr(t *testing.T, e error) {
if e != nil {
logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
}
}
// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
func CheckNoErr(t *testing.T, e error) {
if e != nil {
logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
}
}

View File

@ -0,0 +1,4 @@
/*
Package testhelper container methods that are useful for writing unit tests.
*/
package testhelper

View File

@ -0,0 +1,13 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["helper.go"],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/testhelper/fixture",
importpath = "github.com/gophercloud/gophercloud/testhelper/fixture",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud/testhelper:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/testhelper/client:go_default_library",
],
)

View File

@ -0,0 +1,31 @@
package fixture
import (
"fmt"
"net/http"
"testing"
th "github.com/gophercloud/gophercloud/testhelper"
"github.com/gophercloud/gophercloud/testhelper/client"
)
func SetupHandler(t *testing.T, url, method, requestBody, responseBody string, status int) {
th.Mux.HandleFunc(url, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, method)
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
if requestBody != "" {
th.TestJSONRequest(t, r, requestBody)
}
if responseBody != "" {
w.Header().Add("Content-Type", "application/json")
}
w.WriteHeader(status)
if responseBody != "" {
fmt.Fprintf(w, responseBody)
}
})
}

View File

@ -0,0 +1,91 @@
package testhelper
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
)
var (
// Mux is a multiplexer that can be used to register handlers.
Mux *http.ServeMux
// Server is an in-memory HTTP server for testing.
Server *httptest.Server
)
// SetupHTTP prepares the Mux and Server.
func SetupHTTP() {
Mux = http.NewServeMux()
Server = httptest.NewServer(Mux)
}
// TeardownHTTP releases HTTP-related resources.
func TeardownHTTP() {
Server.Close()
}
// Endpoint returns a fake endpoint that will actually target the Mux.
func Endpoint() string {
return Server.URL + "/"
}
// TestFormValues ensures that all the URL parameters given to the http.Request are the same as values.
func TestFormValues(t *testing.T, r *http.Request, values map[string]string) {
want := url.Values{}
for k, v := range values {
want.Add(k, v)
}
r.ParseForm()
if !reflect.DeepEqual(want, r.Form) {
t.Errorf("Request parameters = %v, want %v", r.Form, want)
}
}
// TestMethod checks that the Request has the expected method (e.g. GET, POST).
func TestMethod(t *testing.T, r *http.Request, expected string) {
if expected != r.Method {
t.Errorf("Request method = %v, expected %v", r.Method, expected)
}
}
// TestHeader checks that the header on the http.Request matches the expected value.
func TestHeader(t *testing.T, r *http.Request, header string, expected string) {
if actual := r.Header.Get(header); expected != actual {
t.Errorf("Header %s = %s, expected %s", header, actual, expected)
}
}
// TestBody verifies that the request body matches an expected body.
func TestBody(t *testing.T, r *http.Request, expected string) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Unable to read body: %v", err)
}
str := string(b)
if expected != str {
t.Errorf("Body = %s, expected %s", str, expected)
}
}
// TestJSONRequest verifies that the JSON payload of a request matches an expected structure, without asserting things about
// whitespace or ordering.
func TestJSONRequest(t *testing.T, r *http.Request, expected string) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Unable to read request body: %v", err)
}
var actualJSON interface{}
err = json.Unmarshal(b, &actualJSON)
if err != nil {
t.Errorf("Unable to parse request body as JSON: %v", err)
}
CheckJSONEquals(t, expected, actualJSON)
}

3
vendor/modules.txt vendored
View File

@ -276,6 +276,9 @@ github.com/gophercloud/gophercloud/internal
github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies
github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts
github.com/gophercloud/gophercloud/openstack/identity/v2/tenants
github.com/gophercloud/gophercloud/testhelper
github.com/gophercloud/gophercloud/testhelper/client
github.com/gophercloud/gophercloud/testhelper/fixture
# github.com/gorilla/mux v1.7.0
github.com/gorilla/mux
# github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7