Repository: apache/trafficcontrol
Pull request #6862 (2022-05-26 15:51:58)

Refactor Physical Locations Test
<!--
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/phys_locations_test.go**, **v3/phys_locations_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. -->
- [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.
-->



Patch:
diff --git a/.asf.yaml b/.asf.yaml
index ace8cc9e1..468f47bf9 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-# ATC Collaborators for May 2022
+# ATC Collaborators for June 2022
 # Collaborators are contributors, other than committers, who have had 2 or more Issue-closing Pull Requests merged
 # in the past 31 days. If you want to be an Apache Traffic Control collaborator:
 # 1. Read our contribution guidelines at https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
@@ -25,8 +25,9 @@
 #   - The ATC Development mailing list: https://trafficcontrol.apache.org/mailing_lists
 github:
   collaborators:
-  - ericholguin
-  - jpappa200
+  - ocket8888
+  - rawlinp
+  - srijeet0406
   description: Apache Traffic Control is an Open Source implementation of a Content
     Delivery Network
   features:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ec525f36..67bdef8de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Traffic Monitor config option `distributed_polling` which enables the ability for Traffic Monitor to poll a subset of the CDN and divide into "local peer groups" and "distributed peer groups". Traffic Monitors in the same group are local peers, while Traffic Monitors in other groups are distibuted peers. Each TM group polls the same set of cachegroups and gets availability data for the other cachegroups from other TM groups. This allows each TM to be responsible for polling a subset of the CDN while still having a full view of CDN availability. In order to use this, `stat_polling` must be disabled.
 - Added support for a new Traffic Ops GLOBAL profile parameter -- `tm_query_status_override` -- to override which status of Traffic Monitors to query (default: ONLINE).
 - Traffic Ops: added new `cdn.conf` option -- `user_cache_refresh_interval_sec` -- which enables an in-memory users cache to improve performance. Default: 0 (disabled).
+- Traffic Ops: added new `cdn.conf` option -- `server_update_status_cache_refresh_interval_sec` -- which enables an in-memory server update status cache to improve performance. Default: 0 (disabled).
 - Traffic Router: Add support for `file`-protocol URLs for the `geolocation.polling.url` for the Geolocation database.
 - Replaces all Traffic Portal Tenant select boxes with a novel tree select box [#6427](https://github.com/apache/trafficcontrol/issues/6427).
 - Traffic Monitor: Add support for `access.log` to TM.
@@ -22,6 +23,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Added layered profile feature to 4.0 for `GET` /servers/, `POST` /servers/, `PUT` /servers/{id} and `DELETE` /servers/{id}.
 - Added a Traffic Ops endpoint and Traffic Portal page to view all CDNi configuration update requests and approve or deny.
 - Added layered profile feature to 4.0 for `GET` /deliveryservices/{id}/servers/ and /deliveryservices/{id}/servers/eligible.
+- Change to t3c regex_revalidate so that STALE is no longer explicitly added for default revalidate rule for ATS version backwards compatibility.
 
 ### Fixed
 - [#6291](https://github.com/apache/trafficcontrol/issues/6291) Prevent Traffic Ops from modifying and/or deleting reserved statuses.
diff --git a/cache-config/t3cutil/toreq/client.go b/cache-config/t3cutil/toreq/client.go
index 4125ca012..6b017c188 100644
--- a/cache-config/t3cutil/toreq/client.go
+++ b/cache-config/t3cutil/toreq/client.go
@@ -170,7 +170,7 @@ func checkLatestAndFallBack(client *toclient.Session, url *url.URL, user string,
 		return &TOClient{c: client}, nil
 	}
 
-	log.Warnf("Traffic Ops '%v' does not support the latest client API version %v, falling back to the previous\n", LatestKnownAPIVersion(), torequtil.MaybeIPStr(toAddr))
+	log.Warnf("Traffic Ops '%v' does not support the latest client API version %v, falling back to the previous\n", torequtil.MaybeIPStr(toAddr), LatestKnownAPIVersion())
 
 	oldClient, err := toreqold.New(url, user, pass, insecure, timeout, userAgent)
 	if err != nil {
diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst
index 6ea3cbf47..0a3c28c66 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -511,6 +511,12 @@ This file deals with the configuration parameters of running Traffic Ops itself.
 
 	.. versionadded:: 7.0
 
+:server_update_status_cache_refresh_interval_sec: This optional integer value specifies the interval (in seconds) between refreshing the in-memory server update status cache. Default: 0 (disabled).
+
+	.. warning:: Enabling the server update status cache improves performance by reducing the number of queries made to the Traffic Ops database, but it means that it may take up to this many seconds before any server updates or revalidations are reflected in the :ref:`to-api-servers-hostname-update_status` API.
+
+	.. versionadded:: 7.0
+
 
 Example cdn.conf
 ''''''''''''''''
diff --git a/infrastructure/ansible/roles/traffic_ops/defaults/main.yml b/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
index bec28e691..c36806935 100644
--- a/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
+++ b/infrastructure/ansible/roles/traffic_ops/defaults/main.yml
@@ -45,6 +45,7 @@ to_disable_auto_cert_deletion: false
 to_use_ims: true
 to_use_rbp: true
 to_user_cache_refresh_interval_sec: 0
+to_server_update_status_cache_refresh_interval_sec: 0
 to_heartbeat_timeout: 20
 to_hypnotoad_number_of_workers: 12
 to_cors_access_control_allow_origin: "http://localhost:8080"
diff --git a/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2 b/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
index 4244b7fcc..5dfeaccfc 100644
--- a/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
+++ b/infrastructure/ansible/roles/traffic_ops/templates/cdn.conf.j2
@@ -50,6 +50,7 @@
       "address" : "{{ to_smtp_address }}"
    },
    "user_cache_refresh_interval_sec": {{ to_user_cache_refresh_interval_sec }},
+   "server_update_status_cache_refresh_interval_sec": {{ to_server_update_status_cache_refresh_interval_sec }},
    "disable_auto_cert_deletion": {{ to_disable_auto_cert_deletion | bool | lower }},
    "use_ims": {{ to_use_ims | bool | lower }},
    "role_based_permissions": {{ to_use_rbp | bool | lower }},
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/config.sh b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
index d31393fd9..c44525a83 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/config.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/config.sh
@@ -81,6 +81,8 @@ cdn_conf=/opt/traffic_ops/app/conf/cdn.conf
     },
     "disable_auto_cert_deletion": false,
     "use_ims": true,
+    "server_update_status_cache_refresh_interval_sec": 0,
+    "user_cache_refresh_interval_sec": 0,
     "role_based_permissions": true,
     "traffic_ops_golang" : {
           "traffic_vault_backend": "$TV_BACKEND",
diff --git a/lib/go-atscfg/regexrevalidatedotconfig.go b/lib/go-atscfg/regexrevalidatedotconfig.go
index 86674ad28..6cc11baed 100644
--- a/lib/go-atscfg/regexrevalidatedotconfig.go
+++ b/lib/go-atscfg/regexrevalidatedotconfig.go
@@ -41,6 +41,12 @@ const DefaultMaxRevalDurationDays = 90
 const JobKeywordPurge = "PURGE"
 const RegexRevalidateMinTTL = time.Hour
 
+type RevalType string
+
+const RevalTypeMiss = RevalType("MISS")
+const RevalTypeStale = RevalType("STALE")
+const RevalTypeDefault = RevalTypeStale
+
 const ContentTypeRegexRevalidateDotConfig = ContentTypeTextASCII
 const LineCommentRegexRevalidateDotConfig = LineCommentHash
 
@@ -110,8 +116,8 @@ func MakeRegexRevalidateDotConfig(
 	txt := makeHdrComment(opt.HdrComment)
 	for _, job := range cfgJobs {
 		txt += job.AssetURL + " " + strconv.FormatInt(job.PurgeEnd.Unix(), 10)
-		if job.Type != "" {
-			txt += " " + job.Type
+		if job.Type != "" && job.Type != RevalTypeDefault {
+			txt += " " + string(job.Type)
 		}
 		txt += "\n"
 	}
@@ -127,7 +133,7 @@ func MakeRegexRevalidateDotConfig(
 type revalJob struct {
 	AssetURL string
 	PurgeEnd time.Time
-	Type     string // MISS or STALE (default)
+	Type     RevalType // RevalTypeMiss or RevalTypeStale (default)
 }
 
 type jobsSort []revalJob
@@ -188,21 +194,18 @@ func filterJobs(tcJobs []InvalidationJob, maxReval time.Duration, minTTL time.Du
 	return newJobs
 }
 
-const MISS = "MISS"
-const STALE = "STALE"
-
 // processRefetch determines the type of Invalidation, returns the corresponding jobtype
 // and "cleans" the regex URL for the asset to be invalidated. REFETCH trumps REFRESH,
 // whether in the AssetURL or as InvalidationType
-func processRefetch(invalidationType, assetURL string) (string, string) {
+func processRefetch(invalidationType, assetURL string) (RevalType, string) {
 
 	if (len(invalidationType) > 0 && invalidationType == tc.REFETCH) || strings.HasSuffix(assetURL, RefetchSuffix) {
 		assetURL = strings.TrimSuffix(assetURL, RefetchSuffix)
-		return MISS, assetURL
+		return RevalTypeMiss, assetURL
 	}
 
 	// Default value. Either the InvalidationType == REFRESH
 	// or the suffix is ##REFRESH## or neither
 	assetURL = strings.TrimSuffix(assetURL, RefreshSuffix)
-	return STALE, assetURL
+	return RevalTypeStale, assetURL
 }
diff --git a/lib/go-tc/enum.go b/lib/go-tc/enum.go
index 8d100fa68..453270e75 100644
--- a/lib/go-tc/enum.go
+++ b/lib/go-tc/enum.go
@@ -107,6 +107,11 @@ func CacheTypeFromString(s string) CacheType {
 	return CacheTypeInvalid
 }
 
+// IsValidCacheType returns true if the given string represents a valid cache type.
+func IsValidCacheType(s string) bool {
+	return CacheTypeFromString(s) != CacheTypeInvalid
+}
+
 // InterfaceName is the name of a server interface.
 type InterfaceName string
 
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index eb3ce5778..9eceeefb3 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -45,6 +45,7 @@
     },
     "disable_auto_cert_deletion": false,
     "user_cache_refresh_interval_sec": 0,
+    "server_update_status_cache_refresh_interval_sec": 0,
     "use_ims": false,
     "role_based_permissions": true,
     "cors" : {
diff --git a/traffic_ops/traffic_ops_golang/auth/usercache.go b/traffic_ops/traffic_ops_golang/auth/usercache.go
index 95520bbbb..1cfebef9a 100644
--- a/traffic_ops/traffic_ops_golang/auth/usercache.go
+++ b/traffic_ops/traffic_ops_golang/auth/usercache.go
@@ -127,17 +127,17 @@ func startUsersCacheRefresher(interval time.Duration, db *sql.DB, timeout time.D
 }
 
 func refreshUsersCache(db *sql.DB, timeout time.Duration) {
-	usersCache.Lock()
-	defer usersCache.Unlock()
 	newUsers, err := getUsers(db, timeout)
 	if err != nil {
 		log.Errorf("refreshing users cache: %s", err.Error())
-	} else {
-		usersCache.userMap = newUsers
-		usersCache.usernamesByToken = createTokenToUsernameMap(newUsers)
-		usersCache.initialized = true
-		log.Infof("refreshed users cache (len = %d)", len(usersCache.userMap))
+		return
 	}
+	usersCache.Lock()
+	defer usersCache.Unlock()
+	usersCache.userMap = newUsers
+	usersCache.usernamesByToken = createTokenToUsernameMap(newUsers)
+	usersCache.initialized = true
+	log.Infof("refreshed users cache (len = %d)", len(usersCache.userMap))
 }
 
 func createTokenToUsernameMap(users map[string]user) map[string]string {
diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go
index 521da1d9b..37cffbe7c 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -67,33 +67,34 @@ type BackendConfig struct {
 
 // Config reflects the structure of the cdn.conf file
 type Config struct {
-	URL                         *url.URL `json:"-"`
-	CertPath                    string   `json:"-"`
-	KeyPath                     string   `json:"-"`
-	ConfigHypnotoad             `json:"hypnotoad"`
-	ConfigTrafficOpsGolang      `json:"traffic_ops_golang"`
-	ConfigTO                    *ConfigTO   `json:"to"`
-	SMTP                        *ConfigSMTP `json:"smtp"`
-	ConfigPortal                `json:"portal"`
-	ConfigLetsEncrypt           `json:"lets_encrypt"`
-	ConfigAcmeRenewal           `json:"acme_renewal"`
-	AcmeAccounts                []ConfigAcmeAccount `json:"acme_accounts"`
-	DB                          ConfigDatabase      `json:"db"`
-	Secrets                     []string            `json:"secrets"`
-	TrafficVaultEnabled         bool
-	ConfigLDAP                  *ConfigLDAP
-	UserCacheRefreshIntervalSec int `json:"user_cache_refresh_interval_sec"`
-	LDAPEnabled                 bool
-	LDAPConfPath                string `json:"ldap_conf_location"`
-	ConfigInflux                *ConfigInflux
-	InfluxEnabled               bool
-	InfluxDBConfPath            string `json:"influxdb_conf_path"`
-	Version                     string
-	DisableAutoCertDeletion     bool                    `json:"disable_auto_cert_deletion"`
-	UseIMS                      bool                    `json:"use_ims"`
-	RoleBasedPermissions        bool                    `json:"role_based_permissions"`
-	DefaultCertificateInfo      *DefaultCertificateInfo `json:"default_certificate_info"`
-	Cdni                        *CdniConf               `json:"cdni"`
+	URL                                       *url.URL `json:"-"`
+	CertPath                                  string   `json:"-"`
+	KeyPath                                   string   `json:"-"`
+	ConfigHypnotoad                           `json:"hypnotoad"`
+	ConfigTrafficOpsGolang                    `json:"traffic_ops_golang"`
+	ConfigTO                                  *ConfigTO   `json:"to"`
+	SMTP                                      *ConfigSMTP `json:"smtp"`
+	ConfigPortal                              `json:"portal"`
+	ConfigLetsEncrypt                         `json:"lets_encrypt"`
+	ConfigAcmeRenewal                         `json:"acme_renewal"`
+	AcmeAccounts                              []ConfigAcmeAccount `json:"acme_accounts"`
+	DB                                        ConfigDatabase      `json:"db"`
+	Secrets                                   []string            `json:"secrets"`
+	TrafficVaultEnabled                       bool
+	ConfigLDAP                                *ConfigLDAP
+	UserCacheRefreshIntervalSec               int `json:"user_cache_refresh_interval_sec"`
+	ServerUpdateStatusCacheRefreshIntervalSec int `json:"server_update_status_cache_refresh_interval_sec"`
+	LDAPEnabled                               bool
+	LDAPConfPath                              string `json:"ldap_conf_location"`
+	ConfigInflux                              *ConfigInflux
+	InfluxEnabled                             bool
+	InfluxDBConfPath                          string `json:"influxdb_conf_path"`
+	Version                                   string
+	DisableAutoCertDeletion                   bool                    `json:"disable_auto_cert_deletion"`
+	UseIMS                                    bool                    `json:"use_ims"`
+	RoleBasedPermissions                      bool                    `json:"role_based_permissions"`
+	DefaultCertificateInfo                    *DefaultCertificateInfo `json:"default_certificate_info"`
+	Cdni                                      *CdniConf               `json:"cdni"`
 }
 
 // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
@@ -490,6 +491,9 @@ func ParseConfig(cfg Config) (Config, error) {
 	if cfg.UserCacheRefreshIntervalSec < 0 {
 		cfg.UserCacheRefreshIntervalSec = 0
 	}
+	if cfg.ServerUpdateStatusCacheRefreshIntervalSec < 0 {
+		cfg.ServerUpdateStatusCacheRefreshIntervalSec = 0
+	}
 
 	invalidTOURLStr := ""
 	var err error
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status.go b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
index bdc2e9aa0..589d643f6 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status.go
@@ -20,16 +20,18 @@ package server
  */
 
 import (
+	"context"
 	"database/sql"
 	"fmt"
 	"net/http"
-
-	"github.com/lib/pq"
+	"sync"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
+	"github.com/lib/pq"
 )
 
 func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
@@ -40,7 +42,7 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	serverUpdateStatuses, err, _ := getServerUpdateStatus(inf.Tx.Tx, inf.Config, inf.Params["host_name"])
+	serverUpdateStatuses, err, _ := getServerUpdateStatus(inf.Tx.Tx, inf.Params["host_name"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -56,7 +58,10 @@ func GetServerUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func getServerUpdateStatus(tx *sql.Tx, cfg *config.Config, hostName string) ([]tc.ServerUpdateStatusV40, error, error) {
+func getServerUpdateStatus(tx *sql.Tx, hostName string) ([]tc.ServerUpdateStatusV40, error, error) {
+	if serverUpdateStatusCacheIsInitialized() {
+		return getServerUpdateStatusFromCache(hostName), nil, nil
+	}
 
 	updateStatuses := []tc.ServerUpdateStatusV40{}
 
@@ -175,3 +180,248 @@ ORDER BY s.id
 	}
 	return updateStatuses, nil, nil
 }
+
+type serverUpdateStatuses struct {
+	serverMap map[string][]tc.ServerUpdateStatusV40
+	*sync.RWMutex
+	initialized bool
+	enabled     bool // note: enabled is only written to once at startup, before serving requests, so it doesn't need synchronized access
+}
+
+var serverUpdateStatusCache = serverUpdateStatuses{RWMutex: &sync.RWMutex{}}
+
+func serverUpdateStatusCacheIsInitialized() bool {
+	if serverUpdateStatusCache.enabled {
+		serverUpdateStatusCache.RLock()
+		defer serverUpdateStatusCache.RUnlock()
+		return serverUpdateStatusCache.initialized
+	}
+	return false
+}
+
+func getServerUpdateStatusFromCache(hostname string) []tc.ServerUpdateStatusV40 {
+	serverUpdateStatusCache.RLock()
+	defer serverUpdateStatusCache.RUnlock()
+	return serverUpdateStatusCache.serverMap[hostname]
+}
+
+var once = sync.Once{}
+
+func InitServerUpdateStatusCache(interval time.Duration, db *sql.DB, timeout time.Duration) {
+	once.Do(func() {
+		if interval <= 0 {
+			return
+		}
+		serverUpdateStatusCache.enabled = true
+		refreshServerUpdateStatusCache(db, timeout)
+		startServerUpdateStatusCacheRefresher(interval, db, timeout)
+	})
+}
+
+func startServerUpdateStatusCacheRefresher(interval time.Duration, db *sql.DB, timeout time.Duration) {
+	go func() {
+		for {
+			time.Sleep(interval)
+			refreshServerUpdateStatusCache(db, timeout)
+		}
+	}()
+}
+
+func refreshServerUpdateStatusCache(db *sql.DB, timeout time.Duration) {
+	newServerUpdateStatuses, err := getServerUpdateStatuses(db, timeout)
+	if err != nil {
+		log.Errorf("refreshing server update status cache: %s", err.Error())
+		return
+	}
+	serverUpdateStatusCache.Lock()
+	defer serverUpdateStatusCache.Unlock()
+	serverUpdateStatusCache.serverMap = newServerUpdateStatuses
+	serverUpdateStatusCache.initialized = true
+	log.Infof("refreshed server update status cache (len = %d)", len(serverUpdateStatusCache.serverMap))
+}
+
+type serverInfo struct {
+	id               int
+	hostName         string
+	typeName         string
+	cdnId            int
+	status           string
+	cachegroup       int
+	configUpdateTime *time.Time
+	configApplyTime  *time.Time
+	revalUpdateTime  *time.Time
+	revalApplyTime   *time.Time
+}
+
+const getUseRevalPendingQuery = `
+	SELECT value::BOOLEAN
+	FROM parameter
+	WHERE name = 'use_reval_pending' AND config_file = 'global'
+	UNION ALL SELECT FALSE FETCH FIRST 1 ROW ONLY
+`
+
+const getServerInfoQuery = `
+	SELECT
+		s.id,
+		s.host_name,
+		t.name,
+		s.cdn_id,
+		st.name,
+		s.cachegroup,
+		s.config_update_time,
+		s.config_apply_time,
+		s.revalidate_update_time,
+		s.revalidate_apply_time
+	FROM server s
+	JOIN type t ON t.id = s.type
+	JOIN status st ON st.id = s.status
+`
+
+const getCacheGroupsQuery = `
+	SELECT
+		c.id,
+		c.parent_cachegroup_id,
+		c.secondary_parent_cachegroup_id
+	FROM cachegroup c
+`
+
+const getTopologyCacheGroupParentsQuery = `
+	SELECT
+		cg_child.id,
+		ARRAY_AGG(DISTINCT cg_parent.id)
+	FROM topology_cachegroup_parents tcp
+	JOIN topology_cachegroup tc_child ON tc_child.id = tcp.child
+	JOIN cachegroup cg_child ON cg_child.name = tc_child.cachegroup
+	JOIN topology_cachegroup tc_parent ON tc_parent.id = tcp.parent
+	JOIN cachegroup cg_parent ON cg_parent.name = tc_parent.cachegroup
+	GROUP BY cg_child.id
+`
+
+func getServerUpdateStatuses(db *sql.DB, timeout time.Duration) (map[string][]tc.ServerUpdateStatusV40, error) {
+	dbCtx, dbClose := context.WithTimeout(context.Background(), timeout)
+	defer dbClose()
+	serversByID := make(map[int]serverInfo)
+	updatePendingByCDNCachegroup := make(map[int]map[int]bool)
+	revalPendingByCDNCachegroup := make(map[int]map[int]bool)
+	tx, err := db.BeginTx(dbCtx, nil)
+	if err != nil {
+		return nil, fmt.Errorf("beginning server update status transaction: %w", err)
+	}
+	defer func() {
+		if err := tx.Commit(); err != nil && err != sql.ErrTxDone {
+			log.Errorln("committing server update status transaction: " + err.Error())
+		}
+	}()
+
+	useRevalPending := false
+	if err := tx.QueryRowContext(dbCtx, getUseRevalPendingQuery).Scan(&useRevalPending); err != nil {
+		return nil, fmt.Errorf("querying use_reval_pending param: %w", err)
+	}
+
+	// get all servers and build map of update/revalPending by cachegroup+CDN
+	serverRows, err := tx.QueryContext(dbCtx, getServerInfoQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying servers: %w", err)
+	}
+	defer log.Close(serverRows, "closing server rows")
+	for serverRows.Next() {
+		s := serverInfo{}
+		if err := serverRows.Scan(&s.id, &s.hostName, &s.typeName, &s.cdnId, &s.status, &s.cachegroup, &s.configUpdateTime, &s.configApplyTime, &s.revalUpdateTime, &s.revalApplyTime); err != nil {
+			return nil, fmt.Errorf("scanning servers: %w", err)
+		}
+		serversByID[s.id] = s
+		if _, ok := updatePendingByCDNCachegroup[s.cdnId]; !ok {
+			updatePendingByCDNCachegroup[s.cdnId] = make(map[int]bool)
+		}
+		if _, ok := revalPendingByCDNCachegroup[s.cdnId]; !ok {
+			revalPendingByCDNCachegroup[s.cdnId] = make(map[int]bool)
+		}
+		status := tc.CacheStatusFromString(s.status)
+		if tc.IsValidCacheType(s.typeName) && (status == tc.CacheStatusOnline || status == tc.CacheStatusReported || status == tc.CacheStatusAdminDown) {
+			if s.configUpdateTime.After(*s.configApplyTime) {
+				updatePendingByCDNCachegroup[s.cdnId][s.cachegroup] = true
+			}
+			if s.revalUpdateTime.After(*s.revalApplyTime) {
+				revalPendingByCDNCachegroup[s.cdnId][s.cachegroup] = true
+			}
+		}
+	}
+	if err := serverRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over server rows: %w", err)
+	}
+
+	// get all legacy cachegroup parents
+	cacheGroupParents := make(map[int]map[int]struct{})
+	cacheGroupRows, err := tx.QueryContext(dbCtx, getCacheGroupsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying cachegroups: %w", err)
+	}
+	defer log.Close(cacheGroupRows, "closing cachegroup rows")
+	for cacheGroupRows.Next() {
+		id := 0
+		parentID := new(int)
+		secondaryParentID := new(int)
+		if err := cacheGroupRows.Scan(&id, &parentID, &secondaryParentID); err != nil {
+			return nil, fmt.Errorf("scanning cachegroups: %w", err)
+		}
+		cacheGroupParents[id] = make(map[int]struct{})
+		if parentID != nil {
+			cacheGroupParents[id][*parentID] = struct{}{}
+		}
+		if secondaryParentID != nil {
+			cacheGroupParents[id][*secondaryParentID] = struct{}{}
+		}
+	}
+	if err := cacheGroupRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over cachegroup rows: %w", err)
+	}
+
+	// get all topology-based cachegroup parents
+	topologyCachegroupRows, err := tx.QueryContext(dbCtx, getTopologyCacheGroupParentsQuery)
+	if err != nil {
+		return nil, fmt.Errorf("querying topology cachegroups: %w", err)
+	}
+	defer log.Close(topologyCachegroupRows, "closing topology cachegroup rows")
+	for topologyCachegroupRows.Next() {
+		id := 0
+		parents := []int32{}
+		if err := topologyCachegroupRows.Scan(&id, pq.Array(&parents)); err != nil {
+			return nil, fmt.Errorf("scanning topology cachegroup rows: %w", err)
+		}
+		for _, p := range parents {
+			cacheGroupParents[id][int(p)] = struct{}{}
+		}
+	}
+	if err = topologyCachegroupRows.Err(); err != nil {
+		return nil, fmt.Errorf("iterating over topology cachegroup rows: %w", err)
+	}
+
+	serverUpdateStatuses := make(map[string][]tc.ServerUpdateStatusV40, len(serversByID))
+	for serverID, server := range serversByID {
+		updateStatus := tc.ServerUpdateStatusV40{
+			HostName:             server.hostName,
+			UpdatePending:        server.configUpdateTime.After(*server.configApplyTime),
+			RevalPending:         server.revalUpdateTime.After(*server.revalApplyTime),
+			UseRevalPending:      useRevalPending,
+			HostId:               serverID,
+			Status:               server.status,
+			ParentPending:        getParentPending(cacheGroupParents[server.cachegroup], updatePendingByCDNCachegroup[server.cdnId]),
+			ParentRevalPending:   getParentPending(cacheGroupParents[server.cachegroup], revalPendingByCDNCachegroup[server.cdnId]),
+			ConfigUpdateTime:     server.configUpdateTime,
+			ConfigApplyTime:      server.configApplyTime,
+			RevalidateUpdateTime: server.revalUpdateTime,
+			RevalidateApplyTime:  server.revalApplyTime,
+		}
+		serverUpdateStatuses[server.hostName] = append(serverUpdateStatuses[server.hostName], updateStatus)
+	}
+	return serverUpdateStatuses, nil
+}
+
+func getParentPending(parents map[int]struct{}, pendingByCacheGroup map[int]bool) bool {
+	for parent := range parents {
+		if pendingByCacheGroup[parent] {
+			return true
+		}
+	}
+	return false
+}
diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
index 42a3acf45..60b9b4c2a 100644
--- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
+++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go
@@ -41,6 +41,7 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/plugin"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/server"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
 	_ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends" // init traffic vault backends
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault/backends/disabled"
@@ -146,6 +147,7 @@ func main() {
 	db.SetConnMaxLifetime(time.Duration(cfg.DBConnMaxLifetimeSeconds) * time.Second)
 
 	auth.InitUsersCache(time.Duration(cfg.UserCacheRefreshIntervalSec)*time.Second, db.DB, time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
+	server.InitServerUpdateStatusCache(time.Duration(cfg.ServerUpdateStatusCacheRefreshIntervalSec)*time.Second, db.DB, time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
 
 	trafficVault := setupTrafficVault(*riakConfigFileName, &cfg)
 
@@ -186,7 +188,7 @@ func main() {
 
 	log.Infof("Listening on " + cfg.Port)
 
-	server := &http.Server{
+	httpServer := &http.Server{
 		Addr:              ":" + cfg.Port,
 		TLSConfig:         cfg.TLSConfig,
 		ReadTimeout:       time.Duration(cfg.ReadTimeout) * time.Second,
@@ -195,11 +197,11 @@ func main() {
 		IdleTimeout:       time.Duration(cfg.IdleTimeout) * time.Second,
 		ErrorLog:          log.Error,
 	}
-	if server.TLSConfig == nil {
-		server.TLSConfig = &tls.Config{}
+	if httpServer.TLSConfig == nil {
+		httpServer.TLSConfig = &tls.Config{}
 	}
 	// Deprecated in 5.0
-	server.TLSConfig.InsecureSkipVerify = cfg.Insecure
+	httpServer.TLSConfig.InsecureSkipVerify = cfg.Insecure
 	// end deprecated block
 
 	go func() {
@@ -226,8 +228,8 @@ func main() {
 		} else {
 			file.Close()
 		}
-		server.Handler = mux
-		if err := server.ListenAndServeTLS(cfg.CertPath, cfg.KeyPath); err != nil {
+		httpServer.Handler = mux
+		if err := httpServer.ListenAndServeTLS(cfg.CertPath, cfg.KeyPath); err != nil {
 			log.Errorf("stopping server: %v\n", err)
 			os.Exit(1)
 		}
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
index 49d83c2bb..ce9c7be8f 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/edit/FormEditDeliveryServiceController.js
@@ -242,9 +242,9 @@ var FormEditDeliveryServiceController = function(deliveryService, origin, topolo
 						function() {
 							deliveryServiceService.updateDeliveryService(deliveryService).
 								then(
-									function() {
+									function(response) {
 										$state.reload(); // reloads all the resolves for the view
-										messageModel.setMessages([ { level: 'success', text: 'Delivery Service [ ' + deliveryService.xmlId + ' ] updated' } ], false);
+										messageModel.setMessages(response.data.alerts, false);
 									}
 								).catch(function(fault) {
 									// if the ds update fails, send to dsr view w/ error message
@@ -265,9 +265,9 @@ var FormEditDeliveryServiceController = function(deliveryService, origin, topolo
 		} else {
 			deliveryServiceService.updateDeliveryService(deliveryService).
 				then(
-					function() {
+					function(response) {
 						$state.reload(); // reloads all the resolves for the view
-						messageModel.setMessages([ { level: 'success', text: 'Delivery Service [ ' + deliveryService.xmlId + ' ] updated' } ], false);
+						messageModel.setMessages(response.data.alerts, false);
 					},
 					function(fault) {
 						$anchorScroll(); // scrolls window to top
@@ -298,11 +298,8 @@ var FormEditDeliveryServiceController = function(deliveryService, origin, topolo
 			} else {
 				deliveryServiceService.deleteDeliveryService(deliveryService)
 					.then(
-						function() {
-							messageModel.setMessages([
-								{ level: 'success', text: 'Delivery service [ ' + deliveryService.xmlId + ' ] deleted.' },
-								{ level: 'info', text: 'Perform a CDN snapshot then queue updates on all servers in the cdn for the changes to take effect.' }
-							], true);
+						function(response) {
+							messageModel.setMessages(response.data.alerts, true);
 							locationUtils.navigateToPath('/delivery-services');
 						},
 						function(fault) {
diff --git a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
index 1f9e808f9..8199aa216 100644
--- a/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
+++ b/traffic_portal/app/src/common/modules/form/deliveryService/new/FormNewDeliveryServiceController.js
@@ -112,7 +112,7 @@ var FormNewDeliveryServiceController = function(deliveryService, origin, topolog
 						then(
 							function(result) {
 								createDeliveryServiceCreateRequest(dsRequest, options.comment, true);
-								messageModel.setMessages([ { level: 'success', text: 'Delivery Service [ ' + deliveryService.xmlId + ' ] created' } ], true);
+								messageModel.setMessages(result.data.alerts, true);
 								locationUtils.navigateToPath('/delivery-services/' + result.data.response[0].id + '?type=' + result.data.response[0].type);
 							},
 							function(fault) {
@@ -132,7 +132,7 @@ var FormNewDeliveryServiceController = function(deliveryService, origin, topolog
 			deliveryServiceService.createDeliveryService(deliveryService).
 				then(
 					function(result) {
-						messageModel.setMessages([ { level: 'success', text: 'Delivery Service [ ' + deliveryService.xmlId + ' ] created' } ], true);
+						messageModel.setMessages(result.data.alerts, true);
 						locationUtils.navigateToPath('/delivery-services/' + result.data.response[0].id + '?type=' + result.data.response[0].type);
 					},
 					function(fault) {


Test patch:
diff --git a/cache-config/testing/ort-tests/t3c-jobs_test.go b/cache-config/testing/ort-tests/t3c-jobs_test.go
index 980ee67ce..f0f1c2546 100644
--- a/cache-config/testing/ort-tests/t3c-jobs_test.go
+++ b/cache-config/testing/ort-tests/t3c-jobs_test.go
@@ -74,8 +74,8 @@ func TestT3CJobs(t *testing.T) {
 			}
 			if strings.Contains(line, "refresh-test") {
 				sawRefresh = true
-				if !strings.HasSuffix(line, "STALE") {
-					t.Errorf("expected regex_revalidate.config refresh-test line to contain 'STALE', actual: %s", line)
+				if strings.HasSuffix(line, "STALE") || strings.HasSuffix(line, "MISS") {
+					t.Errorf("expected regex_revalidate.config refresh-test line to contain no type, actual: %s", line)
 				}
 			}
 			if strings.Contains(line, "refetch-test") {
diff --git a/lib/go-atscfg/regexrevalidatedotconfig_test.go b/lib/go-atscfg/regexrevalidatedotconfig_test.go
index 1073a9346..a2aea7023 100644
--- a/lib/go-atscfg/regexrevalidatedotconfig_test.go
+++ b/lib/go-atscfg/regexrevalidatedotconfig_test.go
@@ -122,7 +122,7 @@ func TestMakeRegexRevalidateDotConfig(t *testing.T) {
 	if strings.Contains(txt, "##REFETCH##") || !strings.Contains(txt, "MISS") {
 		t.Errorf("##REFETCH## directive not properly handled '%v'", txt)
 	}
-	if strings.Contains(txt, "##REFRESH##") || !strings.Contains(txt, "STALE") {
+	if strings.Contains(txt, "##REFRESH##") {
 		t.Errorf("##REFRESH## directive not properly handled '%v'", txt)
 	}
 }
diff --git a/traffic_ops/testing/api/v3/phys_locations_test.go b/traffic_ops/testing/api/v3/phys_locations_test.go
index cd38c2828..40c1b3a1e 100644
--- a/traffic_ops/testing/api/v3/phys_locations_test.go
+++ b/traffic_ops/testing/api/v3/phys_locations_test.go
@@ -16,224 +16,342 @@ package v3
 */
 
 import (
+	"encoding/json"
 	"net/http"
+	"net/url"
 	"sort"
 	"testing"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
-	tc "github.com/apache/trafficcontrol/lib/go-tc"
+	"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"
 )
 
 func TestPhysLocations(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Parameters, Divisions, Regions, PhysLocations}, func() {
-		GetTestPhysLocationsIMS(t)
-		GetDefaultSortPhysLocationsTest(t)
-		GetSortPhysLocationsTest(t)
-		currentTime := time.Now().UTC().Add(-5 * time.Second)
-		time := currentTime.Format(time.RFC1123)
-		var header http.Header
-		header = make(map[string][]string)
-		header.Set(rfc.IfModifiedSince, time)
-		header.Set(rfc.IfUnmodifiedSince, time)
-		SortTestPhysLocations(t)
-		UpdateTestPhysLocations(t)
-		UpdateTestPhysLocationsWithHeaders(t, header)
-		GetTestPhysLocations(t)
-		GetTestPhysLocationsIMSAfterChange(t, header)
-		header = make(map[string][]string)
-		etag := rfc.ETag(currentTime)
-		header.Set(rfc.IfMatch, etag)
-		UpdateTestPhysLocationsWithHeaders(t, header)
-	})
-}
 
-func UpdateTestPhysLocationsWithHeaders(t *testing.T, header http.Header) {
-	if len(testData.PhysLocations) > 0 {
-		firstPhysLocation := testData.PhysLocations[0]
-		// Retrieve the PhysLocation by name so we can get the id for the Update
-		resp, _, err := TOSession.GetPhysLocationByNameWithHdr(firstPhysLocation.Name, header)
-		if err != nil {
-			t.Errorf("cannot GET PhysLocation by name: '%s', %v", firstPhysLocation.Name, err)
-		}
-		if len(resp) > 0 {
-			remotePhysLocation := resp[0]
-			expectedPhysLocationCity := "city1"
-			remotePhysLocation.City = expectedPhysLocationCity
-			_, reqInf, err := TOSession.UpdatePhysLocationByIDWithHdr(remotePhysLocation.ID, remotePhysLocation, header)
-			if err == nil {
-				t.Errorf("Expected error about precondition failed, but got none")
-			}
-			if reqInf.StatusCode != http.StatusPreconditionFailed {
-				t.Errorf("Expected status code 412, got %v", reqInf.StatusCode)
-			}
-		}
-	}
-}
+		currentTime := time.Now().UTC().Add(-15 * time.Second)
+		currentTimeRFC := currentTime.Format(time.RFC1123)
+		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-func GetTestPhysLocationsIMS(t *testing.T) {
-	var header http.Header
-	header = make(map[string][]string)
-	futureTime := time.Now().AddDate(0, 0, 1)
-	time := futureTime.Format(time.RFC1123)
-	header.Set(rfc.IfModifiedSince, time)
-	for _, cdn := range testData.PhysLocations {
-		_, reqInf, err := TOSession.GetPhysLocationByNameWithHdr(cdn.Name, header)
-		if err != nil {
-			t.Fatalf("Expected no error, but got %v", err.Error())
-		}
-		if reqInf.StatusCode != http.StatusNotModified {
-			t.Fatalf("Expected 304 status code, got %v", reqInf.StatusCode)
+		methodTests := utils.V3TestCase{
+			"GET": {
+				"NOT MODIFIED when NO CHANGES made": {
+					ClientSession:  TOSession,
+					RequestHeaders: http.Header{rfc.IfModifiedSince: {tomorrow}},
+					Expectations:   utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)),
+				},
+				"OK when VALID request": {
+					ClientSession: TOSession,
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationSort()),
+				},
+				"OK when VALID NAME parameter": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"name": {"Denver"}},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1),
+						validatePhysicalLocationFields(map[string]interface{}{"Name": "Denver"})),
+				},
+				"SORTED by ID when ORDERBY=ID parameter": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"orderby": {"id"}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationIDSort()),
+				},
+				"FIRST RESULT when LIMIT=1": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"orderby": {"id"}, "limit": {"1"}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("limit")),
+				},
+				"SECOND RESULT when LIMIT=1 OFFSET=1": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"orderby": {"id"}, "limit": {"1"}, "offset": {"1"}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("offset")),
+				},
+				"SECOND RESULT when LIMIT=1 PAGE=2": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"orderby": {"id"}, "limit": {"1"}, "page": {"2"}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("page")),
+				},
+				"BAD REQUEST when INVALID LIMIT parameter": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"limit": {"-2"}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when INVALID OFFSET parameter": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"limit": {"1"}, "offset": {"0"}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when INVALID PAGE parameter": {
+					ClientSession: TOSession,
+					RequestParams: url.Values{"limit": {"1"}, "page": {"0"}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+			},
+			"POST": {
+				"OK when VALID request": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "100 blah lane",
+						"city":      "foo",
+						"comments":  "comment",
+						"email":     "bar@foobar.com",
+						"name":      "testPhysicalLocation",
+						"phone":     "111-222-3333",
+						"region":    "region1",
+						"regionId":  GetRegionID(t, "region1")(),
+						"shortName": "testLocation1",
+						"state":     "CO",
+						"zip":       "80602",
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
+						validatePhysicalLocationUpdateCreateFields("testPhysicalLocation", map[string]interface{}{"Name": "testPhysicalLocation"})),
+				},
+				"BAD REQUEST when REGION ID does NOT MATCH REGION NAME": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"name":      "HotAtlanta",
+						"phone":     "404-222-2222",
+						"region":    "region1",
+						"regionId":  GetRegionID(t, "cdn-region2")(),
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+			},
+			"PUT": {
+				"OK when VALID request": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "NewCity",
+						"name":      "HotAtlanta",
+						"phone":     "404-222-2222",
+						"regionId":  GetRegionID(t, "region1")(),
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
+						validatePhysicalLocationUpdateCreateFields("HotAtlanta", map[string]interface{}{"City": "NewCity"})),
+				},
+				"PRECONDITION FAILED when updating with IMS & IUS Headers": {
+					EndpointId:     GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession:  TOSession,
+					RequestHeaders: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"regionId":  GetRegionID(t, "region1")(),
+						"name":      "HotAtlanta",
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
+				},
+				"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"regionId":  GetRegionID(t, "region1")(),
+						"name":      "HotAtlanta",
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					RequestHeaders: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}},
+					Expectations:   utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
+				},
+			},
+			"DELETE": {
+				"OK when VALID request": {
+					EndpointId:    GetPhysicalLocationID(t, "testDelete"),
+					ClientSession: TOSession,
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+			},
+			"GET AFTER CHANGES": {
+				"OK when CHANGES made": {
+					ClientSession:  TOSession,
+					RequestHeaders: http.Header{rfc.IfModifiedSince: {currentTimeRFC}},
+					Expectations:   utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+			},
 		}
-	}
 
-}
+		for method, testCases := range methodTests {
+			t.Run(method, func(t *testing.T) {
+				for name, testCase := range testCases {
+					pl := tc.PhysLocation{}
+					params := make(map[string]string)
+					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, &pl)
+						assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err)
+					}
 
-func GetTestPhysLocationsIMSAfterChange(t *testing.T, header http.Header) {
-	for _, cdn := range testData.PhysLocations {
-		_, reqInf, err := TOSession.GetPhysLocationByNameWithHdr(cdn.Name, header)
-		if err != nil {
-			t.Fatalf("Expected no error, but got %v", err.Error())
-		}
-		if reqInf.StatusCode != http.StatusOK {
-			t.Fatalf("Expected 200 status code, got %v", reqInf.StatusCode)
-		}
-	}
-	currentTime := time.Now().UTC()
-	currentTime = currentTime.Add(1 * time.Second)
-	timeStr := currentTime.Format(time.RFC1123)
-	header.Set(rfc.IfModifiedSince, timeStr)
-	for _, cdn := range testData.PhysLocations {
-		_, reqInf, err := TOSession.GetPhysLocationByNameWithHdr(cdn.Name, header)
-		if err != nil {
-			t.Fatalf("Expected no error, but got %v", err.Error())
-		}
-		if reqInf.StatusCode != http.StatusNotModified {
-			t.Fatalf("Expected 304 status code, got %v", reqInf.StatusCode)
+					if testCase.RequestParams != nil {
+						for k, v := range testCase.RequestParams {
+							params[k] = v[0]
+						}
+					}
+
+					switch method {
+					case "GET", "GET AFTER CHANGES":
+						t.Run(name, func(t *testing.T) {
+							resp, reqInf, err := testCase.ClientSession.GetPhysLocationsWithHdr(params, testCase.RequestHeaders)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, resp, tc.Alerts{}, err)
+							}
+						})
+					case "POST":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.CreatePhysLocation(pl)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					case "PUT":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.UpdatePhysLocationByIDWithHdr(testCase.EndpointId(), pl, testCase.RequestHeaders)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					case "DELETE":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.DeletePhysLocationByID(testCase.EndpointId())
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					}
+				}
+			})
 		}
-	}
+	})
 }
 
-func CreateTestPhysLocations(t *testing.T) {
-	for _, pl := range testData.PhysLocations {
-		resp, _, err := TOSession.CreatePhysLocation(pl)
-		t.Log("Response: ", resp)
-		if err != nil {
-			t.Errorf("could not CREATE physlocations: %v", err)
+func validatePhysicalLocationFields(expectedResp map[string]interface{}) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		plResp := resp.([]tc.PhysLocation)
+		for field, expected := range expectedResp {
+			for _, pl := range plResp {
+				switch field {
+				case "Name":
+					assert.Equal(t, expected, pl.Name, "Expected Name to be %v, but got %s", expected, pl.Name)
+				case "City":
+					assert.Equal(t, expected, pl.City, "Expected City to be %v, but got %s", expected, pl.City)
+				default:
+					t.Errorf("Expected field: %v, does not exist in response", field)
+				}
+			}
 		}
 	}
-
 }
 
-func SortTestPhysLocations(t *testing.T) {
-	var header http.Header
-	var sortedList []string
-	resp, _, err := TOSession.GetPhysLocationsWithHdr(nil, header)
-	if err != nil {
-		t.Fatalf("Expected no error, but got %v", err.Error())
-	}
-	for i, _ := range resp {
-		sortedList = append(sortedList, resp[i].Name)
-	}
-
-	res := sort.SliceIsSorted(sortedList, func(p, q int) bool {
-		return sortedList[p] < sortedList[q]
-	})
-	if res != true {
-		t.Errorf("list is not sorted by their names: %v", sortedList)
+func validatePhysicalLocationUpdateCreateFields(name string, expectedResp map[string]interface{}) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		pl, _, err := TOSession.GetPhysLocationByNameWithHdr(name, nil)
+		assert.RequireNoError(t, err, "Error getting Physical Location: %v", err)
+		assert.RequireEqual(t, 1, len(pl), "Expected one Physical Location returned Got: %d", len(pl))
+		validatePhysicalLocationFields(expectedResp)(t, toclientlib.ReqInf{}, pl, tc.Alerts{}, nil)
 	}
 }
 
-func UpdateTestPhysLocations(t *testing.T) {
+func validatePhysicalLocationPagination(paginationParam string) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		paginationResp := resp.([]tc.PhysLocation)
 
-	firstPhysLocation := testData.PhysLocations[0]
-	// Retrieve the PhysLocation by name so we can get the id for the Update
-	resp, _, err := TOSession.GetPhysLocationByName(firstPhysLocation.Name)
-	if err != nil {
-		t.Errorf("cannot GET PhysLocation by name: '%s', %v", firstPhysLocation.Name, err)
-	}
-	remotePhysLocation := resp[0]
-	expectedPhysLocationCity := "city1"
-	remotePhysLocation.City = expectedPhysLocationCity
-	var alert tc.Alerts
-	alert, _, err = TOSession.UpdatePhysLocationByID(remotePhysLocation.ID, remotePhysLocation)
-	if err != nil {
-		t.Errorf("cannot UPDATE PhysLocation by id: %v - %v", err, alert)
-	}
+		params := map[string]string{"orderby": "id"}
+		respBase, _, err := TOSession.GetPhysLocationsWithHdr(params, nil)
+		assert.RequireNoError(t, err, "Cannot get Physical Locations: %v", err)
 
-	// Retrieve the PhysLocation to check PhysLocation name got updated
-	resp, _, err = TOSession.GetPhysLocationByID(remotePhysLocation.ID)
-	if err != nil {
-		t.Errorf("cannot GET PhysLocation by name: '$%s', %v", firstPhysLocation.Name, err)
-	}
-	respPhysLocation := resp[0]
-	if respPhysLocation.City != expectedPhysLocationCity {
-		t.Errorf("results do not match actual: %s, expected: %s", respPhysLocation.City, expectedPhysLocationCity)
+		pl := respBase
+		assert.RequireGreaterOrEqual(t, len(pl), 3, "Need at least 3 Physical Locations in Traffic Ops to test pagination support, found: %d", len(pl))
+		switch paginationParam {
+		case "limit:":
+			assert.Exactly(t, pl[:1], paginationResp, "expected GET Physical Locations with limit = 1 to return first result")
+		case "offset":
+			assert.Exactly(t, pl[1:2], paginationResp, "expected GET Physical Locations with limit = 1, offset = 1 to return second result")
+		case "page":
+			assert.Exactly(t, pl[1:2], paginationResp, "expected GET Physical Locations with limit = 1, page = 2 to return second result")
+		}
 	}
-
 }
 
-func GetTestPhysLocations(t *testing.T) {
-
-	for _, cdn := range testData.PhysLocations {
-		resp, _, err := TOSession.GetPhysLocationByName(cdn.Name)
-		if err != nil {
-			t.Errorf("cannot GET PhysLocation by name: %v - %v", err, resp)
+func validatePhysicalLocationSort() utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		var physLocNames []string
+		physLocResp := resp.([]tc.PhysLocation)
+		for _, pl := range physLocResp {
+			physLocNames = append(physLocNames, pl.Name)
 		}
+		assert.Equal(t, true, sort.StringsAreSorted(physLocNames), "List is not sorted by their names: %v", physLocNames)
 	}
-
 }
 
-func GetSortPhysLocationsTest(t *testing.T) {
-	resp, _, err := TOSession.GetPhysLocations(map[string]string{"orderby": "id"})
-	if err != nil {
-		t.Error(err.Error())
+func validatePhysicalLocationIDSort() utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		var physLocIDs []int
+		physLocResp := resp.([]tc.PhysLocation)
+		for _, pl := range physLocResp {
+			physLocIDs = append(physLocIDs, pl.ID)
+		}
+		assert.Equal(t, true, sort.IntsAreSorted(physLocIDs), "List is not sorted by their ids: %v", physLocIDs)
 	}
-	sorted := sort.SliceIsSorted(resp, func(i, j int) bool {
-		return resp[i].ID < resp[j].ID
-	})
-	if !sorted {
-		t.Error("expected response to be sorted by id")
+}
+
+func GetRegionID(t *testing.T, name string) func() int {
+	return func() int {
+		regions, _, err := TOSession.GetRegionByNameWithHdr(name, nil)
+		assert.RequireNoError(t, err, "Get Regions Request failed with error:", err)
+		assert.RequireEqual(t, 1, len(regions), "Expected response object length 1, but got %d", len(regions))
+		return regions[0].ID
 	}
 }
 
-func GetDefaultSortPhysLocationsTest(t *testing.T) {
-	resp, _, err := TOSession.GetPhysLocations(nil)
-	if err != nil {
-		t.Error(err.Error())
+func GetPhysicalLocationID(t *testing.T, name string) func() int {
+	return func() int {
+		physicalLocations, _, err := TOSession.GetPhysLocationByNameWithHdr(name, nil)
+		assert.RequireNoError(t, err, "Get PhysLocation Request failed with error:", err)
+		assert.RequireEqual(t, 1, len(physicalLocations), "Expected response object length 1, but got %d", len(physicalLocations))
+		return physicalLocations[0].ID
 	}
-	sorted := sort.SliceIsSorted(resp, func(i, j int) bool {
-		return resp[i].Name < resp[j].Name
-	})
-	if !sorted {
-		t.Error("expected response to be sorted by name")
+}
+
+func CreateTestPhysLocations(t *testing.T) {
+	for _, pl := range testData.PhysLocations {
+		alerts, _, err := TOSession.CreatePhysLocation(pl)
+		assert.RequireNoError(t, err, "Could not create Physical Location '%s': %v - alerts: %+v", pl.Name, err, alerts)
 	}
 }
 
 func DeleteTestPhysLocations(t *testing.T) {
+	physicalLocations, _, err := TOSession.GetPhysLocationsWithHdr(nil, nil)
+	assert.NoError(t, err, "Cannot get Physical Locations: %v", err)
 
-	for _, cdn := range testData.PhysLocations {
-		// Retrieve the PhysLocation by name so we can get the id for the Update
-		resp, _, err := TOSession.GetPhysLocationByName(cdn.Name)
-		if err != nil {
-			t.Errorf("cannot GET PhysLocation by name: %v - %v", cdn.Name, err)
-		}
-		if len(resp) > 0 {
-			respPhysLocation := resp[0]
-
-			_, _, err := TOSession.DeletePhysLocationByID(respPhysLocation.ID)
-			if err != nil {
-				t.Errorf("cannot DELETE PhysLocation by name: '%s' %v", respPhysLocation.Name, err)
-			}
-
-			// Retrieve the PhysLocation to see if it got deleted
-			cdns, _, err := TOSession.GetPhysLocationByName(cdn.Name)
-			if err != nil {
-				t.Errorf("error deleting PhysLocation name: %s", err.Error())
-			}
-			if len(cdns) > 0 {
-				t.Errorf("expected PhysLocation name: %s to be deleted", cdn.Name)
-			}
-		}
+	for _, pl := range physicalLocations {
+		alerts, _, err := TOSession.DeletePhysLocationByID(pl.ID)
+		assert.NoError(t, err, "Unexpected error deleting Physical Location '%s' (#%d): %v - alerts: %+v", pl.Name, pl.ID, err, alerts.Alerts)
+		// Retrieve the PhysLocation to see if it got deleted
+		getPL, _, err := TOSession.GetPhysLocationByIDWithHdr(pl.ID, nil)
+		assert.NoError(t, err, "Error getting Physical Location '%s' after deletion: %v", pl.Name, err)
+		assert.Equal(t, 0, len(getPL), "Expected Physical Location '%s' to be deleted, but it was found in Traffic Ops", pl.Name)
 	}
 }
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json b/traffic_ops/testing/api/v3/tc-fixtures.json
index 39671def4..47a4fe350 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -1844,6 +1844,19 @@
             "shortName": "atlanta",
             "state": "GA",
             "zip": "30301"
+        },
+        {
+            "address": "1234 delete way",
+            "city": "Denver",
+            "comments": null,
+            "email": null,
+            "name": "testDelete",
+            "phone": "303-222-2222",
+            "poc": null,
+            "region": "region1",
+            "shortName": "testdelete",
+            "state": "C0",
+            "zip": "80202"
         }
     ],
     "profiles": [
diff --git a/traffic_ops/testing/api/v4/phys_locations_test.go b/traffic_ops/testing/api/v4/phys_locations_test.go
index 727aca803..40c14cd06 100644
--- a/traffic_ops/testing/api/v4/phys_locations_test.go
+++ b/traffic_ops/testing/api/v4/phys_locations_test.go
@@ -16,396 +16,346 @@ package v4
 */
 
 import (
+	"encoding/json"
 	"net/http"
 	"net/url"
-	"reflect"
 	"sort"
 	"strconv"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"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"
 	client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
 )
 
 func TestPhysLocations(t *testing.T) {
 	WithObjs(t, []TCObj{CDNs, Parameters, Divisions, Regions, PhysLocations}, func() {
-		GetTestPhysLocationsIMS(t)
-		GetDefaultSortPhysLocationsTest(t)
-		GetSortPhysLocationsTest(t)
-		currentTime := time.Now().UTC().Add(-5 * time.Second)
-		time := currentTime.Format(time.RFC1123)
-		var header http.Header
-		header = make(map[string][]string)
-		header.Set(rfc.IfModifiedSince, time)
-		header.Set(rfc.IfUnmodifiedSince, time)
-		SortTestPhysLocations(t)
-		UpdateTestPhysLocations(t)
-		UpdateTestPhysLocationsWithHeaders(t, header)
-		GetTestPhysLocations(t)
-		GetTestPhysLocationsIMSAfterChange(t, header)
-		header = make(map[string][]string)
-		etag := rfc.ETag(currentTime)
-		header.Set(rfc.IfMatch, etag)
-		UpdateTestPhysLocationsWithHeaders(t, header)
-		GetTestPaginationSupportPhysLocation(t)
-		CreatePhysLocationWithMismatchedRegionNameAndID(t)
-	})
-}
-
-func UpdateTestPhysLocationsWithHeaders(t *testing.T, header http.Header) {
-	if len(testData.PhysLocations) < 1 {
-		t.Fatal("Need at least one Physical Location to test updating Physical Locations, with an HTTP header")
-	}
-	firstPhysLocation := testData.PhysLocations[0]
-
-	// Retrieve the PhysLocation by name so we can get the id for the Update
-	opts := client.NewRequestOptions()
-	opts.Header = header
-	opts.QueryParameters.Set("name", firstPhysLocation.Name)
-	resp, _, err := TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Errorf("cannot get Physical Location by name '%s': %v - alerts: %+v", firstPhysLocation.Name, err, resp.Alerts)
-	}
-	if len(resp.Response) < 1 {
-		t.Fatalf("Expected exactly one Physical Location to exist with name '%s', found: %d", firstPhysLocation.Name, len(resp.Response))
-	}
-
-	remotePhysLocation := resp.Response[0]
-	expectedPhysLocationCity := "city1"
-	remotePhysLocation.City = expectedPhysLocationCity
-	opts.QueryParameters.Del("name")
-	_, reqInf, err := TOSession.UpdatePhysLocation(remotePhysLocation.ID, remotePhysLocation, opts)
-	if err == nil {
-		t.Errorf("Expected error about precondition failed, but got none")
-	}
-	if reqInf.StatusCode != http.StatusPreconditionFailed {
-		t.Errorf("Expected status code 412, got %v", reqInf.StatusCode)
-	}
-}
-
-func GetTestPhysLocationsIMS(t *testing.T) {
-	futureTime := time.Now().AddDate(0, 0, 1)
-	time := futureTime.Format(time.RFC1123)
 
-	opts := client.NewRequestOptions()
-	opts.Header.Set(rfc.IfModifiedSince, time)
-	for _, physLoc := range testData.PhysLocations {
-		opts.QueryParameters.Set("name", physLoc.Name)
-		resp, reqInf, err := TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Fatalf("Expected no error, but got: %v - alerts: %+v", err, resp.Alerts)
+		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 VALID request": {
+					ClientSession: TOSession,
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationSort()),
+				},
+				"OK when VALID NAME parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"name": {"Denver"}}},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1),
+						validatePhysicalLocationFields(map[string]interface{}{"Name": "Denver"})),
+				},
+				"SORTED by ID when ORDERBY=ID parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationIDSort()),
+				},
+				"FIRST RESULT when LIMIT=1": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("limit")),
+				},
+				"SECOND RESULT when LIMIT=1 OFFSET=1": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}, "offset": {"1"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("offset")),
+				},
+				"SECOND RESULT when LIMIT=1 PAGE=2": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}, "page": {"2"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), validatePhysicalLocationPagination("page")),
+				},
+				"BAD REQUEST when INVALID LIMIT parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"limit": {"-2"}}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when INVALID OFFSET parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"limit": {"1"}, "offset": {"0"}}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+				"BAD REQUEST when INVALID PAGE parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"limit": {"1"}, "page": {"0"}}},
+					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+			},
+			"POST": {
+				"OK when VALID request": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "100 blah lane",
+						"city":      "foo",
+						"comments":  "comment",
+						"email":     "bar@foobar.com",
+						"name":      "testPhysicalLocation",
+						"phone":     "111-222-3333",
+						"region":    "region1",
+						"regionId":  GetRegionID(t, "region1")(),
+						"shortName": "testLocation1",
+						"state":     "CO",
+						"zip":       "80602",
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
+						validatePhysicalLocationUpdateCreateFields("testPhysicalLocation", map[string]interface{}{"Name": "testPhysicalLocation"})),
+				},
+				"BAD REQUEST when REGION ID does NOT MATCH REGION NAME": {
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"name":      "HotAtlanta",
+						"phone":     "404-222-2222",
+						"region":    "region1",
+						"regionId":  GetRegionID(t, "cdn-region2")(),
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
+				},
+			},
+			"PUT": {
+				"OK when VALID request": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "NewCity",
+						"name":      "HotAtlanta",
+						"phone":     "404-222-2222",
+						"regionId":  GetRegionID(t, "region1")(),
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
+						validatePhysicalLocationUpdateCreateFields("HotAtlanta", map[string]interface{}{"City": "NewCity"})),
+				},
+				"PRECONDITION FAILED when updating with IMS & IUS Headers": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}}},
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"regionId":  GetRegionID(t, "region1")(),
+						"name":      "HotAtlanta",
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
+				},
+				"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
+					EndpointId:    GetPhysicalLocationID(t, "HotAtlanta"),
+					ClientSession: TOSession,
+					RequestBody: map[string]interface{}{
+						"address":   "1234 southern way",
+						"city":      "Atlanta",
+						"regionId":  GetRegionID(t, "region1")(),
+						"name":      "HotAtlanta",
+						"shortName": "atlanta",
+						"state":     "GA",
+						"zip":       "30301",
+					},
+					RequestOpts:  client.RequestOptions{Header: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}}},
+					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
+				},
+			},
+			"DELETE": {
+				"OK when VALID request": {
+					EndpointId:    GetPhysicalLocationID(t, "testDelete"),
+					ClientSession: TOSession,
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+			},
+			"GET AFTER CHANGES": {
+				"OK when CHANGES made": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {currentTimeRFC}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)),
+				},
+			},
 		}
-		if reqInf.StatusCode != http.StatusNotModified {
-			t.Fatalf("Expected 304 status code, got %v", reqInf.StatusCode)
-		}
-	}
-
-}
-
-func GetTestPhysLocationsIMSAfterChange(t *testing.T, header http.Header) {
-	opts := client.NewRequestOptions()
-	opts.Header = header
-	for _, physLoc := range testData.PhysLocations {
-		opts.QueryParameters.Set("name", physLoc.Name)
-		resp, reqInf, err := TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Fatalf("Expected no error, but got: %v - alerts: %+v", err, resp.Alerts)
-		}
-		if reqInf.StatusCode != http.StatusOK {
-			t.Fatalf("Expected 200 status code, got %v", reqInf.StatusCode)
-		}
-	}
-
-	currentTime := time.Now().UTC()
-	currentTime = currentTime.Add(1 * time.Second)
-	timeStr := currentTime.Format(time.RFC1123)
-	opts.Header.Set(rfc.IfModifiedSince, timeStr)
 
-	for _, physLoc := range testData.PhysLocations {
-		opts.QueryParameters.Set("name", physLoc.Name)
-		resp, reqInf, err := TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Fatalf("Expected no error, but got: %v - alerts: %+v", err, resp.Alerts)
-		}
-		if reqInf.StatusCode != http.StatusNotModified {
-			t.Fatalf("Expected 304 status code, got %v", reqInf.StatusCode)
+		for method, testCases := range methodTests {
+			t.Run(method, func(t *testing.T) {
+				for name, testCase := range testCases {
+					pl := tc.PhysLocation{}
+
+					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, &pl)
+						assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err)
+					}
+
+					switch method {
+					case "GET", "GET AFTER CHANGES":
+						t.Run(name, func(t *testing.T) {
+							resp, reqInf, err := testCase.ClientSession.GetPhysLocations(testCase.RequestOpts)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, resp.Response, resp.Alerts, err)
+							}
+						})
+					case "POST":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.CreatePhysLocation(pl, testCase.RequestOpts)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					case "PUT":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.UpdatePhysLocation(testCase.EndpointId(), pl, testCase.RequestOpts)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					case "DELETE":
+						t.Run(name, func(t *testing.T) {
+							alerts, reqInf, err := testCase.ClientSession.DeletePhysLocation(testCase.EndpointId(), testCase.RequestOpts)
+							for _, check := range testCase.Expectations {
+								check(t, reqInf, nil, alerts, err)
+							}
+						})
+					}
+				}
+			})
 		}
-	}
+	})
 }
 
-func CreateTestPhysLocations(t *testing.T) {
-	for _, pl := range testData.PhysLocations {
-		resp, _, err := TOSession.CreatePhysLocation(pl, client.RequestOptions{})
-		if err != nil {
-			t.Errorf("could not create Physical Location '%s': %v - alerts: %+v", pl.Name, err, resp.Alerts)
+func validatePhysicalLocationFields(expectedResp map[string]interface{}) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		plResp := resp.([]tc.PhysLocation)
+		for field, expected := range expectedResp {
+			for _, pl := range plResp {
+				switch field {
+				case "Name":
+					assert.Equal(t, expected, pl.Name, "Expected Name to be %v, but got %s", expected, pl.Name)
+				case "City":
+					assert.Equal(t, expected, pl.City, "Expected City to be %v, but got %s", expected, pl.City)
+				default:
+					t.Errorf("Expected field: %v, does not exist in response", field)
+				}
+			}
 		}
 	}
-
 }
 
-func SortTestPhysLocations(t *testing.T) {
-	var sortedList []string
-	resp, _, err := TOSession.GetPhysLocations(client.RequestOptions{})
-	if err != nil {
-		t.Fatalf("Expected no error, but got: %v - alerts: %+v", err, resp.Alerts)
-	}
-	for _, pl := range resp.Response {
-		sortedList = append(sortedList, pl.Name)
-	}
-
-	res := sort.SliceIsSorted(sortedList, func(p, q int) bool {
-		return sortedList[p] < sortedList[q]
-	})
-	if res != true {
-		t.Errorf("list is not sorted by their names: %v", sortedList)
+func validatePhysicalLocationUpdateCreateFields(name string, expectedResp map[string]interface{}) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		opts := client.NewRequestOptions()
+		opts.QueryParameters.Set("name", name)
+		pl, _, err := TOSession.GetPhysLocations(opts)
+		assert.RequireNoError(t, err, "Error getting Physical Location: %v - alerts: %+v", err, pl.Alerts)
+		assert.RequireEqual(t, 1, len(pl.Response), "Expected one Physical Location returned Got: %d", len(pl.Response))
+		validatePhysicalLocationFields(expectedResp)(t, toclientlib.ReqInf{}, pl.Response, tc.Alerts{}, nil)
 	}
 }
 
-func UpdateTestPhysLocations(t *testing.T) {
-	if len(testData.PhysLocations) < 1 {
-		t.Fatal("Need at least one Physical Location to test updating Physical Locations")
-	}
-	firstPhysLocation := testData.PhysLocations[0]
-
-	// Retrieve the PhysLocation by name so we can get the id for the Update
-	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", firstPhysLocation.Name)
-	resp, _, err := TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Errorf("cannot get Physical Location by name '%s': %v - alerts: %+v", firstPhysLocation.Name, err, resp.Alerts)
-	}
-	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Physical Location to exist with name '%s', found: %d", firstPhysLocation.Name, len(resp.Response))
-	}
-
-	remotePhysLocation := resp.Response[0]
-	expectedPhysLocationCity := "city1"
-	remotePhysLocation.City = expectedPhysLocationCity
-
-	alerts, _, err := TOSession.UpdatePhysLocation(remotePhysLocation.ID, remotePhysLocation, client.RequestOptions{})
-	if err != nil {
-		t.Errorf("cannot update Physical Location: %v - alerts: %+v", err, alerts.Alerts)
-	}
+func validatePhysicalLocationPagination(paginationParam string) utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
+		paginationResp := resp.([]tc.PhysLocation)
 
-	// Retrieve the PhysLocation to check PhysLocation name got updated
-	opts.QueryParameters.Del("name")
-	opts.QueryParameters.Set("id", strconv.Itoa(remotePhysLocation.ID))
-	resp, _, err = TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Errorf("cannot Physical Location '%s' (#%d) by ID: %v - alerts: %+v", firstPhysLocation.Name, remotePhysLocation.ID, err, resp.Alerts)
-	}
-	if len(resp.Response) != 1 {
-		t.Fatalf("Expected exactly one Physical Location to exist with ID %d, found: %d", remotePhysLocation.ID, len(resp.Response))
-	}
-	respPhysLocation := resp.Response[0]
-	if respPhysLocation.City != expectedPhysLocationCity {
-		t.Errorf("results do not match actual: %s, expected: %s", respPhysLocation.City, expectedPhysLocationCity)
+		opts := client.NewRequestOptions()
+		opts.QueryParameters.Set("orderby", "id")
+		respBase, _, err := TOSession.GetPhysLocations(opts)
+		assert.RequireNoError(t, err, "Cannot get Physical Locations: %v - alerts: %+v", err, respBase.Alerts)
+
+		pl := respBase.Response
+		assert.RequireGreaterOrEqual(t, len(pl), 3, "Need at least 3 Physical Locations in Traffic Ops to test pagination support, found: %d", len(pl))
+		switch paginationParam {
+		case "limit:":
+			assert.Exactly(t, pl[:1], paginationResp, "expected GET Physical Locations with limit = 1 to return first result")
+		case "offset":
+			assert.Exactly(t, pl[1:2], paginationResp, "expected GET Physical Locations with limit = 1, offset = 1 to return second result")
+		case "page":
+			assert.Exactly(t, pl[1:2], paginationResp, "expected GET Physical Locations with limit = 1, page = 2 to return second result")
+		}
 	}
-
 }
 
-func GetTestPhysLocations(t *testing.T) {
-	opts := client.NewRequestOptions()
-	for _, pl := range testData.PhysLocations {
-		opts.QueryParameters.Set("name", pl.Name)
-		resp, _, err := TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Errorf("cannot get Physical Location '%s': %v - alerts: %+v", pl.Name, err, resp.Alerts)
+func validatePhysicalLocationSort() utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		var physLocNames []string
+		physLocResp := resp.([]tc.PhysLocation)
+		for _, pl := range physLocResp {
+			physLocNames = append(physLocNames, pl.Name)
 		}
+		assert.Equal(t, true, sort.StringsAreSorted(physLocNames), "List is not sorted by their names: %v", physLocNames)
 	}
 }
 
-func GetSortPhysLocationsTest(t *testing.T) {
-	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("orderby", "id")
-	resp, _, err := TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Errorf("Unexpected error getting Physical Locations ordered by ID: %v - alerts: %+v", err, resp.Alerts)
-	}
-	sorted := sort.SliceIsSorted(resp.Response, func(i, j int) bool {
-		return resp.Response[i].ID < resp.Response[j].ID
-	})
-	if !sorted {
-		t.Error("expected response to be sorted by id")
+func validatePhysicalLocationIDSort() utils.CkReqFunc {
+	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
+		assert.RequireNotNil(t, resp, "Expected Physical Location response to not be nil.")
+		var physLocIDs []int
+		physLocResp := resp.([]tc.PhysLocation)
+		for _, pl := range physLocResp {
+			physLocIDs = append(physLocIDs, pl.ID)
+		}
+		assert.Equal(t, true, sort.IntsAreSorted(physLocIDs), "List is not sorted by their ids: %v", physLocIDs)
 	}
 }
 
-func GetDefaultSortPhysLocationsTest(t *testing.T) {
-	resp, _, err := TOSession.GetPhysLocations(client.RequestOptions{})
-	if err != nil {
-		t.Errorf("Unexpected error getting Physical Locations: %v - alerts: %+v", err, resp.Alerts)
-	}
-	sorted := sort.SliceIsSorted(resp.Response, func(i, j int) bool {
-		return resp.Response[i].Name < resp.Response[j].Name
-	})
-	if !sorted {
-		t.Error("expected response to be sorted by name")
+func GetRegionID(t *testing.T, name string) func() int {
+	return func() int {
+		opts := client.NewRequestOptions()
+		opts.QueryParameters.Set("name", name)
+		regions, _, err := TOSession.GetRegions(opts)
+		assert.RequireNoError(t, err, "Get Regions Request failed with error:", err)
+		assert.RequireEqual(t, 1, len(regions.Response), "Expected response object length 1, but got %d", len(regions.Response))
+		return regions.Response[0].ID
 	}
 }
 
-func DeleteTestPhysLocations(t *testing.T) {
-	opts := client.NewRequestOptions()
-	for _, pl := range testData.PhysLocations {
-		// Retrieve the PhysLocation by name so we can get the id for the Update
-		opts.QueryParameters.Set("name", pl.Name)
-		resp, _, err := TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Errorf("cannot get Physical Location by name '%s': %v - alerts: %+v", pl.Name, err, resp.Alerts)
-		}
-		if len(resp.Response) != 1 {
-			t.Errorf("Expected exactly one Physical Location to exist with name '%s', found: %d", pl.Name, len(resp.Response))
-			continue
-		}
-
-		respPhysLocation := resp.Response[0]
-
-		alerts, _, err := TOSession.DeletePhysLocation(respPhysLocation.ID, client.RequestOptions{})
-		if err != nil {
-			t.Errorf("Unexpected error deleting Physical Location '%s' (#%d): %v - alerts: %+v", respPhysLocation.Name, respPhysLocation.ID, err, alerts.Alerts)
-		}
-
-		// Retrieve the PhysLocation to see if it got deleted
-		resp, _, err = TOSession.GetPhysLocations(opts)
-		if err != nil {
-			t.Errorf("error getting Physical Location '%s' after deletion: %v - alerts: %+v", pl.Name, err, resp.Alerts)
-		}
-		if len(resp.Response) > 0 {
-			t.Errorf("expected Physical Location '%s' to be deleted, but it was found in Traffic Ops", pl.Name)
-		}
+func GetPhysicalLocationID(t *testing.T, name string) func() int {
+	return func() int {
+		opts := client.NewRequestOptions()
+		opts.QueryParameters.Set("name", name)
+		physicalLocations, _, err := TOSession.GetPhysLocations(opts)
+		assert.RequireNoError(t, err, "Get PhysLocation Request failed with error:", err)
+		assert.RequireEqual(t, 1, len(physicalLocations.Response), "Expected response object length 1, but got %d", len(physicalLocations.Response))
+		return physicalLocations.Response[0].ID
 	}
 }
 
-func CreatePhysLocationWithMismatchedRegionNameAndID(t *testing.T) {
-	resp, _, err := TOSession.GetRegions(client.NewRequestOptions())
-	if err != nil {
-		t.Errorf("Unexpected error getting regions: %v - alerts: %+v", err, resp.Alerts)
-	}
-	if len(resp.Response) < 2 {
-		t.Fatalf("expected at least two regions to be returned, but got none")
-	}
-	physLocation := tc.PhysLocation{
-		Address:    "100 blah lane",
-		City:       "foo",
-		Comments:   "comment",
-		Email:      "bar@foobar.com",
-		Name:       "testPhysicalLocation",
-		Phone:      "111-222-3333",
-		RegionID:   resp.Response[0].ID,
-		RegionName: resp.Response[1].Name,
-		ShortName:  "testLocation1",
-		State:      "CO",
-		Zip:        "80602",
-	}
-	_, _, err = TOSession.CreatePhysLocation(physLocation, client.NewRequestOptions())
-	if err == nil {
-		t.Fatalf("expected an error about mismatched region name and ID, but got nothing")
-	}
-
-	physLocation.RegionName = resp.Response[0].Name
-	_, _, err = TOSession.CreatePhysLocation(physLocation, client.NewRequestOptions())
-	if err != nil {
-		t.Fatalf("expected no error while creating phys location, but got %v", err)
-	}
-
-	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("name", "testPhysicalLocation")
-	response, _, err := TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Fatalf("cannot get Physical Location by name 'testPhysicalLocation': %v - alerts: %+v", err, resp.Alerts)
-	}
-	if len(response.Response) != 1 {
-		t.Fatalf("Expected exactly one Physical Location to exist with name 'testPhysicalLocation', found: %d", len(resp.Response))
-	}
-
-	_, _, err = TOSession.DeletePhysLocation(response.Response[0].ID, client.NewRequestOptions())
-	if err != nil {
-		t.Errorf("error deleteing physical location 'testPhysicalLocation': %v", err)
+func CreateTestPhysLocations(t *testing.T) {
+	for _, pl := range testData.PhysLocations {
+		resp, _, err := TOSession.CreatePhysLocation(pl, client.RequestOptions{})
+		assert.RequireNoError(t, err, "Could not create Physical Location '%s': %v - alerts: %+v", pl.Name, err, resp.Alerts)
 	}
 }
 
-func GetTestPaginationSupportPhysLocation(t *testing.T) {
-	opts := client.NewRequestOptions()
-	opts.QueryParameters.Set("orderby", "id")
-	resp, _, err := TOSession.GetPhysLocations(opts)
-	if err != nil {
-		t.Errorf("Unexpected error getting Physical Locations: %v - alerts: %+v", err, resp.Alerts)
-	}
-	physlocations := resp.Response
-
-	if len(physlocations) > 0 {
-		opts.QueryParameters = url.Values{}
-		opts.QueryParameters.Set("orderby", "id")
-		opts.QueryParameters.Set("limit", "1")
-		physlocationsWithLimit, _, err := TOSession.GetPhysLocations(opts)
-		if err == nil {
-			if !reflect.DeepEqual(physlocations[:1], physlocationsWithLimit.Response) {
-				t.Error("expected GET PhysLocation with limit = 1 to return first result")
-			}
-		} else {
-			t.Errorf("Unexpected error getting Physical Locations with a limit: %v - alerts: %+v", err, physlocationsWithLimit.Alerts)
-		}
-		if len(physlocations) > 1 {
-			opts.QueryParameters = url.Values{}
-			opts.QueryParameters.Set("orderby", "id")
-			opts.QueryParameters.Set("limit", "1")
-			opts.QueryParameters.Set("offset", "1")
-			physlocationsWithOffset, _, err := TOSession.GetPhysLocations(opts)
-			if err == nil {
-				if !reflect.DeepEqual(physlocations[1:2], physlocationsWithOffset.Response) {
-					t.Error("expected GET PhysLocation with limit = 1, offset = 1 to return second result")
-				}
-			} else {
-				t.Errorf("Unexpected error getting Physical Locations with a limit and an offset: %v - alerts: %+v", err, physlocationsWithOffset.Alerts)
-			}
-
-			opts.QueryParameters = url.Values{}
-			opts.QueryParameters.Set("orderby", "id")
-			opts.QueryParameters.Set("limit", "1")
-			opts.QueryParameters.Set("page", "2")
-			physlocationsWithPage, _, err := TOSession.GetPhysLocations(opts)
-			if err == nil {
-				if !reflect.DeepEqual(physlocations[1:2], physlocationsWithPage.Response) {
-					t.Error("expected GET PhysLocation with limit = 1, page = 2 to return second result")
-				}
-			} else {
-				t.Errorf("Unexpected error getting Physical Locations with a limit and a page: %v - alerts: %+v", err, physlocationsWithPage.Alerts)
-			}
-		} else {
-			t.Errorf("only one PhysLocation found, so offset functionality can't test")
-		}
-	} else {
-		t.Errorf("No PhysLocation found to check pagination")
-	}
-
-	opts.QueryParameters = url.Values{}
-	opts.QueryParameters.Set("limit", "-2")
-	resp, _, err = TOSession.GetPhysLocations(opts)
-	if err == nil {
-		t.Error("expected GET PhysLocation to return an error when limit is not bigger than -1")
-	} else if !strings.Contains(err.Error(), "must be bigger than -1") {
-		t.Errorf("expected GET PhysLocation to return an error for limit is not bigger than -1, actual error: %v - alerts: %+v", err, resp.Alerts)
-	}
-
-	opts.QueryParameters = url.Values{}
-	opts.QueryParameters.Set("limit", "1")
-	opts.QueryParameters.Set("offset", "0")
-	resp, _, err = TOSession.GetPhysLocations(opts)
-	if err == nil {
-		t.Error("expected GET PhysLocation to return an error when offset is not a positive integer")
-	} else if !strings.Contains(err.Error(), "must be a positive integer") {
-		t.Errorf("expected GET PhysLocation to return an error for offset is not a positive integer, actual error: %v - alerts: %+v", err, resp.Alerts)
-	}
+func DeleteTestPhysLocations(t *testing.T) {
+	physicalLocations, _, err := TOSession.GetPhysLocations(client.RequestOptions{})
+	assert.NoError(t, err, "Cannot get Physical Locations: %v - alerts: %+v", err, physicalLocations.Alerts)
 
-	opts.QueryParameters = url.Values{}
-	opts.QueryParameters.Set("limit", "1")
-	opts.QueryParameters.Set("page", "0")
-	resp, _, err = TOSession.GetPhysLocations(opts)
-	if err == nil {
-		t.Error("expected GET PhysLocation to return an error when page is not a positive integer")
-	} else if !strings.Contains(err.Error(), "must be a positive integer") {
-		t.Errorf("expected GET PhysLocation to return an error for page is not a positive integer, actual error: %v - alerts: %+v", err, resp.Alerts)
+	for _, pl := range physicalLocations.Response {
+		alerts, _, err := TOSession.DeletePhysLocation(pl.ID, client.RequestOptions{})
+		assert.NoError(t, err, "Unexpected error deleting Physical Location '%s' (#%d): %v - alerts: %+v", pl.Name, pl.ID, err, alerts.Alerts)
+		// Retrieve the PhysLocation to see if it got deleted
+		opts := client.NewRequestOptions()
+		opts.QueryParameters.Set("id", strconv.Itoa(pl.ID))
+		getPL, _, err := TOSession.GetPhysLocations(opts)
+		assert.NoError(t, err, "Error getting Physical Location '%s' after deletion: %v - alerts: %+v", pl.Name, err, getPL.Alerts)
+		assert.Equal(t, 0, len(getPL.Response), "Expected Physical Location '%s' to be deleted, but it was found in Traffic Ops", pl.Name)
 	}
 }
diff --git a/traffic_ops/testing/api/v4/tc-fixtures.json b/traffic_ops/testing/api/v4/tc-fixtures.json
index 134df3008..ccfbe1d10 100644
--- a/traffic_ops/testing/api/v4/tc-fixtures.json
+++ b/traffic_ops/testing/api/v4/tc-fixtures.json
@@ -2016,6 +2016,19 @@
             "shortName": "atlanta",
             "state": "GA",
             "zip": "30301"
+        },
+        {
+            "address": "1234 delete way",
+            "city": "Denver",
+            "comments": null,
+            "email": null,
+            "name": "testDelete",
+            "phone": "303-222-2222",
+            "poc": null,
+            "region": "region1",
+            "shortName": "testdelete",
+            "state": "C0",
+            "zip": "80202"
         }
     ],
     "profiles": [
diff --git a/traffic_ops/traffic_ops_golang/config/config_test.go b/traffic_ops/traffic_ops_golang/config/config_test.go
index 70c03d1f4..ee01de555 100644
--- a/traffic_ops/traffic_ops_golang/config/config_test.go
+++ b/traffic_ops/traffic_ops_golang/config/config_test.go
@@ -99,6 +99,7 @@ const (
 		"workers" : 12
 	},
 	"user_cache_refresh_interval_sec": 30,
+	"server_update_status_cache_refresh_interval_sec": 15,
 	"disable_auto_cert_deletion": true,
 	"traffic_ops_golang" : {
 		"port" : "443",
@@ -239,7 +240,10 @@ func TestLoadConfig(t *testing.T) {
 		t.Errorf("expected traffic_vault_backend to be 'something', actual: '%s'", cfg.TrafficVaultBackend)
 	}
 	if cfg.UserCacheRefreshIntervalSec != 30 {
-		t.Errorf("expected user_refresh_interval_sec: 30, actual: %d", cfg.UserCacheRefreshIntervalSec)
+		t.Errorf("expected user_cache_refresh_interval_sec: 30, actual: %d", cfg.UserCacheRefreshIntervalSec)
+	}
+	if cfg.ServerUpdateStatusCacheRefreshIntervalSec != 15 {
+		t.Errorf("expected server_update_status_cache_refresh_interval_sec: 15, actual: %d", cfg.ServerUpdateStatusCacheRefreshIntervalSec)
 	}
 	tvConfig := make(map[string]string)
 	err = json.Unmarshal(cfg.TrafficVaultConfig, &tvConfig)
diff --git a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
index bdcbf4e91..0d0ce5e7e 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_update_status_test.go
@@ -26,7 +26,7 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+
 	"github.com/jmoiron/sqlx"
 	sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
@@ -58,7 +58,7 @@ func TestGetServerUpdateStatus(t *testing.T) {
 	}
 	defer tx.Commit()
 
-	result, err, _ := getServerUpdateStatus(tx, &config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 20}}, "host_name_1")
+	result, err, _ := getServerUpdateStatus(tx, "host_name_1")
 	if err != nil {
 		t.Errorf("getServerUpdateStatus: %v", err)
 	}
@@ -75,3 +75,130 @@ func TestGetServerUpdateStatus(t *testing.T) {
 
 	reflect.DeepEqual(expected, result)
 }
+
+func TestGetServerUpdateStatuses(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
+	}
+	defer mockDB.Close()
+
+	mock.ExpectBegin()
+	revalPendingRows := sqlmock.NewRows([]string{"value"})
+	revalPendingRows.AddRow(true)
+	mock.ExpectQuery("SELECT").WillReturnRows(revalPendingRows)
+
+	serverInfoRows := sqlmock.NewRows([]string{"id", "host_name", "type", "cdn_id", "status",
+		"cachegroup", "config_update_time", "config_apply_time", "revalidate_update_time",
+		"revalidate_apply_time"})
+	tenSecAfter := time.UnixMilli(10000)
+	epoch := time.UnixMilli(0)
+	serverInfoRows.AddRow(1, "edge1", tc.CacheTypeEdge.String(), 1, tc.CacheStatusReported.String(), 1, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(2, "mid1", tc.CacheTypeMid.String(), 1, tc.CacheStatusReported.String(), 2, tenSecAfter, epoch, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(3, "edge2", tc.CacheTypeEdge.String(), 2, tc.CacheStatusReported.String(), 1, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(4, "mid2", tc.CacheTypeMid.String(), 2, tc.CacheStatusReported.String(), 2, tenSecAfter, tenSecAfter, tenSecAfter, tenSecAfter)
+	serverInfoRows.AddRow(5, "mid3", tc.CacheTypeMid.String(), 2, tc.CacheStatusReported.String(), 3, tenSecAfter, tenSecAfter, tenSecAfter, epoch)
+	mock.ExpectQuery("SELECT").WillReturnRows(serverInfoRows)
+
+	cachegroupRows := sqlmock.NewRows([]string{"id", "parent_cachegroup_id", "secondary_parent_cachegroup_id"})
+	cachegroupRows.AddRow(1, 2, nil)
+	cachegroupRows.AddRow(2, nil, nil)
+	cachegroupRows.AddRow(3, nil, nil)
+	mock.ExpectQuery("SELECT").WillReturnRows(cachegroupRows)
+
+	topologyCachegroupRows := sqlmock.NewRows([]string{"id", "array_agg"})
+	topologyCachegroupRows.AddRow(1, "{3}")
+	mock.ExpectQuery("SELECT").WillReturnRows(topologyCachegroupRows)
+
+	mock.ExpectCommit()
+
+	expected := map[string][]tc.ServerUpdateStatusV40{
+		"edge1": {
+			{
+				HostName:             "edge1",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               1,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        true,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid1": {
+			{
+				HostName:             "mid1",
+				UpdatePending:        true,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               2,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &epoch,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"edge2": {
+			{
+				HostName:             "edge2",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               3,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   true,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid2": {
+			{
+				HostName:             "mid2",
+				UpdatePending:        false,
+				RevalPending:         false,
+				UseRevalPending:      true,
+				HostId:               4,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &tenSecAfter,
+			},
+		},
+		"mid3": {
+			{
+				HostName:             "mid3",
+				UpdatePending:        false,
+				RevalPending:         true,
+				UseRevalPending:      true,
+				HostId:               5,
+				Status:               tc.CacheStatusReported.String(),
+				ParentPending:        false,
+				ParentRevalPending:   false,
+				ConfigUpdateTime:     &tenSecAfter,
+				ConfigApplyTime:      &tenSecAfter,
+				RevalidateUpdateTime: &tenSecAfter,
+				RevalidateApplyTime:  &epoch,
+			},
+		},
+	}
+	actual, err := getServerUpdateStatuses(mockDB, 20*time.Second)
+	if err != nil {
+		t.Fatalf("unexpected error getting server update statuses: %s", err)
+	}
+	if !reflect.DeepEqual(expected, actual) {
+		t.Errorf("getting server update statuses - expected: %+v, actual: %+v", expected, actual)
+	}
+}
diff --git a/traffic_portal/test/integration/Data/deliveryservicerequest.ts b/traffic_portal/test/integration/Data/deliveryservicerequest.ts
index 81f997f46..ce74c531c 100644
--- a/traffic_portal/test/integration/Data/deliveryservicerequest.ts
+++ b/traffic_portal/test/integration/Data/deliveryservicerequest.ts
@@ -37,7 +37,7 @@ export const deliveryservicerequest = {
                     CDN: "dummycdn",
                     RawText: "test",
                     validationMessage: "Created request to create the cdntesting delivery service",
-                    FullfillMessage: "Delivery Service [ cdntesting ] created",
+                    FullfillMessage: "Delivery Service creation was successful",
                     CompleteMessage: "Delivery service request status was updated"
                 }
             ],
@@ -67,7 +67,7 @@ export const deliveryservicerequest = {
                     RawText: "test",
                     validationMessage: "Created request to create the cdntesting3 delivery service",
                     UpdateMessage: "Updated delivery service request for cdntesting3 and set status to submitted",
-                    FullfillMessage: "Delivery Service [ cdntesting3 ] created",
+                    FullfillMessage: "Delivery Service creation was successful",
                     CompleteMessage: "Delivery service request status was updated"
                 }
             ]
diff --git a/traffic_portal/test/integration/Data/deliveryservices.ts b/traffic_portal/test/integration/Data/deliveryservices.ts
index 4f20f4b38..cc90d8d17 100644
--- a/traffic_portal/test/integration/Data/deliveryservices.ts
+++ b/traffic_portal/test/integration/Data/deliveryservices.ts
@@ -312,21 +312,21 @@ export const deliveryservices = {
 					Name: "tpdservice1",
 					Tenant: "tenantSame",
 					Type: "ANY_MAP",
-					validationMessage: "Delivery Service [ tpdservice1 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				},
 				{
 					description: "create DNS delivery service",
 					Name: "tpdservice2",
 					Tenant: "tenantSame",
 					Type: "DNS",
-					validationMessage: "Delivery Service [ tpdservice2 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				},
 				{
 					description: "create STEERING delivery service",
 					Name: "tpdservice3",
 					Tenant: "tenantSame",
 					Type: "STEERING",
-					validationMessage: "Delivery Service [ tpdservice3 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				}
 			],
 			update: [
@@ -334,7 +334,7 @@ export const deliveryservices = {
 					description: "update delivery service display name",
 					Name: "tpdservice1",
 					NewName: "TPServiceNew1",
-					validationMessage: "Delivery Service [ tpdservice1 ] updated"
+					validationMessage: "Delivery Service update was successful"
 				}
 			],
 			assignserver: [
@@ -357,17 +357,17 @@ export const deliveryservices = {
 				{
 					description: "delete a delivery service",
 					Name: "tpdservice1",
-					validationMessage: "Delivery service [ tpdservice1 ] deleted."
+					validationMessage: "ds was deleted."
 				},
 				{
 					description: "delete a delivery service",
 					Name: "tpdservice2",
-					validationMessage: "Delivery service [ tpdservice2 ] deleted."
+					validationMessage: "ds was deleted."
 				},
 				{
 					description: "delete a delivery service",
 					Name: "tpdservice3",
-					validationMessage: "Delivery service [ tpdservice3 ] deleted."
+					validationMessage: "ds was deleted."
 				}
 			]
 		},
@@ -434,21 +434,21 @@ export const deliveryservices = {
 					Name: "optpdservice1",
 					Tenant: "tenantSame",
 					Type: "ANY_MAP",
-					validationMessage: "Delivery Service [ optpdservice1 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				},
 				{
 					description: "create DNS delivery service",
 					Name: "optpdservice2",
 					Tenant: "tenantSame",
 					Type: "DNS",
-					validationMessage: "Delivery Service [ optpdservice2 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				},
 				{
 					description: "create STEERING delivery service",
 					Name: "optpdservice3",
 					Tenant: "tenantSame",
 					Type: "STEERING",
-					validationMessage: "Delivery Service [ optpdservice3 ] created"
+					validationMessage: "Delivery Service creation was successful"
 				}
 			],
 			update: [
@@ -456,7 +456,7 @@ export const deliveryservices = {
 					description: "update delivery service display name",
 					Name: "optpdservice1",
 					NewName: "opTPServiceNew1",
-					validationMessage: "Delivery Service [ optpdservice1 ] updated"
+					validationMessage: "Delivery Service update was successful"
 				}
 			],
 			assignserver: [
@@ -479,17 +479,17 @@ export const deliveryservices = {
 				{
 					description: "delete a delivery service",
 					Name: "optpdservice1",
-					validationMessage: "Delivery service [ optpdservice1 ] deleted."
+					validationMessage: "ds was deleted."
 				},
 				{
 					description: "delete a delivery service",
 					Name: "optpdservice2",
-					validationMessage: "Delivery service [ optpdservice2 ] deleted."
+					validationMessage: "ds was deleted."
 				},
 				{
 					description: "delete a delivery service",
 					Name: "optpdservice3",
-					validationMessage: "Delivery service [ optpdservice3 ] deleted."
+					validationMessage: "ds was deleted."
 				}
 			]
 		}
diff --git a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
index ac98c1526..8176ad8f7 100644
--- a/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
+++ b/traffic_portal/test/integration/PageObjects/DeliveryServicePage.po.ts
@@ -87,9 +87,6 @@ export class DeliveryServicePage extends BasePage {
     let result = false;
     let type: string = deliveryservice.Type;
     const basePage = new BasePage();
-    if (deliveryservice.validationMessage.includes("created")) {
-      deliveryservice.validationMessage = deliveryservice.validationMessage.replace(deliveryservice.Name, deliveryservice.Name + this.randomize)
-    }
     await this.btnMore.click();
     await this.btnCreateNewDeliveryServices.click();
     await this.mnuFormDropDown.sendKeys(type);
@@ -164,9 +161,6 @@ export class DeliveryServicePage extends BasePage {
   public async UpdateDeliveryService(deliveryservice: UpdateDeliveryService): Promise<boolean | undefined> {
     let result: boolean | undefined = false;
     const basePage = new BasePage();
-    if (deliveryservice.validationMessage.includes("updated")) {
-      deliveryservice.validationMessage = deliveryservice.validationMessage.replace(deliveryservice.Name, deliveryservice.Name + this.randomize)
-    }
     switch (deliveryservice.description) {
       case "update delivery service display name":
         await this.txtDisplayName.clear();