You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@trafficcontrol.apache.org by GitBox <gi...@apache.org> on 2022/09/02 22:13:37 UTC

[GitHub] [trafficcontrol] ericholguin opened a new pull request, #7056: Refactor Topologies Tests

ericholguin opened a new pull request, #7056:
URL: https://github.com/apache/trafficcontrol/pull/7056

   <!--
   Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
   If this closes or relates to an existing issue, please reference it using one of the following:
   
   Closes: #ISSUE
   Related: #ISSUE
   
   If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
   the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
   guidelines at https://apache.org/security regarding vulnerability disclosure.
   -->
   
   This PR refactors **v4/topologies_test.go**, **v3/topologies_test.go**, in order to make tests more granular, providing transparency on what is being tested and reducing the need to dig through test code to confirm what was tested. In addition this new test structure should also make it easier for new tests to be added as well as reduce redundant code.
   
   - Adds data driven testing
       - Descriptive test names
   - Sub tests for organization
       - Use GoLangs test runner
   - Use assert functionality
       - Provides fundamental assertions
       - Streamlines code
       - Reduces nested conditionals
   - Adds expectation functions
        - Test specific expectations in a clear concise way
        - Reusable expectations
   
   
   
   <!-- **^ Add meaningful description above** --><hr/>
   
   ## Which Traffic Control components are affected by this PR?
   <!-- Please delete all components from this list that are NOT affected by this PR.
   Feel free to add the name of a tool or script that is affected but not on the list.
   -->
   
   - Traffic Ops Tests
   
   ## What is the best way to verify this PR?
   <!-- Please include here ALL the steps necessary to test your PR.
   If your PR has tests (and most should), provide the steps needed to run the tests.
   If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->
   
   Run TO Integration tests
   
   
   ## PR submission checklist
   - [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
   - [ ] This PR has documentation <!-- If not, please delete this text and explain why this PR does not need documentation. -->
   - [ ] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
   - [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)
   
   <!--
   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.
   -->
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [trafficcontrol] zrhoffman merged pull request #7056: Refactor Topologies Tests

Posted by GitBox <gi...@apache.org>.
zrhoffman merged PR #7056:
URL: https://github.com/apache/trafficcontrol/pull/7056


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


[GitHub] [trafficcontrol] zrhoffman commented on a diff in pull request #7056: Refactor Topologies Tests

Posted by GitBox <gi...@apache.org>.
zrhoffman commented on code in PR #7056:
URL: https://github.com/apache/trafficcontrol/pull/7056#discussion_r977467947


##########
traffic_ops/testing/api/v3/topologies_test.go:
##########
@@ -20,316 +20,468 @@ package v3
  */
 
 import (
-	"fmt"
+	"encoding/json"
+	"net/http"
 	"net/url"
-	"reflect"
-	"strconv"
-	"strings"
 	"testing"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/traffic_ops/testing/api/assert"
+	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
+	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
 )
 
-type topologyTestCase struct {
-	testCaseDescription string
-	tc.Topology
-}
-
 func TestTopologies(t *testing.T) {
-	WithObjs(t, []TCObj{Types, CacheGroups, CDNs, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, ServerCapabilities, ServerServerCapabilities, Topologies, Tenants, DeliveryServices, DeliveryServicesRequiredCapabilities}, func() {
-		GetTestTopologies(t)
-		UpdateTestTopologies(t)
-		ValidationTestTopologies(t)
-		UpdateValidateTopologyORGServerCacheGroup(t)
-		EdgeParentOfEdgeSucceedsWithWarning(t)
-	})
-}
+	WithObjs(t, []TCObj{Tenants, Users, Types, CacheGroups, CDNs, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, ServerCapabilities, ServerServerCapabilities, Topologies, ServiceCategories, DeliveryServices, DeliveryServicesRequiredCapabilities, DeliveryServiceServerAssignments}, func() {
 
-func CreateTestTopologies(t *testing.T) {
-	var (
-		postResponse *tc.TopologyResponse
-		err          error
-	)
-	for _, topology := range testData.Topologies {
-		if postResponse, _, err = TOSession.CreateTopology(topology); err != nil {
-			t.Fatalf("could not CREATE topology: %v", err)
-		}
-		postResponse.Response.LastUpdated = nil
-		if !reflect.DeepEqual(topology, postResponse.Response) {
-			t.Fatalf("Topology in response should be the same as the one POSTed. expected: %v\nactual: %v", topology, postResponse.Response)
+		methodTests := utils.V3TestCase{
+			"GET": {
+				"OK when VALID REQUEST": {
+					ClientSession: TOSession,
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1)),
+				},
+			},
+			"POST": {
+				"OK when MISSING DESCRIPTION": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-missing-description",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+				"BAD REQUEST when EMPTY NODES": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-no-nodes",
+						"nodes": []map[string]interface{}{},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when NODE PARENT of ITSELF": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "self-parent",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{0}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when TOO MANY PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "too-many-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "edge-parents-mid",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-tiered",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-nontopology",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "outofbounds",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{7}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP DOESNT EXIST": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-nonexistent-cachegroup",
+						"nodes": []map[string]interface{}{{"cachegroup": "doesntexist", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when MISSING NAME": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"description": "missing name",
+						"nodes":       []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when ALREADY EXISTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP has NO SERVERS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"cachegroup": "noServers", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when DUPLICATE PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "topology-duplicate-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "topology-midloc-leaf",
+						"nodes": []map[string]interface{}{{"cachegroup": "parentCachegroup", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"WARNING LEVEL ALERT when MID PARENTING EDGE": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":        "topology-mid-parent",
+						"description": "mid parent to edge",
+						"nodes": []map[string]interface{}{
+							{
+								"cachegroup": "cachegroup1",
+								"parents":    []int{1},
+							},
+							{
+								"cachegroup": "cachegroup2",
+								"parents":    []int{},
+							},
+						},
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.HasAlertLevel(tc.WarnLevel.String())),
+				},
+			},
+			"PUT": {
+				"OK when VALID request": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"top-with-no-mids"}},
+					RequestBody: map[string]interface{}{
+						"name":        "top-with-no-mids-updated",
+						"description": "Updating fields",
+						"nodes":       []map[string]interface{}{{"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,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name":  "topology-invalid-parent",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{100}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP has NO SERVERS": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"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,
+					RequestParams: url.Values{"name": {"top-for-ds-req"}},
+					RequestBody: map[string]interface{}{
+						"name":  "top-for-ds-req",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when NODE PARENT of ITSELF": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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,
+					RequestParams: url.Values{"name": {"top-used-by-cdn1-and-cdn2"}},
+					RequestBody: map[string]interface{}{
+						"name":  "top-used-by-cdn1-and-cdn2",
+						"nodes": []map[string]interface{}{{"cachegroup": "cdn1-only", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when ORG_LOC is CHILD NODE": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name":        "topology-child-midloc",
+						"description": "child mid_loc",
+						"nodes":       []map[string]interface{}{{"cachegroup": "parentCachegroup", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when DUPLICATE PARENTS": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"another-topology"}},
+					RequestBody: map[string]interface{}{
+						"name": "topology-same-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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,
+					RequestParams: url.Values{"name": {"mso-topology"}},
+					RequestBody: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"cachegroup": "topology-edge-cg-01", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+			},
 		}
-	}
-}
 
-func GetTestTopologies(t *testing.T) {
-	if len(testData.Topologies) < 1 {
-		t.Fatalf("test data has no topologies, can't test")
-	}
-	topos, _, err := TOSession.GetTopologiesWithHdr(nil)
-	if err != nil {
-		t.Fatalf("expected GET error to be nil, actual: %v", err)
-	}
-	if len(topos) != len(testData.Topologies) {
-		t.Errorf("expected topologies GET to return %v topologies, actual %v", len(testData.Topologies), len(topos))
-	}
-}
+		for method, testCases := range methodTests {
+			t.Run(method, func(t *testing.T) {
+				for name, testCase := range testCases {
+					topology := tc.Topology{}
 
-func EdgeParentOfEdgeSucceedsWithWarning(t *testing.T) {
-	testCase := topologyTestCase{testCaseDescription: "an edge parenting a mid", Topology: tc.Topology{
-		Name:        "edge-parent-of-edge",
-		Description: "An edge is a parent, which is technically valid, but we will warn the user in case it was a mistake",
-		Nodes: []tc.TopologyNode{
-			{Cachegroup: "cachegroup1", Parents: []int{1}},
-			{Cachegroup: "cachegroup2", Parents: []int{}},
-		}}}
-	response, _, err := TOSession.CreateTopology(testCase.Topology)
-	if err != nil {
-		t.Fatalf("expected POST with %v to succeed, actual: nil", testCase.testCaseDescription)
-	}
-	containsWarning := false
-	for _, alert := range response.Alerts.Alerts {
-		if alert.Level == "warning" {
-			containsWarning = true
+					if testCase.RequestBody != nil {
+						dat, err := json.Marshal(testCase.RequestBody)
+						assert.NoError(t, err, "Error occurred when marshalling request body: %v", err)
+						err = json.Unmarshal(dat, &topology)
+						assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err)
+					}
+
+					switch method {

Review Comment:
   needs a default case



##########
traffic_ops/testing/api/v4/topologies_test.go:
##########
@@ -20,1393 +20,552 @@ package v4
  */
 
 import (
-	"fmt"
+	"encoding/json"
 	"net/http"
 	"net/url"
-	"reflect"
-	"strconv"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/testing/api/assert"
+	"github.com/apache/trafficcontrol/traffic_ops/testing/api/utils"
+	"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
 	client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
-	toclient "github.com/apache/trafficcontrol/traffic_ops/v4-client"
 )
 
-type topologyTestCase struct {
-	testCaseDescription string
-	tc.Topology
-}
-
 func TestTopologies(t *testing.T) {
-	WithObjs(t, []TCObj{Types, CacheGroups, CDNs, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, ServerCapabilities, ServerServerCapabilities, Topologies, Tenants, ServiceCategories, DeliveryServices, DeliveryServicesRequiredCapabilities}, func() {
-		GetTestTopologies(t)
-		currentTime := time.Now().UTC().Add(-5 * time.Second)
-		rfcTime := currentTime.Format(time.RFC1123)
-		var header http.Header
-		header = make(map[string][]string)
-		header.Set(rfc.IfModifiedSince, rfcTime)
-		header.Set(rfc.IfUnmodifiedSince, rfcTime)
-		UpdateTestTopologies(t)
-		UpdateTestTopologiesWithHeaders(t, header)
-		header = make(map[string][]string)
-		etag := rfc.ETag(currentTime)
-		header.Set(rfc.IfMatch, etag)
-		UpdateTestTopologiesWithHeaders(t, header)
-		ValidationTestTopologies(t)
-		UpdateValidateTopologyORGServerCacheGroup(t)
-		EdgeParentOfEdgeSucceedsWithWarning(t)
-		UpdateTopologyName(t)
-		GetTopologyWithNonExistentName(t)
-		CreateTopologyWithInvalidCacheGroup(t)
-		CreateTopologyWithInvalidParentNumber(t)
-		CreateTopologyWithoutDescription(t)
-		CreateTopologyWithoutName(t)
-		CreateTopologyWithoutServers(t)
-		CreateTopologyWithDuplicateParents(t)
-		CreateTopologyWithNodeAsParentOfItself(t)
-		CreateTopologyWithOrgLocAsChildNode(t)
-		CreateTopologyWithExistingName(t)
-		CreateTopologyWithMidLocTypeWithoutChild(t)
-		CRUDTopologyReadOnlyUser(t)
-		UpdateTopologyWithCachegroupAssignedToBecomeParentOfItself(t)
-		UpdateTopologyWithNoServers(t)
-		UpdateTopologyWithInvalidParentNumber(t)
-		UpdateTopologyWithMidLocTypeWithoutChild(t)
-		UpdateTopologyWithOrgLocAsChildNode(t)
-		UpdateTopologyWithSameParentAndSecondaryParent(t)
-		DeleteTopologyWithNonExistentName(t)
-		DeleteTopologyBeingUsedByDeliveryService(t)
-	})
-}
-
-func CreateTestTopologies(t *testing.T) {
-	for _, topology := range testData.Topologies {
-		postResponse, _, err := TOSession.CreateTopology(topology, toclient.RequestOptions{})
-		if err != nil {
-			t.Fatalf("could not create Topology: %v - alerts: %+v", err, postResponse.Alerts)
-		}
-		postResponse.Response.LastUpdated = nil
-		if !reflect.DeepEqual(topology, postResponse.Response) {
-			t.Fatalf("Topology in response should be the same as the one POSTed. expected: %v\nactual: %v", topology, postResponse.Response)
-		}
-	}
-}
-
-func GetTestTopologies(t *testing.T) {
-	if len(testData.Topologies) < 1 {
-		t.Fatalf("test data has no topologies, can't test")
-	}
-	topos, _, err := TOSession.GetTopologies(toclient.RequestOptions{})
-	if err != nil {
-		t.Fatalf("expected error to be nil, actual: %v - alerts: %+v", err, topos.Alerts)
-	}
-	if len(topos.Response) != len(testData.Topologies) {
-		t.Errorf("expected %d Topologies to exist in Traffic Ops, actual: %d", len(testData.Topologies), len(topos.Response))
-	}
-}
-
-func UpdateTestTopologiesWithHeaders(t *testing.T, header http.Header) {
-	originalName := "top-used-by-cdn1-and-cdn2"
-	newName := "blah"
-
-	// Retrieve the Topology by name so we can get the id for Update()
-	opts := toclient.NewRequestOptions()
-	opts.QueryParameters.Set("name", originalName)
-	resp, _, err := TOSession.GetTopologies(opts)
-	if err != nil {
-		t.Errorf("cannot get Topology by name '%s': %v - alerts: %+v", originalName, err, resp.Alerts)
-	}
-	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Topology to exist with name '%s', found: %d", originalName, len(resp.Response))
-	}
-	resp.Response[0].Name = newName
-	_, reqInf, err := TOSession.UpdateTopology(originalName, resp.Response[0], toclient.RequestOptions{Header: header})
-	if err == nil {
-		t.Errorf("Expected error about Precondition Failed, got none")
-	}
-	if reqInf.StatusCode != http.StatusPreconditionFailed {
-		t.Errorf("Expected status code 412, got %v", reqInf.StatusCode)
-	}
-}
-
-func EdgeParentOfEdgeSucceedsWithWarning(t *testing.T) {
-	testCase := topologyTestCase{testCaseDescription: "an edge parenting a mid", Topology: tc.Topology{
-		Name:        "edge-parent-of-edge",
-		Description: "An edge is a parent, which is technically valid, but we will warn the user in case it was a mistake",
-		Nodes: []tc.TopologyNode{
-			{Cachegroup: "cachegroup1", Parents: []int{1}},
-			{Cachegroup: "cachegroup2", Parents: []int{}},
-		}}}
-	response, _, err := TOSession.CreateTopology(testCase.Topology, toclient.RequestOptions{})
-	if err != nil {
-		t.Fatalf("Unexpected error creating Topology for '%s': %v - alerts: %+v", testCase.testCaseDescription, err, response.Alerts)
-	}
-	containsWarning := false
-	for _, alert := range response.Alerts.Alerts {
-		if alert.Level == tc.WarnLevel.String() {
-			containsWarning = true
+	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.CreateV4Session(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.V4TestCase{
+			"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: map[string]interface{}{
+						"name":  "topology-missing-description",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+				"BAD REQUEST when EMPTY NODES": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-no-nodes",
+						"nodes": []map[string]interface{}{},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when NODE PARENT of ITSELF": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "self-parent",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{0}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when TOO MANY PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "too-many-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "edge-parents-mid",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-tiered",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-nontopology",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "outofbounds",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{7}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP DOESNT EXIST": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-nonexistent-cachegroup",
+						"nodes": []map[string]interface{}{{"cachegroup": "doesntexist", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when MISSING NAME": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"description": "missing name",
+						"nodes":       []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when ALREADY EXISTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP has NO SERVERS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"cachegroup": "noServers", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when DUPLICATE PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "topology-duplicate-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "topology-midloc-leaf",
+						"nodes": []map[string]interface{}{{"cachegroup": "parentCachegroup", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"WARNING LEVEL ALERT when MID PARENTING EDGE": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":        "topology-mid-parent",
+						"description": "mid parent to edge",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "topology-ro",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":        "top-with-no-mids-updated",
+						"description": "Updating fields",
+						"nodes":       []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-invalid-parent",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "top-for-ds-req",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "top-used-by-cdn1-and-cdn2",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":        "topology-child-midloc",
+						"description": "child mid_loc",
+						"nodes":       []map[string]interface{}{{"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: map[string]interface{}{
+						"name": "topology-same-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-ro",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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 := tc.Topology{}
+
+					if testCase.RequestBody != nil {
+						dat, err := json.Marshal(testCase.RequestBody)
+						assert.NoError(t, err, "Error occurred when marshalling request body: %v", err)
+						err = json.Unmarshal(dat, &topology)
+						assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err)
+					}
+
+					switch method {

Review Comment:
   needs a default case



##########
traffic_ops/testing/api/v5/topologies_test.go:
##########
@@ -20,1393 +20,552 @@ package v5
  */
 
 import (
-	"fmt"
+	"encoding/json"
 	"net/http"
 	"net/url"
-	"reflect"
-	"strconv"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/testing/api/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"
-	toclient "github.com/apache/trafficcontrol/traffic_ops/v5-client"
 )
 
-type topologyTestCase struct {
-	testCaseDescription string
-	tc.Topology
-}
-
 func TestTopologies(t *testing.T) {
-	WithObjs(t, []TCObj{Types, CacheGroups, CDNs, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, ServerCapabilities, ServerServerCapabilities, Topologies, Tenants, ServiceCategories, DeliveryServices, DeliveryServicesRequiredCapabilities}, func() {
-		GetTestTopologies(t)
-		currentTime := time.Now().UTC().Add(-5 * time.Second)
-		rfcTime := currentTime.Format(time.RFC1123)
-		var header http.Header
-		header = make(map[string][]string)
-		header.Set(rfc.IfModifiedSince, rfcTime)
-		header.Set(rfc.IfUnmodifiedSince, rfcTime)
-		UpdateTestTopologies(t)
-		UpdateTestTopologiesWithHeaders(t, header)
-		header = make(map[string][]string)
-		etag := rfc.ETag(currentTime)
-		header.Set(rfc.IfMatch, etag)
-		UpdateTestTopologiesWithHeaders(t, header)
-		ValidationTestTopologies(t)
-		UpdateValidateTopologyORGServerCacheGroup(t)
-		EdgeParentOfEdgeSucceedsWithWarning(t)
-		UpdateTopologyName(t)
-		GetTopologyWithNonExistentName(t)
-		CreateTopologyWithInvalidCacheGroup(t)
-		CreateTopologyWithInvalidParentNumber(t)
-		CreateTopologyWithoutDescription(t)
-		CreateTopologyWithoutName(t)
-		CreateTopologyWithoutServers(t)
-		CreateTopologyWithDuplicateParents(t)
-		CreateTopologyWithNodeAsParentOfItself(t)
-		CreateTopologyWithOrgLocAsChildNode(t)
-		CreateTopologyWithExistingName(t)
-		CreateTopologyWithMidLocTypeWithoutChild(t)
-		CRUDTopologyReadOnlyUser(t)
-		UpdateTopologyWithCachegroupAssignedToBecomeParentOfItself(t)
-		UpdateTopologyWithNoServers(t)
-		UpdateTopologyWithInvalidParentNumber(t)
-		UpdateTopologyWithMidLocTypeWithoutChild(t)
-		UpdateTopologyWithOrgLocAsChildNode(t)
-		UpdateTopologyWithSameParentAndSecondaryParent(t)
-		DeleteTopologyWithNonExistentName(t)
-		DeleteTopologyBeingUsedByDeliveryService(t)
-	})
-}
-
-func CreateTestTopologies(t *testing.T) {
-	for _, topology := range testData.Topologies {
-		postResponse, _, err := TOSession.CreateTopology(topology, toclient.RequestOptions{})
-		if err != nil {
-			t.Fatalf("could not create Topology: %v - alerts: %+v", err, postResponse.Alerts)
-		}
-		postResponse.Response.LastUpdated = nil
-		if !reflect.DeepEqual(topology, postResponse.Response) {
-			t.Fatalf("Topology in response should be the same as the one POSTed. expected: %v\nactual: %v", topology, postResponse.Response)
-		}
-	}
-}
-
-func GetTestTopologies(t *testing.T) {
-	if len(testData.Topologies) < 1 {
-		t.Fatalf("test data has no topologies, can't test")
-	}
-	topos, _, err := TOSession.GetTopologies(toclient.RequestOptions{})
-	if err != nil {
-		t.Fatalf("expected error to be nil, actual: %v - alerts: %+v", err, topos.Alerts)
-	}
-	if len(topos.Response) != len(testData.Topologies) {
-		t.Errorf("expected %d Topologies to exist in Traffic Ops, actual: %d", len(testData.Topologies), len(topos.Response))
-	}
-}
-
-func UpdateTestTopologiesWithHeaders(t *testing.T, header http.Header) {
-	originalName := "top-used-by-cdn1-and-cdn2"
-	newName := "blah"
-
-	// Retrieve the Topology by name so we can get the id for Update()
-	opts := toclient.NewRequestOptions()
-	opts.QueryParameters.Set("name", originalName)
-	resp, _, err := TOSession.GetTopologies(opts)
-	if err != nil {
-		t.Errorf("cannot get Topology by name '%s': %v - alerts: %+v", originalName, err, resp.Alerts)
-	}
-	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Topology to exist with name '%s', found: %d", originalName, len(resp.Response))
-	}
-	resp.Response[0].Name = newName
-	_, reqInf, err := TOSession.UpdateTopology(originalName, resp.Response[0], toclient.RequestOptions{Header: header})
-	if err == nil {
-		t.Errorf("Expected error about Precondition Failed, got none")
-	}
-	if reqInf.StatusCode != http.StatusPreconditionFailed {
-		t.Errorf("Expected status code 412, got %v", reqInf.StatusCode)
-	}
-}
-
-func EdgeParentOfEdgeSucceedsWithWarning(t *testing.T) {
-	testCase := topologyTestCase{testCaseDescription: "an edge parenting a mid", Topology: tc.Topology{
-		Name:        "edge-parent-of-edge",
-		Description: "An edge is a parent, which is technically valid, but we will warn the user in case it was a mistake",
-		Nodes: []tc.TopologyNode{
-			{Cachegroup: "cachegroup1", Parents: []int{1}},
-			{Cachegroup: "cachegroup2", Parents: []int{}},
-		}}}
-	response, _, err := TOSession.CreateTopology(testCase.Topology, toclient.RequestOptions{})
-	if err != nil {
-		t.Fatalf("Unexpected error creating Topology for '%s': %v - alerts: %+v", testCase.testCaseDescription, err, response.Alerts)
-	}
-	containsWarning := false
-	for _, alert := range response.Alerts.Alerts {
-		if alert.Level == tc.WarnLevel.String() {
-			containsWarning = true
+	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.V5TestCase{
+			"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: map[string]interface{}{
+						"name":  "topology-missing-description",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+				"BAD REQUEST when EMPTY NODES": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-no-nodes",
+						"nodes": []map[string]interface{}{},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when NODE PARENT of ITSELF": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "self-parent",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{0}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when TOO MANY PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "too-many-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "edge-parents-mid",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-tiered",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "cyclical-nodes-nontopology",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "outofbounds",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{7}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP DOESNT EXIST": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-nonexistent-cachegroup",
+						"nodes": []map[string]interface{}{{"cachegroup": "doesntexist", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when MISSING NAME": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"description": "missing name",
+						"nodes":       []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when ALREADY EXISTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"cachegroup": "cachegroup1", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when CACHEGROUP has NO SERVERS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"cachegroup": "noServers", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when DUPLICATE PARENTS": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name": "topology-duplicate-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "topology-midloc-leaf",
+						"nodes": []map[string]interface{}{{"cachegroup": "parentCachegroup", "parents": []int{}}},
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"WARNING LEVEL ALERT when MID PARENTING EDGE": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"name":        "topology-mid-parent",
+						"description": "mid parent to edge",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "topology-ro",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":        "top-with-no-mids-updated",
+						"description": "Updating fields",
+						"nodes":       []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-invalid-parent",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-empty-cg",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "top-for-ds-req",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "top-used-by-cdn1-and-cdn2",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name": "topology-orgloc-child",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":        "topology-child-midloc",
+						"description": "child mid_loc",
+						"nodes":       []map[string]interface{}{{"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: map[string]interface{}{
+						"name": "topology-same-parents",
+						"nodes": []map[string]interface{}{
+							{
+								"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: map[string]interface{}{
+						"name":  "mso-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "topology-ro",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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: map[string]interface{}{
+						"name":  "another-topology",
+						"nodes": []map[string]interface{}{{"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 := tc.Topology{}
+
+					if testCase.RequestBody != nil {
+						dat, err := json.Marshal(testCase.RequestBody)
+						assert.NoError(t, err, "Error occurred when marshalling request body: %v", err)
+						err = json.Unmarshal(dat, &topology)
+						assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err)
+					}
+
+					switch method {

Review Comment:
   needs a default case



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscribe@trafficcontrol.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org