blob: e022a8726488b28145223482a01378cc46aa174a [file] [log] [blame]
// Package utils provides helpful utilities for easing the process of testing
// Traffic Ops API clients. The functions here don't depend on any particular
// API version, so they need not be copy/pasted along with the entire testing
// suites.
package utils
/*
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.
*/
import (
"encoding/json"
"net/http"
"net/url"
"reflect"
"sort"
"testing"
"time"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util/assert"
"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
v3client "github.com/apache/trafficcontrol/traffic_ops/v3-client"
v4client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
v5client "github.com/apache/trafficcontrol/traffic_ops/v5-client"
)
// FindNeedle searches a "haystack" slice of values for the the "needle" value,
// returning true if the value is in the haystack, false otherwise.
func FindNeedle[T comparable](needle T, haystack []T) bool {
found := false
for _, s := range haystack {
if s == needle {
found = true
break
}
}
return found
}
// ErrorsToStrings converts a slice of errors to a slice of their error
// messages.
func ErrorsToStrings(errs []error) []string {
errorStrs := make([]string, 0, len(errs))
for _, err := range errs {
errorStrs = append(errorStrs, err.Error())
}
return errorStrs
}
// Compare compares a set of expected alert messages to those actually received.
// It checks for the existence of each expected alert, not that they appear in
// any particular order. It also checks that no unexpected alert strings exist.
// Note that this isn't particularly efficient, which is fine because it's only
// meant to be used in testing.
func Compare(t *testing.T, expected []string, alertsStrs []string) {
t.Helper()
sort.Strings(alertsStrs)
expectedFmt, _ := json.MarshalIndent(expected, "", " ")
errorsFmt, _ := json.MarshalIndent(alertsStrs, "", " ")
var found bool
// Compare both directions
for _, s := range alertsStrs {
found = FindNeedle(s, expected)
if !found {
t.Errorf("\nExpected %s and \n Actual %v must match exactly", string(expectedFmt), string(errorsFmt))
break
}
}
found = false
if !found {
// Compare both directions
for _, s := range expected {
found = FindNeedle(s, expected)
if !found {
t.Errorf("\nExpected %s and \n Actual %v must match exactly", string(expectedFmt), string(errorsFmt))
break
}
}
}
}
// CreateV3Session creates a session for client v4 using the passed in username and password.
func CreateV3Session(t *testing.T, trafficOpsURL string, username string, password string, toReqTimeout int) *v3client.Session {
t.Helper()
userSession, _, err := v3client.LoginWithAgent(trafficOpsURL, username, password, true, "to-api-v3-client-tests", false, time.Second*time.Duration(toReqTimeout))
assert.RequireNoError(t, err, "Could not login with user %v: %v", username, err)
return userSession
}
// CreateV4Session creates a session for client v4 using the passed in username and password.
func CreateV4Session(t *testing.T, trafficOpsURL string, username string, password string, toReqTimeout int) *v4client.Session {
t.Helper()
userSession, _, err := v4client.LoginWithAgent(trafficOpsURL, username, password, true, "to-api-v4-client-tests", false, time.Second*time.Duration(toReqTimeout))
assert.RequireNoError(t, err, "Could not login with user %v: %v", username, err)
return userSession
}
// CreateV5Session creates a session for client v5 using the passed in username and password.
func CreateV5Session(t *testing.T, trafficOpsURL, username, password string, toReqTimeout int) *v5client.Session {
t.Helper()
userSession, _, err := v5client.LoginWithAgent(trafficOpsURL, username, password, true, "to-api-v5-client-tests", false, time.Second*time.Duration(toReqTimeout))
assert.RequireNoError(t, err, "Could not login with user %v: %v", username, err)
return userSession
}
// V3TestData represents the data needed for testing the v3 api endpoints.
type V3TestData struct {
EndpointID func() int
ClientSession *v3client.Session
RequestParams url.Values
RequestHeaders http.Header
RequestBody map[string]interface{}
PreReqFuncs []func()
Expectations []CkReqFunc
}
// V3TestDataT represents the data needed for testing the v3 api endpoints.
type V3TestDataT[B any] struct {
EndpointID func() int
ClientSession *v3client.Session
RequestParams url.Values
RequestHeaders http.Header
RequestBody B
Expectations []CkReqFunc
}
// V4TestData represents the data needed for testing the v4 api endpoints.
type V4TestData struct {
EndpointID func() int
ClientSession *v4client.Session
RequestOpts v4client.RequestOptions
RequestBody map[string]interface{}
PreReqFuncs []func()
Expectations []CkReqFunc
}
// V5TestData represents the data needed for testing the v5 api endpoints.
type V5TestData struct {
EndpointID func() int
ClientSession *v5client.Session
RequestOpts v5client.RequestOptions
RequestBody map[string]interface{}
PreReqFuncs []func()
Expectations []CkReqFunc
}
type clientSession interface {
v4client.Session | v5client.Session
}
type requestOpts interface {
v4client.RequestOptions | v5client.RequestOptions
}
// TestData represents the data needed for testing the api endpoints.
type TestData[C clientSession, R requestOpts, B any] struct {
EndpointID func() int
ClientSession *C
RequestOpts R
RequestBody B
Expectations []CkReqFunc
}
// V3TestCase is the type of the V3TestData struct.
// Uses nested map to represent the method being tested and the test's description.
type V3TestCase map[string]map[string]V3TestData
// V3TestCaseT is the type of the V3TestDataT struct.
// Uses nested map to represent the method being tested and the test's description.
type V3TestCaseT[B any] map[string]map[string]V3TestDataT[B]
// V4TestCase is the type of the V4TestData struct.
// Uses nested map to represent the method being tested and the test's description.
type V4TestCase map[string]map[string]V4TestData
// V5TestCase is a map of test names to maps of HTTP request method descriptions
// to V5TestData structures.
// Uses nested map to represent the method being tested and the test's description.
type V5TestCase map[string]map[string]V5TestData
type TestCase[C clientSession, R requestOpts, B any] map[string]map[string]TestData[C, R, B]
// CkReqFunc defines the reusable signature for all other functions that perform checks.
// Common parameters that are checked include the request's info, response, alerts, and errors.
type CkReqFunc func(*testing.T, toclientlib.ReqInf, interface{}, tc.Alerts, error)
// CkRequest wraps CkReqFunc functions, to be concise and reduce lexical tokens
// i.e. Instead of `[]CkReqFunc {` we can use `CkRequest(` in test case declarations.
func CkRequest(c ...CkReqFunc) []CkReqFunc {
return c
}
// NoError checks that no error was returned (i.e. `nil`).
func NoError() CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, _ interface{}, _ tc.Alerts, err error) {
t.Helper()
assert.NoError(t, err, "Expected no error. Got: %v", err)
}
}
// HasError checks that an error was returned (i.e. not `nil`).
func HasError() CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, _ interface{}, alerts tc.Alerts, err error) {
t.Helper()
assert.Error(t, err, "Expected error. Got: %v", alerts)
}
}
// HasStatus checks that the status code from the request is as expected.
func HasStatus(expectedStatus int) CkReqFunc {
return func(t *testing.T, reqInf toclientlib.ReqInf, _ interface{}, _ tc.Alerts, _ error) {
t.Helper()
assert.Equal(t, expectedStatus, reqInf.StatusCode, "Expected Status Code: %d Got: %d", expectedStatus, reqInf.StatusCode)
}
}
// HasAlertLevel checks that the alert from the request matches the expected level.
func HasAlertLevel(expectedAlertLevel string) CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, _ interface{}, alerts tc.Alerts, _ error) {
t.Helper()
assert.RequireNotNil(t, alerts, "Expected alerts to not be nil.")
found := false
for _, alert := range alerts.Alerts {
if expectedAlertLevel == alert.Level {
found = true
}
}
assert.Equal(t, true, found, "Expected to find Alert Level: %s", expectedAlertLevel)
}
}
// ResponseHasLength checks that the length of the response is as expected.
// Determines that response is a slice before checking the length of the reflected value.
func ResponseHasLength(expected int) CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
t.Helper()
rt := reflect.TypeOf(resp)
switch rt.Kind() {
case reflect.Slice:
actual := reflect.ValueOf(resp).Len()
assert.Equal(t, expected, actual, "Expected response object length: %d Got: %d", expected, actual)
default:
t.Errorf("Expected response to be an array. Got: %v", rt)
}
}
}
// ResponseLengthGreaterOrEqual checks that the response is greater or equal to the expected length.
// Determines that response is a slice before checking the length of the reflected value.
func ResponseLengthGreaterOrEqual(expected int) CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
t.Helper()
rt := reflect.TypeOf(resp)
switch rt.Kind() {
case reflect.Slice:
actual := reflect.ValueOf(resp).Len()
assert.GreaterOrEqual(t, actual, expected, "Expected response object length: %d Got: %d", expected, actual)
default:
t.Errorf("Expected response to be an array. Got: %v", rt)
}
}
}