You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by zr...@apache.org on 2023/03/14 15:01:34 UTC

[trafficcontrol] branch master updated: Add unit tests for deliveryservice folder in Traffic Ops (#7390)

This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new da0dc68067 Add unit tests for deliveryservice folder in Traffic Ops (#7390)
da0dc68067 is described below

commit da0dc6806798b30fcf2b3843d1054f00fd6bea10
Author: Srijeet Chatterjee <30...@users.noreply.github.com>
AuthorDate: Tue Mar 14 09:01:27 2023 -0600

    Add unit tests for deliveryservice folder in Traffic Ops (#7390)
    
    * wip
    
    * wip
    
    * Adding tests for ds folder
    
    * sort imports
    
    * go fmt
    
    * license
---
 .../deliveryservice/acme_test.go                   | 169 ++++++++
 .../deliveryservice/gencert_test.go                |  31 ++
 .../deliveryservice/request/requests_test.go       | 465 +++++++++++++++++++++
 .../deliveryservice/request/validate_test.go       | 278 ++++++++++++
 .../deliveryservice/safe_test.go                   |  70 ++++
 .../deliveryservice/servers/delete_test.go         | 119 ++++++
 .../deliveryservice/servers/servers.go             |   2 +-
 .../deliveryservice/servers/servers_test.go        | 339 +++++++++++++++
 8 files changed, 1472 insertions(+), 1 deletion(-)

diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/acme_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/acme_test.go
new file mode 100644
index 0000000000..24089cd643
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/acme_test.go
@@ -0,0 +1,169 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config"
+	"github.com/go-acme/lego/challenge/dns01"
+
+	"github.com/jmoiron/sqlx"
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestGetStoredAcmeAccountInfo(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	priv, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("expected no error while generating key, but got %v", err)
+	}
+	keyBuf := bytes.Buffer{}
+	keyDer := x509.MarshalPKCS1PrivateKey(priv)
+	err = pem.Encode(&keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDer})
+	if err != nil {
+		t.Fatalf("expected no error while encoding key, but got %v", err)
+	}
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"email", "private_key", "uri"})
+	rows.AddRow("testuser@blah.com", keyBuf.Bytes(), "https://uri.com")
+	mock.ExpectQuery("SELECT email, private_key, uri").WithArgs("testuser@blah.com", "Lets Encrypt").WillReturnRows(rows)
+
+	info, err := getStoredAcmeAccountInfo(db.MustBegin().Tx, "testuser@blah.com", "Lets Encrypt")
+	if err != nil {
+		t.Errorf("expected no error while getting stored acme account into, but got %v", err)
+	}
+	if info == nil {
+		t.Fatalf("expected valid acme account info in response, but got nothing")
+	}
+	if info.Email != "testuser@blah.com" {
+		t.Errorf("expected email to be testuser@blah.com, but got %s", info.Email)
+	}
+	if info.Key != string(keyBuf.Bytes()) {
+		t.Errorf("expected key to be %s, but got %s", string(keyBuf.Bytes()), info.Key)
+	}
+	if info.URI != "https://uri.com" {
+		t.Errorf("expected uri to be https://uri.com, but got %s", info.URI)
+	}
+}
+
+func TestGetAcmeAccountConfig(t *testing.T) {
+	cfgAcmeAccounts := make([]config.ConfigAcmeAccount, 0)
+	cfg := config.Config{
+		AcmeAccounts: cfgAcmeAccounts,
+		ConfigLetsEncrypt: config.ConfigLetsEncrypt{
+			Email:       "testuser@apache.org",
+			Environment: "production",
+		},
+	}
+	c := GetAcmeAccountConfig(&cfg, tc.LetsEncryptAuthType)
+	if c == nil {
+		t.Fatalf("expected a valid Acme Account Config in response, but got nothing")
+	}
+	if c.UserEmail != cfg.Email {
+		t.Errorf("expected user email to be %s, but got %s", cfg.Email, c.UserEmail)
+	}
+	if c.AcmeUrl != "https://acme-v02.api.letsencrypt.org/directory" {
+		t.Errorf("expected AcmeProvider to be https://acme-v02.api.letsencrypt.org/directory, but got %s", c.AcmeUrl)
+	}
+	if c.AcmeProvider != tc.LetsEncryptAuthType {
+		t.Errorf("expected AcmeProvider to be Lets Encrypt, but got %s", c.AcmeProvider)
+	}
+}
+
+func TestDNSProviderTrafficRouter_Present(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	d := DNSProviderTrafficRouter{
+		db:    db,
+		xmlId: util.Ptr("dsXMLID"),
+	}
+	keyAuthShaBytes := sha256.Sum256([]byte("blah"))
+	value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
+	mock.ExpectBegin()
+	mock.ExpectExec("INSERT INTO dnschallenges").WithArgs("_acme-challenge.test.", value, *d.xmlId).WillReturnResult(sqlmock.NewResult(1, 1))
+	mock.ExpectCommit()
+	err = d.Present("test", "token", "blah")
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+}
+
+func TestDNSProviderTrafficRouter_Cleanup(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	d := DNSProviderTrafficRouter{
+		db:    db,
+		xmlId: util.Ptr("dsXMLID"),
+	}
+	keyAuthShaBytes := sha256.Sum256([]byte("blah"))
+	value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
+	mock.ExpectBegin()
+	mock.ExpectExec("DELETE FROM dnschallenges").WithArgs("_acme-challenge.test.", value).WillReturnResult(sqlmock.NewResult(1, 1))
+	mock.ExpectCommit()
+	err = d.CleanUp("test", "token", "blah")
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+}
+
+func TestGetRecord(t *testing.T) {
+	fqdn, val := dns01.GetRecord("test", "blah")
+	keyAuthShaBytes := sha256.Sum256([]byte("blah"))
+	value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
+	if fqdn != "_acme-challenge.test." {
+		t.Errorf("expected fqdn to be _acme-challenge.test., but got %s", fqdn)
+	}
+	if val != value {
+		t.Errorf("expected returned value to be %s, but got %s", value, val)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/gencert_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/gencert_test.go
new file mode 100644
index 0000000000..724c9d2e86
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/gencert_test.go
@@ -0,0 +1,31 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"testing"
+)
+
+func TestGenerateCert(t *testing.T) {
+	_, _, _, err := GenerateCert("localhost", "US", "Denver", "CO", "Comcast", "IPCDN")
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
index 7e403fe3b6..388288d008 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/requests_test.go
@@ -20,12 +20,477 @@ package request
  */
 
 import (
+	"net/http"
 	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 
 	"github.com/jmoiron/sqlx"
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
+func TestInsert(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("opening mock database: %v", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+	inf := api.APIInfo{
+		Params:    nil,
+		IntParams: nil,
+		User: &auth.CurrentUser{
+			UserName:     "testUser",
+			ID:           1,
+			PrivLevel:    10,
+			TenantID:     1,
+			Role:         1,
+			RoleName:     "testRole",
+			Capabilities: nil,
+			UCDN:         "",
+		},
+		ReqID:    0,
+		Version:  nil,
+		Tx:       db.MustBegin(),
+		CancelTx: nil,
+		Vault:    nil,
+		Config:   nil,
+	}
+	dsr := tc.DeliveryServiceRequestV5{
+		Assignee:       util.StrPtr("assignee"),
+		AssigneeID:     util.IntPtr(25),
+		Author:         "test",
+		AuthorID:       util.IntPtr(35),
+		ChangeType:     tc.DSRChangeTypeUpdate,
+		CreatedAt:      time.Now(),
+		LastEditedBy:   "test",
+		LastEditedByID: util.IntPtr(35),
+		LastUpdated:    time.Now(),
+		Original:       nil,
+		Requested:      &tc.DeliveryServiceV5{},
+		Status:         tc.RequestStatusDraft,
+		XMLID:          "dsXMLID",
+	}
+
+	rows := sqlmock.NewRows([]string{"id", "last_updated", "created_at"})
+	rows.AddRow(1, time.Now(), time.Now())
+	mock.ExpectQuery("INSERT INTO deliveryservice_request*").WillReturnRows(rows)
+
+	rows2 := sqlmock.NewRows([]string{"active", "anonymous_blocking_enabled", "ccr_dns_ttl", "cdn_id", "cdnname", "check_path",
+		"consistent_hash_regex", "deep_caching_type", "display_name", "dns_bypass_cname", "dns_bypass_ip", "dns_bypass_ip6",
+		"dns_bypass_ttl", "dscp", "ecs_enabled", "edge_header_rewrite", "first_header_rewrite", "geolimit_redirect_url",
+		"geo_limit", "geo_limit_countries", "geo_provider", "global_max_mbps", "global_max_tps", "fq_pacing_rate", "http_bypass_fqdn",
+		"id", "info_url", "initial_dispersion", "inner_header_rewrite", "ipv6_routing_enabled", "last_header_rewrite", "last_updated",
+		"logs_enabled", "long_desc", "long_desc_1", "long_desc_2", "max_dns_answers", "max_origin_connections", "max_request_header_bytes",
+		"mid_header_rewrite", "miss_lat", "miss_long", "multi_site_origin", "org_server_fqdn ", "origin_shield", "profileid", "profile_name",
+		"profile_description", "protocol", "qstring_ignore", "query_keys", "range_request_handling", "regex_remap", "regional", "regional_geo_blocking",
+		"remap_text", "required_capabilities", "routing_name", "service_category", "signing_algorithm", "range_slice_block_size", "ssl_key_version", "tenant_id",
+		"name", "tls_versions", "topology", "tr_request_headers", "tr_response_headers", "name", "type_id", "xml_id", "cdn_domain",
+	})
+	ds := tc.DeliveryServiceV5{
+		Active:                   tc.DeliveryServiceActiveState("PRIMED"),
+		AnonymousBlockingEnabled: false,
+		CCRDNSTTL:                util.IntPtr(20),
+		CDNID:                    11,
+		CDNName:                  util.StrPtr("testCDN"),
+		CheckPath:                util.StrPtr("blah"),
+		ConsistentHashRegex:      nil,
+		DeepCachingType:          tc.DeepCachingTypeNever,
+		DisplayName:              "ds",
+		DNSBypassCNAME:           nil,
+		DNSBypassIP:              nil,
+		DNSBypassIP6:             nil,
+		DNSBypassTTL:             nil,
+		DSCP:                     0,
+		EcsEnabled:               false,
+		EdgeHeaderRewrite:        nil,
+		FirstHeaderRewrite:       nil,
+		GeoLimitRedirectURL:      nil,
+		GeoLimit:                 0,
+		GeoLimitCountries:        nil,
+		GeoProvider:              0,
+		GlobalMaxMBPS:            nil,
+		GlobalMaxTPS:             nil,
+		FQPacingRate:             nil,
+		HTTPBypassFQDN:           nil,
+		ID:                       util.IntPtr(1),
+		InfoURL:                  nil,
+		InitialDispersion:        util.IntPtr(1),
+		InnerHeaderRewrite:       nil,
+		IPV6RoutingEnabled:       util.BoolPtr(true),
+		LastHeaderRewrite:        nil,
+		LastUpdated:              time.Now(),
+		LogsEnabled:              true,
+		LongDesc:                 "",
+		MaxDNSAnswers:            util.IntPtr(5),
+		MaxOriginConnections:     util.IntPtr(2),
+		MaxRequestHeaderBytes:    util.IntPtr(0),
+		MidHeaderRewrite:         nil,
+		MissLat:                  util.FloatPtr(0.0),
+		MissLong:                 util.FloatPtr(0.0),
+		MultiSiteOrigin:          false,
+		OrgServerFQDN:            util.StrPtr("http://1.2.3.4"),
+		OriginShield:             nil,
+		ProfileID:                util.IntPtr(99),
+		ProfileName:              util.StrPtr("profile99"),
+		ProfileDesc:              nil,
+		Protocol:                 util.IntPtr(1),
+		QStringIgnore:            nil,
+		RangeRequestHandling:     nil,
+		RegexRemap:               nil,
+		Regional:                 false,
+		RegionalGeoBlocking:      false,
+		RemapText:                nil,
+		RequiredCapabilities:     nil,
+		RoutingName:              "",
+		ServiceCategory:          nil,
+		SigningAlgorithm:         nil,
+		RangeSliceBlockSize:      nil,
+		SSLKeyVersion:            nil,
+		TenantID:                 100,
+		Tenant:                   util.StrPtr("tenant100"),
+		TLSVersions:              nil,
+		Topology:                 nil,
+		TRRequestHeaders:         nil,
+		TRResponseHeaders:        nil,
+		Type:                     util.StrPtr("type101"),
+		TypeID:                   101,
+		XMLID:                    "dsXMLID",
+	}
+
+	rows2.AddRow(
+		ds.Active,
+		ds.AnonymousBlockingEnabled,
+		ds.CCRDNSTTL,
+		ds.CDNID,
+		ds.CDNName,
+		ds.CheckPath,
+		ds.ConsistentHashRegex,
+		ds.DeepCachingType,
+		ds.DisplayName,
+		ds.DNSBypassCNAME,
+		ds.DNSBypassIP,
+		ds.DNSBypassIP6,
+		ds.DNSBypassTTL,
+		ds.DSCP,
+		ds.EcsEnabled,
+		ds.EdgeHeaderRewrite,
+		ds.FirstHeaderRewrite,
+		ds.GeoLimitRedirectURL,
+		ds.GeoLimit,
+		nil,
+		ds.GeoProvider,
+		ds.GlobalMaxMBPS,
+		ds.GlobalMaxTPS,
+		ds.FQPacingRate,
+		ds.HTTPBypassFQDN,
+		ds.ID,
+		ds.InfoURL,
+		ds.InitialDispersion,
+		ds.InnerHeaderRewrite,
+		ds.IPV6RoutingEnabled,
+		ds.LastHeaderRewrite,
+		ds.LastUpdated,
+		ds.LogsEnabled,
+		ds.LongDesc,
+		ds.LongDesc,
+		ds.LongDesc,
+		ds.MaxDNSAnswers,
+		ds.MaxOriginConnections,
+		ds.MaxRequestHeaderBytes,
+		ds.MidHeaderRewrite,
+		ds.MissLat,
+		ds.MissLong,
+		ds.MultiSiteOrigin,
+		ds.OrgServerFQDN,
+		ds.OriginShield,
+		ds.ProfileID,
+		ds.ProfileName,
+		ds.ProfileDesc,
+		ds.Protocol,
+		ds.QStringIgnore,
+		nil,
+		ds.RangeRequestHandling,
+		ds.RegexRemap,
+		ds.Regional,
+		ds.RegionalGeoBlocking,
+		ds.RemapText,
+		nil,
+		ds.RoutingName,
+		ds.ServiceCategory,
+		ds.SigningAlgorithm,
+		ds.RangeSliceBlockSize,
+		ds.SSLKeyVersion,
+		ds.TenantID,
+		ds.Tenant,
+		nil,
+		ds.Topology,
+		ds.TRRequestHeaders,
+		ds.TRResponseHeaders,
+		ds.Type,
+		ds.TypeID,
+		ds.XMLID,
+		"cdn_domain_name")
+
+	mock.ExpectQuery("SELECT ds.active*").WillReturnRows(rows2)
+
+	rows3 := sqlmock.NewRows([]string{
+		"ds_name",
+		"type",
+		"pattern",
+		"set_number",
+	})
+	rows3.AddRow(
+		"dsXMLID",
+		"HOST_REGEXP",
+		".*\\.dsXMLID\\..*",
+		0)
+	mock.ExpectQuery("SELECT ds.xml_id as ds_name*").WillReturnRows(rows3)
+
+	sc, userErr, sysErr := insert(&dsr, &inf)
+
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("expected no error, but got userErr: %v, sysErr: %v", userErr, sysErr)
+	}
+	if sc != http.StatusOK {
+		t.Fatalf("expected a 200 status code, but got %d", sc)
+	}
+	if dsr.Original == nil {
+		t.Fatalf("expected original to be a valid delivery service, but got nothing")
+	}
+	if dsr.Original.XMLID != "dsXMLID" {
+		t.Fatalf("expected original to have a DS with XMLID 'dsXMLID', but got %s", dsr.Original.XMLID)
+	}
+
+}
+func TestGetOriginals(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("opening mock database: %v", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+	ID := 66
+	ids := []int{ID}
+	needOriginals := make(map[int][]*tc.DeliveryServiceRequestV5)
+	dsr := tc.DeliveryServiceRequestV5{
+		Assignee:       util.StrPtr("assignee"),
+		AssigneeID:     util.IntPtr(25),
+		Author:         "test",
+		AuthorID:       util.IntPtr(35),
+		ChangeType:     tc.DSRChangeTypeUpdate,
+		CreatedAt:      time.Now(),
+		ID:             util.IntPtr(1),
+		LastEditedBy:   "test",
+		LastEditedByID: util.IntPtr(35),
+		LastUpdated:    time.Now(),
+		Original:       nil,
+		Requested:      nil,
+		Status:         tc.RequestStatusDraft,
+		XMLID:          "dsXMLID",
+	}
+	needOriginals[ID] = []*tc.DeliveryServiceRequestV5{&dsr}
+
+	rows := sqlmock.NewRows([]string{"active", "anonymous_blocking_enabled", "ccr_dns_ttl", "cdn_id", "cdnname", "check_path",
+		"consistent_hash_regex", "deep_caching_type", "display_name", "dns_bypass_cname", "dns_bypass_ip", "dns_bypass_ip6",
+		"dns_bypass_ttl", "dscp", "ecs_enabled", "edge_header_rewrite", "first_header_rewrite", "geolimit_redirect_url",
+		"geo_limit", "geo_limit_countries", "geo_provider", "global_max_mbps", "global_max_tps", "fq_pacing_rate", "http_bypass_fqdn",
+		"id", "info_url", "initial_dispersion", "inner_header_rewrite", "ipv6_routing_enabled", "last_header_rewrite", "last_updated",
+		"logs_enabled", "long_desc", "long_desc_1", "long_desc_2", "max_dns_answers", "max_origin_connections", "max_request_header_bytes",
+		"mid_header_rewrite", "miss_lat", "miss_long", "multi_site_origin", "org_server_fqdn ", "origin_shield", "profileid", "profile_name",
+		"profile_description", "protocol", "qstring_ignore", "query_keys", "range_request_handling", "regex_remap", "regional", "regional_geo_blocking",
+		"remap_text", "required_capabilities", "routing_name", "service_category", "signing_algorithm", "range_slice_block_size", "ssl_key_version", "tenant_id",
+		"name", "tls_versions", "topology", "tr_request_headers", "tr_response_headers", "name", "type_id", "xml_id", "cdn_domain",
+	})
+	ds := tc.DeliveryServiceV5{
+		Active:                   tc.DeliveryServiceActiveState("PRIMED"),
+		AnonymousBlockingEnabled: false,
+		CCRDNSTTL:                util.IntPtr(20),
+		CDNID:                    11,
+		CDNName:                  util.StrPtr("testCDN"),
+		CheckPath:                util.StrPtr("blah"),
+		ConsistentHashRegex:      nil,
+		DeepCachingType:          tc.DeepCachingTypeNever,
+		DisplayName:              "ds",
+		DNSBypassCNAME:           nil,
+		DNSBypassIP:              nil,
+		DNSBypassIP6:             nil,
+		DNSBypassTTL:             nil,
+		DSCP:                     0,
+		EcsEnabled:               false,
+		EdgeHeaderRewrite:        nil,
+		FirstHeaderRewrite:       nil,
+		GeoLimitRedirectURL:      nil,
+		GeoLimit:                 0,
+		GeoLimitCountries:        nil,
+		GeoProvider:              0,
+		GlobalMaxMBPS:            nil,
+		GlobalMaxTPS:             nil,
+		FQPacingRate:             nil,
+		HTTPBypassFQDN:           nil,
+		ID:                       util.IntPtr(ID),
+		InfoURL:                  nil,
+		InitialDispersion:        util.IntPtr(1),
+		InnerHeaderRewrite:       nil,
+		IPV6RoutingEnabled:       util.BoolPtr(true),
+		LastHeaderRewrite:        nil,
+		LastUpdated:              time.Now(),
+		LogsEnabled:              true,
+		LongDesc:                 "",
+		MaxDNSAnswers:            util.IntPtr(5),
+		MaxOriginConnections:     util.IntPtr(2),
+		MaxRequestHeaderBytes:    util.IntPtr(0),
+		MidHeaderRewrite:         nil,
+		MissLat:                  util.FloatPtr(0.0),
+		MissLong:                 util.FloatPtr(0.0),
+		MultiSiteOrigin:          false,
+		OrgServerFQDN:            util.StrPtr("http://1.2.3.4"),
+		OriginShield:             nil,
+		ProfileID:                util.IntPtr(99),
+		ProfileName:              util.StrPtr("profile99"),
+		ProfileDesc:              nil,
+		Protocol:                 util.IntPtr(1),
+		QStringIgnore:            nil,
+		RangeRequestHandling:     nil,
+		RegexRemap:               nil,
+		Regional:                 false,
+		RegionalGeoBlocking:      false,
+		RemapText:                nil,
+		RequiredCapabilities:     nil,
+		RoutingName:              "",
+		ServiceCategory:          nil,
+		SigningAlgorithm:         nil,
+		RangeSliceBlockSize:      nil,
+		SSLKeyVersion:            nil,
+		TenantID:                 100,
+		Tenant:                   util.StrPtr("tenant100"),
+		TLSVersions:              nil,
+		Topology:                 nil,
+		TRRequestHeaders:         nil,
+		TRResponseHeaders:        nil,
+		Type:                     util.StrPtr("type101"),
+		TypeID:                   101,
+		XMLID:                    "dsXMLID",
+	}
+	rows.AddRow(
+		ds.Active,
+		ds.AnonymousBlockingEnabled,
+		ds.CCRDNSTTL,
+		ds.CDNID,
+		ds.CDNName,
+		ds.CheckPath,
+		ds.ConsistentHashRegex,
+		ds.DeepCachingType,
+		ds.DisplayName,
+		ds.DNSBypassCNAME,
+		ds.DNSBypassIP,
+		ds.DNSBypassIP6,
+		ds.DNSBypassTTL,
+		ds.DSCP,
+		ds.EcsEnabled,
+		ds.EdgeHeaderRewrite,
+		ds.FirstHeaderRewrite,
+		ds.GeoLimitRedirectURL,
+		ds.GeoLimit,
+		nil,
+		ds.GeoProvider,
+		ds.GlobalMaxMBPS,
+		ds.GlobalMaxTPS,
+		ds.FQPacingRate,
+		ds.HTTPBypassFQDN,
+		ds.ID,
+		ds.InfoURL,
+		ds.InitialDispersion,
+		ds.InnerHeaderRewrite,
+		ds.IPV6RoutingEnabled,
+		ds.LastHeaderRewrite,
+		ds.LastUpdated,
+		ds.LogsEnabled,
+		ds.LongDesc,
+		ds.LongDesc,
+		ds.LongDesc,
+		ds.MaxDNSAnswers,
+		ds.MaxOriginConnections,
+		ds.MaxRequestHeaderBytes,
+		ds.MidHeaderRewrite,
+		ds.MissLat,
+		ds.MissLong,
+		ds.MultiSiteOrigin,
+		ds.OrgServerFQDN,
+		ds.OriginShield,
+		ds.ProfileID,
+		ds.ProfileName,
+		ds.ProfileDesc,
+		ds.Protocol,
+		ds.QStringIgnore,
+		nil,
+		ds.RangeRequestHandling,
+		ds.RegexRemap,
+		ds.Regional,
+		ds.RegionalGeoBlocking,
+		ds.RemapText,
+		nil,
+		ds.RoutingName,
+		ds.ServiceCategory,
+		ds.SigningAlgorithm,
+		ds.RangeSliceBlockSize,
+		ds.SSLKeyVersion,
+		ds.TenantID,
+		ds.Tenant,
+		nil,
+		ds.Topology,
+		ds.TRRequestHeaders,
+		ds.TRResponseHeaders,
+		ds.Type,
+		ds.TypeID,
+		ds.XMLID,
+		"cdn_domain_name")
+
+	mock.ExpectQuery("SELECT ds.active*").WillReturnRows(rows)
+
+	rows2 := sqlmock.NewRows([]string{
+		"ds_name",
+		"type",
+		"pattern",
+		"set_number",
+	})
+	rows2.AddRow(
+		"dsXMLID",
+		"HOST_REGEXP",
+		".*\\.dsXMLID\\..*",
+		0)
+	mock.ExpectQuery("SELECT ds.xml_id as ds_name*").WillReturnRows(rows2)
+
+	if needOriginals[ID][0].Original != nil {
+		t.Errorf("expected original to be initially empty")
+	}
+	sc, userErr, sysErr := getOriginals(ids, db.MustBegin(), needOriginals)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("expected no error, but got userErr: %v, sysErr: %v", userErr, sysErr)
+	}
+	if sc != http.StatusOK {
+		t.Fatalf("expected a 200 status code, but got %d", sc)
+	}
+	if needOriginals[ID][0].Original == nil {
+		t.Fatalf("expected original to be a valid delivery service, but got nothing")
+	}
+	if needOriginals[ID][0].Original.XMLID != "dsXMLID" {
+		t.Fatalf("expected original to have a DS with XMLID 'dsXMLID', but got %s", needOriginals[ID][0].Original.XMLID)
+	}
+}
+
 func TestGetAssignee(t *testing.T) {
 	req := assignmentRequest{
 		AssigneeID: nil,
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/request/validate_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate_test.go
new file mode 100644
index 0000000000..60012cb70b
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/request/validate_test.go
@@ -0,0 +1,278 @@
+package request
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"testing"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/jmoiron/sqlx"
+
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+var ds = tc.DeliveryServiceV5{
+	Active:                   tc.DeliveryServiceActiveState("PRIMED"),
+	AnonymousBlockingEnabled: false,
+	CCRDNSTTL:                util.IntPtr(20),
+	CDNID:                    11,
+	CDNName:                  util.StrPtr("testCDN"),
+	CheckPath:                util.StrPtr("blah"),
+	ConsistentHashRegex:      nil,
+	DeepCachingType:          tc.DeepCachingTypeNever,
+	DisplayName:              "ds",
+	DNSBypassCNAME:           nil,
+	DNSBypassIP:              nil,
+	DNSBypassIP6:             nil,
+	DNSBypassTTL:             nil,
+	DSCP:                     0,
+	EcsEnabled:               false,
+	EdgeHeaderRewrite:        nil,
+	FirstHeaderRewrite:       nil,
+	GeoLimitRedirectURL:      nil,
+	GeoLimit:                 0,
+	GeoLimitCountries:        nil,
+	GeoProvider:              0,
+	GlobalMaxMBPS:            nil,
+	GlobalMaxTPS:             nil,
+	FQPacingRate:             nil,
+	HTTPBypassFQDN:           nil,
+	ID:                       util.IntPtr(1),
+	InfoURL:                  nil,
+	InitialDispersion:        util.IntPtr(1),
+	InnerHeaderRewrite:       nil,
+	IPV6RoutingEnabled:       util.BoolPtr(true),
+	LastHeaderRewrite:        nil,
+	LastUpdated:              time.Now(),
+	LogsEnabled:              true,
+	LongDesc:                 "",
+	MaxDNSAnswers:            util.IntPtr(5),
+	MaxOriginConnections:     util.IntPtr(2),
+	MaxRequestHeaderBytes:    util.IntPtr(0),
+	MidHeaderRewrite:         nil,
+	MissLat:                  util.FloatPtr(0.0),
+	MissLong:                 util.FloatPtr(0.0),
+	MultiSiteOrigin:          false,
+	OrgServerFQDN:            util.StrPtr("http://1.2.3.4"),
+	OriginShield:             nil,
+	ProfileID:                util.IntPtr(99),
+	ProfileName:              util.StrPtr("profile99"),
+	ProfileDesc:              nil,
+	Protocol:                 util.IntPtr(1),
+	QStringIgnore:            nil,
+	RangeRequestHandling:     nil,
+	RegexRemap:               nil,
+	Regional:                 false,
+	RegionalGeoBlocking:      false,
+	RemapText:                nil,
+	RequiredCapabilities:     nil,
+	RoutingName:              "",
+	ServiceCategory:          nil,
+	SigningAlgorithm:         nil,
+	RangeSliceBlockSize:      nil,
+	SSLKeyVersion:            nil,
+	TenantID:                 100,
+	Tenant:                   util.StrPtr("tenant100"),
+	TLSVersions:              nil,
+	Topology:                 nil,
+	TRRequestHeaders:         nil,
+	TRResponseHeaders:        nil,
+	Type:                     util.StrPtr("type101"),
+	TypeID:                   101,
+	XMLID:                    "dsXMLID",
+}
+
+var dsr = tc.DeliveryServiceRequestNullable{
+	AssigneeID:      nil,
+	Assignee:        nil,
+	AuthorID:        nil,
+	Author:          nil,
+	ChangeType:      nil,
+	CreatedAt:       nil,
+	ID:              nil,
+	LastEditedBy:    nil,
+	LastEditedByID:  nil,
+	LastUpdated:     nil,
+	DeliveryService: nil,
+	Status:          util.Ptr(tc.RequestStatusDraft),
+	XMLID:           util.StrPtr("dsXMLID"),
+}
+
+func TestValidateV5(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("opening mock database: %v", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+
+	dsrV5 := dsr.Upgrade().Upgrade().Upgrade()
+	userErr, sysErr := validateV5(dsrV5, db.MustBegin().Tx)
+
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because change type is absent, but got nothing")
+	}
+
+	dsrV5.ChangeType = tc.DSRChangeTypeCreate
+	mock.ExpectBegin()
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because requested is absent for changetype 'change', but got nothing")
+	}
+
+	dsrV5.Requested = &ds
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{
+		"name",
+		"use_in_table",
+	})
+	rows.AddRow("type101", "server")
+	mock.ExpectQuery("SELECT name, use_in_table*").WillReturnRows(rows)
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because use_in_table is not deliveryservice, but got nothing")
+	}
+
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{
+		"name",
+		"use_in_table",
+	})
+	rows.AddRow("type101", "deliveryservice")
+	mock.ExpectQuery("SELECT name, use_in_table*").WillReturnRows(rows)
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("no error expected, but got usererr: %v, sysErr: %v", userErr, sysErr)
+	}
+
+	dsrV5.ChangeType = tc.DSRChangeTypeDelete
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{
+		"name",
+		"use_in_table",
+	})
+	rows.AddRow("type101", "deliveryservice")
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because original is not present for changetype 'delete', but got nothing")
+	}
+
+	dsrV5.Requested = nil
+	dsrV5.Original = &ds
+	mock.ExpectBegin()
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("no error expected, but got usererr: %v, sysErr: %v", userErr, sysErr)
+	}
+
+	dsrV5.Assignee = util.StrPtr("testUser")
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{
+		"id",
+	})
+	rows.AddRow(10)
+	mock.ExpectQuery("SELECT id FROM tm_user*").WillReturnRows(rows)
+	userErr, sysErr = validateV5(dsrV5, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("no error expected, but got usererr: %v, sysErr: %v", userErr, sysErr)
+	}
+}
+
+func TestValidateLegacy(t *testing.T) {
+	mockDB, mock, err := sqlmock.New()
+	if err != nil {
+		t.Fatalf("opening mock database: %v", err)
+	}
+	defer mockDB.Close()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+
+	dsV30 := ds.Downgrade().DowngradeToV31()
+	dsr.DeliveryService = &dsV30
+	// expect error because ChangeType is absent
+	userErr, sysErr := validateLegacy(dsr, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because change type is absent, but got nothing")
+	}
+	dsr.ChangeType = util.StrPtr(string(tc.DSRChangeTypeCreate))
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{
+		"name",
+		"use_in_table",
+	})
+	rows.AddRow("type101", "server")
+	mock.ExpectQuery("SELECT name, use_in_table*").WillReturnRows(rows)
+	userErr, sysErr = validateLegacy(dsr, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Fatalf("expected no error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Fatalf("expected userErr because use_in_table is not deliveryservice, but got nothing")
+	}
+
+	mock.ExpectBegin()
+	rows.AddRow("type101", "deliveryservice")
+	mock.ExpectQuery("SELECT name, use_in_table*").WillReturnRows(rows)
+	userErr, sysErr = validateLegacy(dsr, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("no error expected, but got usererr: %v, sysErr: %v", userErr, sysErr)
+	}
+
+	dsr.ID = util.IntPtr(1)
+	dsr.Status = util.Ptr(tc.RequestStatusSubmitted)
+	mock.ExpectBegin()
+
+	rows2 := sqlmock.NewRows([]string{
+		"status",
+	})
+	rows2.AddRow([]byte("submitted"))
+	mock.ExpectQuery("SELECT status*").WillReturnRows(rows2)
+
+	rows.AddRow("type101", "deliveryservice")
+	mock.ExpectQuery("SELECT name, use_in_table*").WillReturnRows(rows)
+	userErr, sysErr = validateLegacy(dsr, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Fatalf("no error expected, but got usererr: %v, sysErr: %v", userErr, sysErr)
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/safe_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/safe_test.go
new file mode 100644
index 0000000000..9cdea41527
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/safe_test.go
@@ -0,0 +1,70 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"testing"
+
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/jmoiron/sqlx"
+
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestUpdateDSSafe(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	// test with a DS that exists
+	mock.ExpectBegin()
+	dsID := 1
+	dsr := tc.DeliveryServiceSafeUpdateRequest{
+		DisplayName: util.Ptr("displayName"),
+		InfoURL:     util.Ptr("http://blah.com"),
+		LongDesc:    util.Ptr("longdesc"),
+		LongDesc1:   util.Ptr("longdesc1"),
+	}
+	mock.ExpectExec("UPDATE deliveryservice").WithArgs(*dsr.DisplayName, *dsr.InfoURL, *dsr.LongDesc, *dsr.LongDesc1, dsID).WillReturnResult(sqlmock.NewResult(int64(dsID), 1))
+	exists, err := updateDSSafe(db.MustBegin().Tx, dsID, dsr, false)
+	if err != nil {
+		t.Errorf("expected no error, but got: %v", err)
+	}
+	if !exists {
+		t.Errorf("expected DS with id 1 to exist")
+	}
+
+	// test with a DS that doesn't exist
+	mock.ExpectBegin()
+	mock.ExpectExec("UPDATE deliveryservice").WithArgs(*dsr.DisplayName, *dsr.InfoURL, *dsr.LongDesc, *dsr.LongDesc1, 2).WillReturnResult(sqlmock.NewResult(2, 0))
+	exists, err = updateDSSafe(db.MustBegin().Tx, 2, dsr, false)
+	if err != nil {
+		t.Errorf("expected no error, but got: %v", err)
+	}
+	if exists {
+		t.Errorf("expected DS with id 2 to not exist")
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete_test.go
new file mode 100644
index 0000000000..5f08e11c98
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/delete_test.go
@@ -0,0 +1,119 @@
+package servers
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/jmoiron/sqlx"
+
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestCheckLastAvailableEdgeOrOrigin(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	// check DS with no topology or mso
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"available", "available"})
+	rows.AddRow(true, true)
+	mock.ExpectQuery("SELECT").WithArgs(1, 2).WillReturnRows(rows)
+	sc, userErr, sysErr := checkLastAvailableEdgeOrOrigin(1, 2, false, false, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Errorf("expected no system error, but got %v", sysErr)
+	}
+	if userErr == nil {
+		t.Errorf("expected error because removing the given server would result in active DS with no REPORTED/ ONLINE EDGE servers, but got nothing")
+	}
+	if sc != http.StatusConflict {
+		t.Errorf("expected 409 status code, but got %d", sc)
+	}
+
+	// check DS with topology, but no MSO
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{"available", "available"})
+	rows.AddRow(true, true)
+	mock.ExpectQuery("SELECT").WithArgs(1, 2).WillReturnRows(rows)
+	sc, userErr, sysErr = checkLastAvailableEdgeOrOrigin(1, 2, false, true, db.MustBegin().Tx)
+	if userErr != nil || sysErr != nil {
+		t.Errorf("expected no error, but got userErr: %v, sysErr: %v", userErr, sysErr)
+	}
+	if sc != http.StatusOK {
+		t.Errorf("ecpected status code 200, but got %d", sc)
+	}
+
+	// check DS with MSO, but no topology
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{"available", "available"})
+	rows.AddRow(false, true)
+	mock.ExpectQuery("SELECT").WithArgs(1, 2).WillReturnRows(rows)
+	sc, userErr, sysErr = checkLastAvailableEdgeOrOrigin(1, 2, true, false, db.MustBegin().Tx)
+	if sysErr != nil {
+		t.Errorf("expected no system error, but got %v", sysErr)
+	}
+	if userErr == nil {
+		t.Errorf("expected error because removing the given server would result in active DS with no REPORTED/ ONLINE EDGE servers, but got nothing")
+	}
+	if sc != http.StatusConflict {
+		t.Errorf("expected 409 status code, but got %d", sc)
+	}
+}
+
+func TestDeleteDSServer(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	mock.ExpectBegin()
+	rows := sqlmock.NewRows([]string{"server"})
+	mock.ExpectQuery("DELETE").WithArgs(1, 2).WillReturnRows(rows)
+	exists, err := deleteDSServer(db.MustBegin().Tx, 1, 2)
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+	if exists {
+		t.Errorf("expected exists to be false, but got true")
+	}
+
+	rows = sqlmock.NewRows([]string{"server"})
+	rows.AddRow(2)
+	mock.ExpectBegin()
+	mock.ExpectQuery("DELETE").WithArgs(1, 2).WillReturnRows(rows)
+	exists, err = deleteDSServer(db.MustBegin().Tx, 1, 2)
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+	if !exists {
+		t.Errorf("expected exists to be true, but got false")
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
index e5b4463c7b..0dc7c0bd13 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go
@@ -78,7 +78,7 @@ func (dss TODeliveryServiceServer) GetKeys() (map[string]interface{}, bool) {
 }
 
 func (dss *TODeliveryServiceServer) GetAuditName() string {
-	if dss.DeliveryService != nil {
+	if dss.DeliveryService != nil && dss.Server != nil {
 		return strconv.Itoa(*dss.DeliveryService) + "-" + strconv.Itoa(*dss.Server)
 	}
 	return "unknown"
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go
index eed56f1613..d3e077cd59 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/servers/servers_test.go
@@ -21,19 +21,358 @@ package servers
 
 import (
 	"fmt"
+	"net/http"
 	"strconv"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
+func getTestDeliveryServiceServer() TODeliveryServiceServer {
+	return TODeliveryServiceServer{
+		APIInfoImpl: api.APIInfoImpl{},
+		DeliveryServiceServer: tc.DeliveryServiceServer{
+			Server:          nil,
+			DeliveryService: nil,
+			LastUpdated:     nil,
+		},
+		TenantIDs:          nil,
+		DeliveryServiceIDs: nil,
+		ServerIDs:          nil,
+		CDN:                "",
+	}
+}
+
 func TestValidateDSSAssignments(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+	mock.ExpectBegin()
+
+	serverInfo := make([]tc.ServerInfo, 0)
+	s := tc.ServerInfo{
+		Cachegroup:   "cg1",
+		CachegroupID: 20,
+		CDNID:        10,
+		DomainName:   "test",
+		HostName:     "blah",
+		ID:           100,
+		Status:       "ONLINE",
+		Type:         "EDGE",
+	}
+	serverInfo = append(serverInfo, s)
+	s2 := s
+	s2.ID = 200
+	s2.HostName = "blah2"
+	serverInfo = append(serverInfo, s2)
+
+	dsInfo := DSInfo{Active: true,
+		ID:                   1,
+		Name:                 "ds1",
+		Type:                 tc.DSTypeDNS,
+		EdgeHeaderRewrite:    nil,
+		MidHeaderRewrite:     nil,
+		RegexRemap:           nil,
+		SigningAlgorithm:     nil,
+		CacheURL:             nil,
+		MaxOriginConnections: nil,
+		Topology:             util.Ptr("topology1"),
+		CDNID:                util.Ptr(10),
+		UseMultiSiteOrigin:   false}
+
+	// Try to assign non-ORG servers to a topology based DS (with required capabilities)
+	userErr, sysErr, sc := validateDSSAssignments(db.MustBegin().Tx, dsInfo, serverInfo, false)
+
+	if sysErr != nil {
+		t.Errorf("expected no system error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Errorf("expected error while trying to assign EDGE server to a topology, but got nothing")
+	}
+	if sc != http.StatusBadRequest {
+		t.Errorf("expected status code to be 400, but got %d instead", sc)
+	}
+
+	// Try to assign ORG servers without required capabilities to a topology based DS (with required capabilities)
+	for i, _ := range serverInfo {
+		serverInfo[i].Type = "ORG"
+	}
+	mock.ExpectBegin()
+
+	rows := sqlmock.NewRows([]string{"array_agg", "array_agg"})
+	rows.AddRow([]byte("{20,21}"), []byte("{cg1,cg2}"))
+	mock.ExpectQuery("SELECT ARRAY(c.id)*").WithArgs("topology1").WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"required_capabilities"})
+	rows.AddRow("{reqCap1}")
+	mock.ExpectQuery("SELECT required_capabilities*").WithArgs(1).WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"host_name", "capabilities"})
+	rows.AddRow("blah", "{reqCap2, reqCap3}")
+	mock.ExpectQuery("SELECT s.host_name*").WithArgs(pq.StringArray{}).WillReturnRows(rows)
+	userErr, sysErr, sc = validateDSSAssignments(db.MustBegin().Tx, dsInfo, serverInfo, false)
+
+	if sysErr != nil {
+		t.Errorf("expected no system error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Errorf("expected error while trying to assign server without a required capability, but got nothing")
+	}
+	if sc != http.StatusBadRequest {
+		t.Errorf("expected status code to be 400, but got %d instead", sc)
+	}
+
+	// Try to assign ORG servers with required capabilities to a topology based DS (with required capabilities)
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{"array_agg", "array_agg"})
+	rows.AddRow([]byte("{20,21}"), []byte("{cg1,cg2}"))
+	mock.ExpectQuery("SELECT ARRAY(c.id)*").WithArgs("topology1").WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"required_capabilities"})
+	rows.AddRow("{reqCap1}")
+	mock.ExpectQuery("SELECT required_capabilities*").WithArgs(1).WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"host_name", "capabilities"})
+	rows.AddRow("blah", "{reqCap1, reqCap2, reqCap3}")
+	mock.ExpectQuery("SELECT s.host_name*").WithArgs(pq.StringArray{}).WillReturnRows(rows)
+	userErr, sysErr, sc = validateDSSAssignments(db.MustBegin().Tx, dsInfo, serverInfo, false)
+
+	if userErr != nil || sysErr != nil {
+		t.Errorf("expected no errors, but got userErr: %v, sysErr: %v", userErr, sysErr)
+	}
+	if sc != http.StatusOK {
+		t.Errorf("expected status code to be 200, but got %d instead", sc)
+	}
+
+	// Try to assign EDGE servers without required capabilities to a DS (with required capabilities)
+	dsInfo.Topology = nil
+	for i, _ := range serverInfo {
+		serverInfo[i].Type = "EDGE"
+	}
+
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{"required_capabilities"})
+	rows.AddRow("{reqCap1}")
+	mock.ExpectQuery("SELECT required_capabilities*").WithArgs(1).WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"host_name", "capabilities"})
+	rows.AddRow("blah", "{reqCap2, reqCap3}")
+	mock.ExpectQuery("SELECT s.host_name*").WithArgs(pq.StringArray{"blah", "blah2"}).WillReturnRows(rows)
+	userErr, sysErr, sc = validateDSSAssignments(db.MustBegin().Tx, dsInfo, serverInfo, false)
+
+	if sysErr != nil {
+		t.Errorf("expected no system error, but got sysErr: %v", sysErr)
+	}
+	if userErr == nil {
+		t.Errorf("expected error while trying to assign server without a required capability, but got nothing")
+	}
+	if sc != http.StatusBadRequest {
+		t.Errorf("expected status code to be 400, but got %d instead", sc)
+	}
+
+	// Try to assign EDGE servers with required capabilities to a DS (with required capabilities)
+	mock.ExpectBegin()
+	rows = sqlmock.NewRows([]string{"required_capabilities"})
+	rows.AddRow("{reqCap1}")
+	mock.ExpectQuery("SELECT required_capabilities*").WithArgs(1).WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"host_name", "capabilities"})
+	rows.AddRow("blah", "{reqCap1, reqCap2, reqCap3}")
+	mock.ExpectQuery("SELECT s.host_name*").WithArgs(pq.StringArray{"blah", "blah2"}).WillReturnRows(rows)
+	userErr, sysErr, sc = validateDSSAssignments(db.MustBegin().Tx, dsInfo, serverInfo, false)
+
+	if userErr != nil || sysErr != nil {
+		t.Errorf("expected no errors, but got userErr: %v, sysErr: %v", userErr, sysErr)
+	}
+	if sc != http.StatusOK {
+		t.Errorf("expected status code to be 200, but got %d instead", sc)
+	}
+}
+
+func TestHasAvailableEdgesCurrentlyAssigned(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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+	mock.ExpectBegin()
+
+	rows := sqlmock.NewRows([]string{"name"})
+	rows.AddRow("edge1")
+
+	mock.ExpectQuery("SELECT t.name AS name*").WithArgs(1).WillReturnRows(rows)
+	assigned, err := hasAvailableEdgesCurrentlyAssigned(db.MustBegin().Tx, 1)
+	if err != nil {
+		t.Fatalf("expected no error, but got %v", err)
+	}
+	if !assigned {
+		t.Errorf("expected 'hasAvailableEdgesCurrentlyAssigned' to return true, but got false")
+	}
+}
+
+func TestReadDSS(t *testing.T) {
+	//func (dss *TODeliveryServiceServer) readDSS(h http.Header, tx *sqlx.Tx, user *auth.CurrentUser, params map[string]string, intParams map[string]int, dsIDs []int64, serverIDs []int64, useIMS bool) (*tc.DeliveryServiceServerResponse, error, *time.Time)
+	dss := getTestDeliveryServiceServer()
+
+	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()
+
+	db := sqlx.NewDb(mockDB, "sqlmock")
+	defer db.Close()
+
+	rows := sqlmock.NewRows([]string{"id"})
+	rows.AddRow(10)
+	rows.AddRow(20)
+
+	mock.ExpectBegin()
+	mock.ExpectQuery("WITH RECURSIVE*").WithArgs(10).WillReturnRows(rows)
+
+	rows = sqlmock.NewRows([]string{"server", "deliveryservice", "last_updated"})
+	rows.AddRow(1, 2, time.Now())
+
+	mock.ExpectQuery("SELECT*").WithArgs(pq.Int64Array{10, 20}).WillReturnRows(rows)
+	response, err, _ := dss.readDSS(nil, db.MustBegin(), &auth.CurrentUser{PrivLevel: 30, TenantID: 10}, nil, nil, nil, nil, false)
+	if err != nil {
+		t.Fatalf("expected no error, but got: %v", err)
+	}
+	if response == nil {
+		t.Fatalf("expected a valid response, but got nothing")
+	}
+	if len(response.Response) != 1 {
+		t.Fatalf("expected response to have 1 deliveryserviceServer, but got %d", len(response.Response))
+	}
+	if response.Response[0].Server == nil || response.Response[0].DeliveryService == nil {
+		t.Fatalf("expected valid values for server and deliveryservice, but got nil instead. server: %v, deliveryservice: %v", response.Response[0].Server, response.Response[0].DeliveryService)
+	}
+	if *response.Response[0].Server != 1 || *response.Response[0].DeliveryService != 2 {
+		t.Errorf("expected server to be 1 and deliveryservice to be 2, but got server: %d, deliveryservice: %d instead", *response.Response[0].Server, *response.Response[0].DeliveryService)
+	}
+}
+
+func TestValidate(t *testing.T) {
+	dss := getTestDeliveryServiceServer()
+	err := dss.Validate(nil)
+	if err == nil {
+		t.Errorf("expected error about deliveryservice and server not being present, but got nothing")
+	}
+	dss.Server = util.Ptr(1)
+	dss.DeliveryService = util.Ptr(2)
+	err = dss.Validate(nil)
+	if err != nil {
+		t.Errorf("expected no error, but got %v", err)
+	}
+}
+
+func TestSetKeys(t *testing.T) {
+	dss := getTestDeliveryServiceServer()
+	keys := make(map[string]interface{})
+	keys["server"] = 1
+	keys["deliveryservice"] = 2
+	dss.SetKeys(keys)
+	if dss.DeliveryService == nil || dss.Server == nil {
+		t.Fatalf("expected both server and deliveryservice to be not nil")
+	}
+	if *dss.DeliveryService != 2 {
+		t.Errorf("expected deliveryservice key to be 2, but got %d", *dss.DeliveryService)
+	}
+	if *dss.Server != 1 {
+		t.Errorf("expected server key to be 1, but got %d", *dss.Server)
+	}
+}
+
+func TestGetAuditName(t *testing.T) {
+	dss := getTestDeliveryServiceServer()
+
+	auditName := dss.GetAuditName()
+	if auditName != "unknown" {
+		t.Errorf("expected audit name to be 'unknown', but got %s", auditName)
+	}
+
+	dss.DeliveryServiceServer.Server = util.Ptr(1)
+	dss.DeliveryServiceServer.DeliveryService = util.Ptr(2)
+	auditName = dss.GetAuditName()
+	if auditName != "2-1" {
+		t.Errorf("expected audit name to be '2-1', but got %s", auditName)
+	}
+}
+
+func TestGetKeys(t *testing.T) {
+	dss := getTestDeliveryServiceServer()
+	dss.Server = util.Ptr(1)
+	dss.DeliveryService = util.Ptr(2)
+	keys, exists := dss.GetKeys()
+	if keys == nil {
+		t.Fatalf("expected function to return a valid map of keys, but got nothing")
+	}
+	if !exists {
+		t.Fatalf("expected function to return a true boolean for exists, got false")
+	}
+	if serverID, ok := keys["server"]; !ok {
+		t.Fatalf("expected returned keys to have 'server' key, but key wasn't present")
+	} else if serverID.(int) != 1 {
+		t.Errorf("expected serverID to be 1, but got %d", serverID.(int))
+	}
+
+	if dsID, ok := keys["deliveryservice"]; !ok {
+		t.Fatalf("expected returned keys to have 'deliveryservice' key, but key wasn't present")
+	} else if dsID.(int) != 2 {
+		t.Errorf("expected dsID to be 2, but got %d", dsID.(int))
+	}
+
+	// check with nil values for server and deliveryservice
+	dss.DeliveryServiceServer.Server = nil
+	dss.DeliveryServiceServer.DeliveryService = nil
+	keys, exists = dss.GetKeys()
+	if keys == nil {
+		t.Fatalf("expected function to return a valid map of keys, but got nothing")
+	}
+	if exists {
+		t.Fatalf("expected function to return a false boolean for exists, got true")
+	}
+	if dsID, ok := keys["deliveryservice"]; !ok {
+		t.Fatalf("expected returned keys to have 'deliveryservice' key, but key wasn't present")
+	} else if dsID.(int) != 0 {
+		t.Errorf("expected dsID to be 0, but got %d", dsID.(int))
+	}
+	if _, ok := keys["server"]; ok {
+		t.Errorf("'server' key was not expected to be present")
+	}
+	dss.DeliveryServiceServer.DeliveryService = util.Ptr(2)
+	keys, exists = dss.GetKeys()
+	if keys == nil {
+		t.Fatalf("expected function to return a valid map of keys, but got nothing")
+	}
+	if exists {
+		t.Fatalf("expected function to return a false boolean for exists, got true")
+	}
+	if serverID, ok := keys["server"]; !ok {
+		t.Fatalf("expected returned keys to have 'server' key, but key wasn't present")
+	} else if serverID.(int) != 0 {
+		t.Errorf("expected serverID to be 0, but got %d", serverID.(int))
+	}
+}
+
+func TestValidateDSS(t *testing.T) {
 	expected := `server and delivery service CDNs do not match`
 	cdnID := 1
 	ds := DSInfo{