blob: 964a2d71a0824592148a1fd90d23b7e2db919588 [file] [log] [blame]
package v5
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 (
"net/http"
"net/url"
"testing"
"time"
"github.com/apache/trafficcontrol/lib/go-rfc"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/lib/go-util/assert"
"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
client "github.com/apache/trafficcontrol/traffic_ops/v5-client"
)
func TestTopologies(t *testing.T) {
WithObjs(t, []TCObj{Tenants, Users, Types, CacheGroups, CDNs, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, ServerCapabilities, ServerServerCapabilities, Topologies, ServiceCategories, DeliveryServices, DeliveryServicesRequiredCapabilities, DeliveryServiceServerAssignments}, func() {
readOnlyUserSession := utils.CreateV5Session(t, Config.TrafficOps.URL, "readonlyuser", "pa$$word", Config.Default.Session.TimeoutInSecs)
currentTime := time.Now().UTC().Add(-15 * time.Second)
currentTimeRFC := currentTime.Format(time.RFC1123)
tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.Topology]{
"GET": {
"NOT MODIFIED when NO CHANGES made": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {tomorrow}}},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)),
},
"OK when CHANGES made": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {currentTimeRFC}}},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
},
"OK when READ ONLY USER": {
ClientSession: readOnlyUserSession,
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1)),
},
"EMPTY RESPONSE when TOPOLOGY DOESNT EXIST": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"non-existent-topology"}}},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(0)),
},
},
"POST": {
"OK when MISSING DESCRIPTION": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-missing-description",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
},
"BAD REQUEST when EMPTY NODES": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-no-nodes",
Nodes: []tc.TopologyNode{},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when NODE PARENT of ITSELF": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "self-parent",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{0}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when TOO MANY PARENTS": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "too-many-parents",
Nodes: []tc.TopologyNode{
{
Cachegroup: "parentCachegroup",
Parents: []int{},
},
{
Cachegroup: "secondaryCachegroup",
Parents: []int{},
},
{
Cachegroup: "parentCachegroup2",
Parents: []int{},
},
{
Cachegroup: "cachegroup1",
Parents: []int{0, 1, 2},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when EDGE_LOC PARENTS MID_LOC": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "edge-parents-mid",
Nodes: []tc.TopologyNode{
{
Cachegroup: "parentCachegroup",
Parents: []int{1},
},
{
Cachegroup: "cachegroup2",
Parents: []int{},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CYCLICAL NODES": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "cyclical-nodes",
Nodes: []tc.TopologyNode{
{
Cachegroup: "cachegroup1",
Parents: []int{1, 2},
},
{
Cachegroup: "parentCachegroup",
Parents: []int{2},
},
{
Cachegroup: "secondaryCachegroup",
Parents: []int{1},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CYCLES ACROSS TOPOLOGIES": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "cyclical-nodes-tiered",
Nodes: []tc.TopologyNode{
{
Cachegroup: "parentCachegroup",
Parents: []int{1},
},
{
Cachegroup: "parentCachegroup2",
Parents: []int{},
},
{
Cachegroup: "cachegroup1",
Parents: []int{0},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CYCLICAL NODES BUT EMPTY CACHE GROUPS": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "cyclical-nodes-nontopology",
Nodes: []tc.TopologyNode{
{
Cachegroup: "edge-parent1",
Parents: []int{1},
},
{
Cachegroup: "has-edge-parent1",
Parents: []int{},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when OUT-OF-BOUNDS PARENT INDEX": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "outofbounds",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{7}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CACHEGROUP DOESNT EXIST": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-nonexistent-cachegroup",
Nodes: []tc.TopologyNode{{Cachegroup: "doesntexist", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when MISSING NAME": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Description: "missing name",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when ALREADY EXISTS": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "mso-topology",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CACHEGROUP has NO SERVERS": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-empty-cg",
Nodes: []tc.TopologyNode{{Cachegroup: "noServers", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when DUPLICATE PARENTS": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-duplicate-parents",
Nodes: []tc.TopologyNode{
{
Cachegroup: "parentCachegroup",
Parents: []int{},
},
{
Cachegroup: "cachegroup1",
Parents: []int{0, 0},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when ORG_LOC is CHILD NODE": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-orgloc-child",
Nodes: []tc.TopologyNode{
{
Cachegroup: "cachegroup1",
Parents: []int{},
},
{
Cachegroup: "multiOriginCachegroup",
Parents: []int{0},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when LEAF NODE is a MID_LOC": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-midloc-leaf",
Nodes: []tc.TopologyNode{{Cachegroup: "parentCachegroup", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"WARNING LEVEL ALERT when MID PARENTING EDGE": {
ClientSession: TOSession,
RequestBody: tc.Topology{
Name: "topology-mid-parent",
Description: "mid parent to edge",
Nodes: []tc.TopologyNode{
{
Cachegroup: "cachegroup1",
Parents: []int{1},
},
{
Cachegroup: "cachegroup2",
Parents: []int{},
},
},
},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.HasAlertLevel(tc.WarnLevel.String())),
},
"FORBIDDEN when READ-ONLY USER": {
ClientSession: readOnlyUserSession,
RequestBody: tc.Topology{
Name: "topology-ro",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusForbidden)),
},
},
"PUT": {
"OK when VALID request": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"top-with-no-mids"}}},
RequestBody: tc.Topology{
Name: "top-with-no-mids-updated",
Description: "Updating fields",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup2", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
validateTopologiesUpdateCreateFields(map[string]interface{}{"Name": "top-with-no-mids-updated", "Description": "Updating fields"})),
},
"BAD REQUEST when OUT-OF-BOUNDS PARENT INDEX": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-invalid-parent",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{100}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CACHEGROUP has NO SERVERS": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-empty-cg",
Nodes: []tc.TopologyNode{{Cachegroup: "noServers", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CACHEGROUP SERVERS DO NOT HAVE REQUIRED CAPABILITIES": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"top-for-ds-req"}}},
RequestBody: tc.Topology{
Name: "top-for-ds-req",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when NODE PARENT of ITSELF": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "another-topology",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{0}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when CACHEGROUP HAS NO SERVERS IN TOPOLOGY CDN": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"top-used-by-cdn1-and-cdn2"}}},
RequestBody: tc.Topology{
Name: "top-used-by-cdn1-and-cdn2",
Nodes: []tc.TopologyNode{{Cachegroup: "cdn1-only", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when ORG_LOC is CHILD NODE": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-orgloc-child",
Nodes: []tc.TopologyNode{
{
Cachegroup: "cachegroup1",
Parents: []int{},
},
{
Cachegroup: "multiOriginCachegroup",
Parents: []int{0},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when LEAF NODE is a MID_LOC": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-child-midloc",
Description: "child mid_loc",
Nodes: []tc.TopologyNode{{Cachegroup: "parentCachegroup", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when DUPLICATE PARENTS": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-same-parents",
Nodes: []tc.TopologyNode{
{
Cachegroup: "cachegroup1",
Parents: []int{},
},
{
Cachegroup: "cachegroup2",
Parents: []int{0, 0},
},
},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when REMOVING ORG ASSIGNED DS": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"mso-topology"}}},
RequestBody: tc.Topology{
Name: "mso-topology",
Nodes: []tc.TopologyNode{{Cachegroup: "topology-edge-cg-01", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"FORBIDDEN when READ-ONLY USER": {
ClientSession: readOnlyUserSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"another-topology"}}},
RequestBody: tc.Topology{
Name: "topology-ro",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusForbidden)),
},
"PRECONDITION FAILED when updating with IMS & IUS Headers": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{
QueryParameters: url.Values{"name": {"another-topology"}},
Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
},
RequestBody: tc.Topology{
Name: "another-topology",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
},
"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{
QueryParameters: url.Values{"name": {"another-topology"}},
Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
},
RequestBody: tc.Topology{
Name: "another-topology",
Nodes: []tc.TopologyNode{{Cachegroup: "cachegroup1", Parents: []int{}}},
},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
},
},
"DELETE": {
"BAD REQUEST when TOPOLOGY DOESNT EXIST": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"non-existent-topology"}}},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"BAD REQUEST when TOPOLOGY IN USE by DELIVERY SERVICE": {
ClientSession: TOSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"mso-topology"}}},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
},
"FORBIDDEN when READ-ONLY USER": {
ClientSession: readOnlyUserSession,
RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"mso-topology"}}},
Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusForbidden)),
},
},
}
for method, testCases := range methodTests {
t.Run(method, func(t *testing.T) {
for name, testCase := range testCases {
topology := testCase.RequestBody
switch method {
case "GET", "GET AFTER CHANGES":
t.Run(name, func(t *testing.T) {
resp, reqInf, err := testCase.ClientSession.GetTopologies(testCase.RequestOpts)
for _, check := range testCase.Expectations {
check(t, reqInf, resp.Response, resp.Alerts, err)
}
})
case "POST":
t.Run(name, func(t *testing.T) {
resp, reqInf, err := testCase.ClientSession.CreateTopology(topology, testCase.RequestOpts)
for _, check := range testCase.Expectations {
check(t, reqInf, resp.Response, resp.Alerts, err)
}
})
case "PUT":
t.Run(name, func(t *testing.T) {
if _, ok := testCase.RequestOpts.QueryParameters["name"]; !ok {
t.Fatalf("Query Parameter: \"name\" is required for PUT method tests.")
}
resp, reqInf, err := testCase.ClientSession.UpdateTopology(testCase.RequestOpts.QueryParameters["name"][0], topology, testCase.RequestOpts)
for _, check := range testCase.Expectations {
check(t, reqInf, resp.Response, resp.Alerts, err)
}
})
case "DELETE":
t.Run(name, func(t *testing.T) {
if _, ok := testCase.RequestOpts.QueryParameters["name"]; !ok {
t.Fatalf("Query Parameter: \"name\" is required for DELETE method tests.")
}
alerts, reqInf, err := testCase.ClientSession.DeleteTopology(testCase.RequestOpts.QueryParameters["name"][0], testCase.RequestOpts)
for _, check := range testCase.Expectations {
check(t, reqInf, nil, alerts, err)
}
})
default:
t.Errorf("Method: %s, is not a valid test method.", method)
}
}
})
}
})
}
func validateTopologiesFields(expectedResp map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Topology response to not be nil.")
topologyResp := resp.([]tc.Topology)
for field, expected := range expectedResp {
for _, topology := range topologyResp {
switch field {
case "Description":
assert.Equal(t, expected, topology.Description, "Expected Description to be %v, but got %s", expected, topology.Description)
case "Name":
assert.Equal(t, expected, topology.Name, "Expected Name to be %v, but got %s", expected, topology.Name)
default:
t.Errorf("Expected field: %v, does not exist in response", field)
}
}
}
}
}
func validateTopologiesUpdateCreateFields(expectedResp map[string]interface{}) utils.CkReqFunc {
return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
assert.RequireNotNil(t, resp, "Expected Topology response to not be nil.")
topology := resp.(tc.Topology)
topologyResp := []tc.Topology{topology}
validateTopologiesFields(expectedResp)(t, toclientlib.ReqInf{}, topologyResp, tc.Alerts{}, nil)
}
}
func CreateTestTopologies(t *testing.T) {
for _, topology := range testData.Topologies {
resp, _, err := TOSession.CreateTopology(topology, client.RequestOptions{})
assert.RequireNoError(t, err, "Could not create Topology: %v - alerts: %+v", err, resp.Alerts)
}
}
func DeleteTestTopologies(t *testing.T) {
topologies, _, err := TOSession.GetTopologies(client.RequestOptions{})
assert.NoError(t, err, "Cannot get Topologies: %v - alerts: %+v", err, topologies.Alerts)
for _, topology := range topologies.Response {
alerts, _, err := TOSession.DeleteTopology(topology.Name, client.RequestOptions{})
assert.NoError(t, err, "Cannot delete Topology: %v - alerts: %+v", err, alerts.Alerts)
// Retrieve the Topology to see if it got deleted
opts := client.NewRequestOptions()
opts.QueryParameters.Set("name", topology.Name)
resp, _, err := TOSession.GetTopologies(opts)
assert.NoError(t, err, "Unexpected error trying to fetch Topologies after deletion: %v - alerts: %+v", err, resp.Alerts)
assert.Equal(t, 0, len(resp.Response), "Expected Topology '%s' to be deleted, but it was found in Traffic Ops", topology.Name)
}
}