You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2023/04/19 17:13:59 UTC

[trafficcontrol] branch master updated: Fixes service_category apis to respond with RFC3339 date/time Format (#7408)

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

ocket8888 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 664c2d5748 Fixes service_category apis to respond with RFC3339 date/time Format (#7408)
664c2d5748 is described below

commit 664c2d57483bc426cf59b067b76c38820dd8fc8e
Author: Jagan Parthiban <33...@users.noreply.github.com>
AuthorDate: Wed Apr 19 22:43:53 2023 +0530

    Fixes service_category apis to respond with RFC3339 date/time Format (#7408)
    
    * This fixes service_category apis to respond with RFC3339 date/time strings. Issue: https://github.com/apache/trafficcontrol/issues/5911
    
    * This fixes service_category apis to respond with RFC3339 date/time strings.
    
    * This fixes service_category apis to respond with RFC3339 date/time strings. This commit removed new struct TimeRFC3339 and uses time.Time instead
    
    * This commit removed new struct TimeRFC3339 and uses time.Time instead
    
    * https://github.com/apache/trafficcontrol/issues/7413
    
    Removing dependency of service catergory on generic cruder for V5 version
    
    * Corrected Error messages statement
    
    * Added Doc content for RFC 3339 Date/Time Format and updated service category docs.
    
    * Updated Http Get operations for Service Category V5 to handle If-Modified-Since request HTTP header
    
    * Added Unit Test Cases for V5 service_category functions
    
    * Updated comments for the V5 service category functions.
    
    * Updated CHANGELOG.md
    
    * Added Unit test for db_helpers.go
    
    * Addressed Code review from the PR.
    
    * Addressed Code review from the PR.
    
    * Addressed Code review from the PR.
    
    * Addressed Code review from the PR.
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v5/service_categories.rst          |  12 +-
 docs/source/api/v5/service_categories_name.rst     |   8 +-
 lib/go-tc/service_category.go                      |  34 +++
 .../testing/api/v5/servicecategories_test.go       |  20 +-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  15 +
 .../dbhelpers/db_helpers_test.go                   |  51 ++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   8 +-
 .../servicecategory/servicecategories.go           | 319 +++++++++++++++++++++
 .../servicecategory/servicecategories_test.go      | 146 ++++++++++
 traffic_ops/v5-client/serviceCategory.go           |  11 +-
 12 files changed, 596 insertions(+), 31 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b997b88acf..aa564ef54c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 - [#7441](https://github.com/apache/trafficcontrol/pull/7441) *Traffic Ops* Fixed the invalidation jobs endpoint to respect CDN locks.
+- [#7413](https://github.com/apache/trafficcontrol/issues/7413) *Traffic Ops* Fixes service_category apis to respond with RFC3339 date/time Format
 - [#7414](https://github.com/apache/trafficcontrol/pull/7414) * Traffic Portal* Fixed DSR difference for DS required capability.
 - [#7130](https://github.com/apache/trafficcontrol/issues/7130) *Traffic Ops* Fixes service_categories response to POST API.
 - [#7340](https://github.com/apache/trafficcontrol/pull/7340) *Traffic Router* Fixed TR logging for the `cqhv` field when absent.
diff --git a/docs/source/api/v5/service_categories.rst b/docs/source/api/v5/service_categories.rst
index 7f6aa563b3..28408acdb0 100644
--- a/docs/source/api/v5/service_categories.rst
+++ b/docs/source/api/v5/service_categories.rst
@@ -64,7 +64,7 @@ Request Structure
 Response Structure
 ------------------
 :name:        This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
 
 .. code-block:: http
 	:caption: Response Example
@@ -78,13 +78,13 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Whole-Content-Sha512: Yzr6TfhxgpZ3pbbrr4TRG4wC3PlnHDDzgs2igtz/1ppLSy2MzugqaGW4y5yzwzl5T3+7q6HWej7GQZt1XIVeZQ==
 	X-Server-Name: traffic_ops_golang/
-	Date: Wed, 11 Mar 2020 20:02:47 GMT
+	Date: Wed, 29 Mar 2023 15:56:34 GMT
 	Content-Length: 102
 
 	{
 		"response": [
 			{
-				"lastUpdated": "2020-03-04 15:46:20-07",
+				"lastUpdated": "2023-03-29T19:43:17.557642+05:30",
 				"name": "SERVICE_CATEGORY_NAME"
 			}
 		]
@@ -121,7 +121,7 @@ Request Structure
 Response Structure
 ------------------
 :name:        This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
 
 .. code-block:: http
 	:caption: Response Example
@@ -135,7 +135,7 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Whole-Content-Sha512: +pJm4c3O+JTaSXNt+LP+u240Ba/SsvSSDOQ4rDc6hcyZ0FIL+iY/WWrMHhpLulRGKGY88bM4YPCMaxGn3FZ9yQ==
 	X-Server-Name: traffic_ops_golang/
-	Date: Wed, 11 Mar 2020 20:12:20 GMT
+	Date: Wed, 29 Mar 2023 15:58:37 GMT
 	Content-Length: 154
 
 	{
@@ -146,7 +146,7 @@ Response Structure
 			}
 		],
 		"response": {
-			"lastUpdated": "2020-03-11 14:12:20-06",
+			"lastUpdated": "2023-03-29T21:28:37.884457+05:30",
 			"name": "SERVICE_CATEGORY_NAME"
 		}
 	}
diff --git a/docs/source/api/v5/service_categories_name.rst b/docs/source/api/v5/service_categories_name.rst
index ba16efd3d2..d876ae8860 100644
--- a/docs/source/api/v5/service_categories_name.rst
+++ b/docs/source/api/v5/service_categories_name.rst
@@ -58,7 +58,7 @@ Request Structure
 Response Structure
 ------------------
 :name:        This :term:`Service Category`'s name
-:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :ref:`non-rfc-datetime`
+:lastUpdated: The date and time at which this :term:`Service Category` was last modified, in :rfc:`3339`
 
 .. code-block:: http
 	:caption: Response Example
@@ -72,7 +72,7 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 18 Nov 2019 17:40:54 GMT; Max-Age=3600; HttpOnly
 	Whole-Content-Sha512: +pJm4c3O+JTaSXNt+LP+u240Ba/SsvSSDOQ4rDc6hcyZ0FIL+iY/WWrMHhpLulRGKGY88bM4YPCMaxGn3FZ9yQ==
 	X-Server-Name: traffic_ops_golang/
-	Date: Wed, 11 Mar 2020 20:12:20 GMT
+	Date: Wed, 29 Mar 2023 15:58:37 GMT
 	Content-Length: 189
 
 	{
@@ -83,7 +83,7 @@ Response Structure
 			}
 		],
 		"response": {
-			"lastUpdated": "2020-03-11 14:12:20-06",
+			"lastUpdated": "2023-03-29T21:28:37.884457+05:30",
 			"name": "New Name"
 		}
 	}
@@ -136,7 +136,7 @@ Response Structure
 	Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 17 Aug 2020 16:13:31 GMT; Max-Age=3600; HttpOnly
 	Whole-Content-Sha512: yErJobzG9IA0khvqZQK+Yi7X4pFVvOqxn6PjrdzN5DnKVm/K8Kka3REul1XmKJnMXVRY8RayoEVGDm16mBFe4Q==
 	X-Server-Name: traffic_ops_golang/
-	Date: Mon, 17 Aug 2020 15:13:31 GMT
+	Date: Wed, 29 Mar 2023 15:58:37 GMT
 	Content-Length: 103
 
 	{
diff --git a/lib/go-tc/service_category.go b/lib/go-tc/service_category.go
index 19c2d45ecf..69d0716d2d 100644
--- a/lib/go-tc/service_category.go
+++ b/lib/go-tc/service_category.go
@@ -19,6 +19,8 @@ package tc
  * under the License.
  */
 
+import "time"
+
 // ServiceCategoriesResponse is a list of Service Categories as a response.
 type ServiceCategoriesResponse struct {
 	Response []ServiceCategory `json:"response"`
@@ -37,3 +39,35 @@ type ServiceCategory struct {
 	LastUpdated TimeNoMod `json:"lastUpdated" db:"last_updated"`
 	Name        string    `json:"name" db:"name"`
 }
+
+// ServiceCategoriesResponseV50 is a list of Service Categories as a response.
+type ServiceCategoriesResponseV50 struct {
+	Response []ServiceCategoryV50 `json:"response"`
+	Alerts
+}
+
+// ServiceCategoryResponseV50 is a single Service Category response for Update and Create to
+// depict what changed.
+type ServiceCategoryResponseV50 struct {
+	Response ServiceCategoryV50 `json:"response"`
+	Alerts
+}
+
+// ServiceCategoryV50 holds the name and last updated time stamp.
+type ServiceCategoryV50 struct {
+	LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
+	Name        string    `json:"name" db:"name"`
+}
+
+// ServiceCategoriesResponseV5 is the type of a response from the service_categories
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of ServiceCategoriesResponseV5x APIv5.
+type ServiceCategoriesResponseV5 = ServiceCategoriesResponseV50
+
+// ServiceCategoryResponseV5 is the type of a response from the service_categories
+// Traffic Ops endpoint.
+// It always points to the type for the latest minor version of ServiceCategoryResponseV5x APIv5.
+type ServiceCategoryResponseV5 = ServiceCategoryResponseV50
+
+// ServiceCategoryV5 always points to the type for the latest minor version of serviceCategoryV5x APIv5.
+type ServiceCategoryV5 = ServiceCategoryV50
diff --git a/traffic_ops/testing/api/v5/servicecategories_test.go b/traffic_ops/testing/api/v5/servicecategories_test.go
index 9870ff9d08..10921f8c93 100644
--- a/traffic_ops/testing/api/v5/servicecategories_test.go
+++ b/traffic_ops/testing/api/v5/servicecategories_test.go
@@ -37,7 +37,7 @@ func TestServiceCategories(t *testing.T) {
 		currentTimeRFC := currentTime.Format(time.RFC1123)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServiceCategory]{
+		methodTests := utils.TestCase[client.Session, client.RequestOptions, tc.ServiceCategoryV5]{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
 					ClientSession: TOSession,
@@ -103,12 +103,12 @@ func TestServiceCategories(t *testing.T) {
 			"POST": {
 				"BAD REQUEST when ALREADY EXISTS": {
 					ClientSession: TOSession,
-					RequestBody:   tc.ServiceCategory{Name: "serviceCategory1"},
+					RequestBody:   tc.ServiceCategoryV5{Name: "serviceCategory1"},
 					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 				"BAD REQUEST when NAME FIELD is BLANK": {
 					ClientSession: TOSession,
-					RequestBody:   tc.ServiceCategory{Name: ""},
+					RequestBody:   tc.ServiceCategoryV5{Name: ""},
 					Expectations:  utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)),
 				},
 			},
@@ -116,7 +116,7 @@ func TestServiceCategories(t *testing.T) {
 				"OK when VALID request": {
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"name": {"barServiceCategory2"}}},
-					RequestBody:   tc.ServiceCategory{Name: "newName"},
+					RequestBody:   tc.ServiceCategoryV5{Name: "newName"},
 					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
 						validateServiceCategoriesUpdateCreateFields("newName", map[string]interface{}{"Name": "newName"})),
 				},
@@ -126,12 +126,12 @@ func TestServiceCategories(t *testing.T) {
 						QueryParameters: url.Values{"name": {"serviceCategory1"}},
 						Header:          http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}},
 					},
-					RequestBody:  tc.ServiceCategory{Name: "newName"},
+					RequestBody:  tc.ServiceCategoryV5{Name: "newName"},
 					Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)),
 				},
 				"PRECONDITION FAILED when updating with IFMATCH ETAG Header": {
 					ClientSession: TOSession,
-					RequestBody:   tc.ServiceCategory{Name: "newName"},
+					RequestBody:   tc.ServiceCategoryV5{Name: "newName"},
 					RequestOpts: client.RequestOptions{
 						QueryParameters: url.Values{"name": {"serviceCategory1"}},
 						Header:          http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}},
@@ -196,7 +196,7 @@ func TestServiceCategories(t *testing.T) {
 func validateServiceCategoriesFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
-		serviceCategoryResp := resp.([]tc.ServiceCategory)
+		serviceCategoryResp := resp.([]tc.ServiceCategoryV5)
 		for field, expected := range expectedResp {
 			for _, serviceCategory := range serviceCategoryResp {
 				switch field {
@@ -223,7 +223,7 @@ func validateServiceCategoriesUpdateCreateFields(name string, expectedResp map[s
 
 func validateServiceCategoriesPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
-		paginationResp := resp.([]tc.ServiceCategory)
+		paginationResp := resp.([]tc.ServiceCategoryV5)
 
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "id")
@@ -247,7 +247,7 @@ func validateServiceCategoriesSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
 		var serviceCategoryNames []string
-		serviceCategoryResp := resp.([]tc.ServiceCategory)
+		serviceCategoryResp := resp.([]tc.ServiceCategoryV5)
 		for _, serviceCategory := range serviceCategoryResp {
 			serviceCategoryNames = append(serviceCategoryNames, serviceCategory.Name)
 		}
@@ -258,7 +258,7 @@ func validateServiceCategoriesSort() utils.CkReqFunc {
 func validateServiceCategoriesDescSort() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, alerts tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected Service Categories response to not be nil.")
-		serviceCategoriesDescResp := resp.([]tc.ServiceCategory)
+		serviceCategoriesDescResp := resp.([]tc.ServiceCategoryV5)
 		var descSortedList []string
 		var ascSortedList []string
 		assert.RequireGreaterOrEqual(t, len(serviceCategoriesDescResp), 2, "Need at least 2 Service Categories in Traffic Ops to test desc sort, found: %d", len(serviceCategoriesDescResp))
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go b/traffic_ops/testing/api/v5/traffic_control_test.go
index bb747942e6..b6673951c8 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -48,7 +48,7 @@ type TrafficControl struct {
 	Servers                                           []tc.ServerV4                           `json:"servers"`
 	ServerServerCapabilities                          []tc.ServerServerCapability             `json:"serverServerCapabilities"`
 	ServerCapabilities                                []tc.ServerCapabilityV41                `json:"serverCapabilities"`
-	ServiceCategories                                 []tc.ServiceCategory                    `json:"serviceCategories"`
+	ServiceCategories                                 []tc.ServiceCategoryV5                  `json:"serviceCategories"`
 	Statuses                                          []tc.StatusNullable                     `json:"statuses"`
 	StaticDNSEntries                                  []tc.StaticDNSEntry                     `json:"staticdnsentries"`
 	StatsSummaries                                    []tc.StatsSummary                       `json:"statsSummaries"`
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 39f6487efa..1f0ec7f3a3 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2187,3 +2187,18 @@ func GetSCInfo(tx *sql.Tx, name string) (bool, error) {
 	}
 	return true, nil
 }
+
+// ServiceCategoryExists confirms whether the service category exists, and an error (if one occurs).
+func ServiceCategoryExists(tx *sql.Tx, name string) (bool, error) {
+	var count int
+	if err := tx.QueryRow("SELECT count(name) FROM service_category AS sc WHERE sc.name=$1", name).Scan(&count); err != nil {
+		return false, fmt.Errorf("error getting service category info: %w", err)
+	}
+	if count == 0 {
+		return false, nil
+	}
+	if count != 1 {
+		return false, fmt.Errorf("getting service category info - expected row count: 1, actual: %d", count)
+	}
+	return true, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
index 282311caca..7409dc0aa9 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers_test.go
@@ -460,3 +460,54 @@ func TestGetSCInfo(t *testing.T) {
 		})
 	}
 }
+
+func TestServiceCategoryExists(t *testing.T) {
+	var testCases = []struct {
+		description   string
+		name          string
+		expectedError error
+		exists        bool
+	}{
+		{
+			description:   "Success: Get valid Service Category",
+			name:          "testServiceCategory1",
+			expectedError: nil,
+			exists:        true,
+		},
+		{
+			description:   "Failure: Service Category not in DB",
+			name:          "testServiceCategory2",
+			expectedError: sql.ErrNoRows,
+			exists:        false,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.description, func(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{"count"})
+			if testCase.exists {
+				rows = rows.AddRow(1)
+			}
+			mock.ExpectQuery("SELECT").WillReturnRows(rows)
+			mock.ExpectCommit()
+
+			scExists, err := ServiceCategoryExists(db.MustBegin().Tx, testCase.name)
+			if testCase.exists != scExists {
+				t.Errorf("Expected return exists: %t, actual %t", testCase.exists, scExists)
+			}
+
+			if !errors.Is(err, testCase.expectedError) {
+				t.Errorf("ServiceCategoryExists expected: %s, actual: %s", testCase.expectedError, err)
+			}
+		})
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 725456bf9e..c5fbb8ba47 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -424,10 +424,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `deliveryservices/{dsid}/regexes/{regexid}?$`, Handler: deliveryservicesregexes.Delete, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"DELIVERY-SERVICE:UPDATE", "DELIVERY-SERVICE:READ", "TYPE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 424673166331},
 
 		//ServiceCategories
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `service_categories/?$`, Handler: api.ReadHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40851815431},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `service_categories/{name}/?$`, Handler: servicecategory.Update, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:UPDATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4063691411},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `service_categories/?$`, Handler: api.CreateHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:CREATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4537138011},
-		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `service_categories/{name}$`, Handler: api.DeleteHandler(&servicecategory.TOServiceCategory{}), RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:DELETE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 43253822381},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `service_categories/?$`, Handler: servicecategory.Get, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40851815431},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPut, Path: `service_categories/{name}/?$`, Handler: servicecategory.UpdateServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:UPDATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4063691411},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodPost, Path: `service_categories/?$`, Handler: servicecategory.CreateServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:CREATE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 4537138011},
+		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodDelete, Path: `service_categories/{name}$`, Handler: servicecategory.DeleteServiceCategory, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVICE-CATEGORY:DELETE", "SERVICE-CATEGORY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 43253822381},
 
 		//StaticDNSEntries
 		{Version: api.Version{Major: 5, Minor: 0}, Method: http.MethodGet, Path: `staticdnsentries/?$`, Handler: api.ReadHandler(&staticdnsentry.TOStaticDNSEntry{}), RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"STATIC-DN:READ", "CACHE-GROUP:READ", "DELIVERY-SERVICE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 42893947731},
diff --git a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
index 8201de1fd5..2559b7e881 100644
--- a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
+++ b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories.go
@@ -23,15 +23,20 @@ import (
 	"database/sql"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"time"
 
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
 	"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/dbhelpers"
+
 	validation "github.com/go-ozzo/ozzo-validation"
+	"github.com/jmoiron/sqlx"
 )
 
 type TOServiceCategory struct {
@@ -187,3 +192,317 @@ WHERE name=$2 RETURNING last_updated`
 func deleteQuery() string {
 	return `DELETE FROM service_category WHERE name=:name`
 }
+
+// Get [Version : V5] function Process the *http.Request and writes the response. It uses GetServiceCategory function.
+func Get(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	code := http.StatusOK
+	useIMS := false
+	config, e := api.GetConfig(r.Context())
+	if e == nil && config != nil {
+		useIMS = config.UseIMS
+	} else {
+		log.Warnf("Couldn't get config %v", e)
+	}
+
+	var maxTime time.Time
+	var usrErr error
+	var syErr error
+
+	var scList []tc.ServiceCategoryV5
+
+	tx := inf.Tx
+
+	scList, maxTime, code, usrErr, syErr = GetServiceCategory(tx, inf.Params, useIMS, r.Header)
+	if code == http.StatusNotModified {
+		w.WriteHeader(code)
+		api.WriteResp(w, r, []tc.ServiceCategoryV5{})
+		return
+	}
+
+	if code == http.StatusBadRequest {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, usrErr, nil)
+		return
+	}
+
+	if sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, syErr)
+		return
+	}
+
+	if maxTime != (time.Time{}) && api.SetLastModifiedHeader(r, useIMS) {
+		api.AddLastModifiedHdr(w, maxTime)
+	}
+
+	api.WriteResp(w, r, scList)
+}
+
+// GetServiceCategory [Version : V5] receives transactions from Get function and returns service_categories list.
+func GetServiceCategory(tx *sqlx.Tx, params map[string]string, useIMS bool, header http.Header) ([]tc.ServiceCategoryV5, time.Time, int, error, error) {
+	var runSecond bool
+	var maxTime time.Time
+	scList := []tc.ServiceCategoryV5{}
+
+	selectQuery := `SELECT name, last_updated FROM service_category as sc`
+
+	// Query Parameters to Database Query column mappings
+	queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+		"name": {Column: "sc.name", Checker: nil},
+	}
+	if _, ok := params["orderby"]; !ok {
+		params["orderby"] = "name"
+	}
+	where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToQueryCols)
+	if len(errs) > 0 {
+		return nil, time.Time{}, http.StatusBadRequest, util.JoinErrs(errs), nil
+	}
+
+	if useIMS {
+		runSecond, maxTime = TryIfModifiedSinceQuery(header, tx, where, queryValues)
+		if !runSecond {
+			log.Debugln("IMS HIT")
+			return scList, maxTime, http.StatusNotModified, nil, nil
+		}
+		log.Debugln("IMS MISS")
+	} else {
+		log.Debugln("Non IMS request")
+	}
+	query := selectQuery + where + orderBy + pagination
+	rows, err := tx.NamedQuery(query, queryValues)
+	if err != nil {
+		return nil, time.Time{}, http.StatusInternalServerError, nil, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		sc := tc.ServiceCategoryV5{}
+		if err = rows.Scan(&sc.Name, &sc.LastUpdated); err != nil {
+			return nil, time.Time{}, http.StatusInternalServerError, nil, err
+		}
+		scList = append(scList, sc)
+	}
+
+	return scList, maxTime, http.StatusOK, nil, nil
+}
+
+// TryIfModifiedSinceQuery [Version : V5] function receives transactions and header from GetServiceCategory function and returns bool value if status is not modified.
+func TryIfModifiedSinceQuery(header http.Header, tx *sqlx.Tx, where string, queryValues map[string]interface{}) (bool, time.Time) {
+	var max time.Time
+	var imsDate time.Time
+	var ok bool
+	imsDateHeader := []string{}
+	runSecond := true
+	dontRunSecond := false
+
+	if header == nil {
+		return runSecond, max
+	}
+
+	imsDateHeader = header[rfc.IfModifiedSince]
+	if len(imsDateHeader) == 0 {
+		return runSecond, max
+	}
+
+	if imsDate, ok = rfc.ParseHTTPDate(imsDateHeader[0]); !ok {
+		log.Warnf("IMS request header date '%s' not parsable", imsDateHeader[0])
+		return runSecond, max
+	}
+
+	imsQuery := `SELECT max(last_updated) as t from service_category sc`
+	query := imsQuery + where
+	rows, err := tx.NamedQuery(query, queryValues)
+
+	if errors.Is(err, sql.ErrNoRows) {
+		return dontRunSecond, max
+	}
+
+	if err != nil {
+		log.Warnf("Couldn't get the max last updated time: %v", err)
+		return runSecond, max
+	}
+
+	defer rows.Close()
+	// This should only ever contain one row
+	if rows.Next() {
+		v := time.Time{}
+		if err = rows.Scan(&v); err != nil {
+			log.Warnf("Failed to parse the max time stamp into a struct %v", err)
+			return runSecond, max
+		}
+
+		max = v
+		// The request IMS time is later than the max of (lastUpdated, deleted_time)
+		if imsDate.After(v) {
+			return dontRunSecond, max
+		}
+	}
+	return runSecond, max
+}
+
+// CreateServiceCategory [Version : V5] function creates the service category with the passed name.
+func CreateServiceCategory(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+	tx := inf.Tx.Tx
+
+	sc, readValErr := readAndValidateJsonStruct(r)
+	if readValErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+		return
+	}
+
+	// check if service category already exists
+	var exists bool
+	err := tx.QueryRow(`SELECT EXISTS(SELECT * from service_category where name = $1)`, sc.Name).Scan(&exists)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if service category with name %s exists", err, sc.Name))
+		return
+	}
+	if exists {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("service category name '%s' already exists.", sc.Name), nil)
+		return
+	}
+
+	// create service category
+	query := `INSERT INTO service_category (name) VALUES ($1) RETURNING name, last_updated`
+	err = tx.QueryRow(query, sc.Name).Scan(&sc.Name, &sc.LastUpdated)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating service category with name: %s", err, sc.Name), nil)
+			return
+		}
+		usrErr, sysErr, code := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, code, usrErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "service category was created.")
+	w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/service_category?name=%s", inf.Version.Major, inf.Version.Minor, sc.Name))
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, sc)
+	return
+}
+
+// UpdateServiceCategory [Version : V5] function updates the name of the service category passed.
+func UpdateServiceCategory(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	tx := inf.Tx.Tx
+	sc, readValErr := readAndValidateJsonStruct(r)
+	if readValErr != nil {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+		return
+	}
+
+	requestedName := inf.Params["name"]
+	// check if the entity was already updated
+	userErr, sysErr, errCode = api.CheckIfUnModifiedByName(r.Header, inf.Tx, requestedName, "service_category")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+
+	//update name of a service category
+	query := `UPDATE service_category sc SET
+		name = $1
+	WHERE sc.name = $2
+	RETURNING sc.name, sc.last_updated`
+
+	err := tx.QueryRow(query, sc.Name, requestedName).Scan(&sc.Name, &sc.LastUpdated)
+	if err != nil {
+		if errors.Is(err, sql.ErrNoRows) {
+			api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("service category with name: %s not found", requestedName), nil)
+			return
+		}
+		usrErr, sysErr, code := api.ParseDBError(err)
+		api.HandleErr(w, r, tx, code, usrErr, sysErr)
+		return
+	}
+	alerts := tc.CreateAlerts(tc.SuccessLevel, "service category was updated")
+	api.WriteAlertsObj(w, r, http.StatusOK, alerts, sc)
+	return
+}
+
+// DeleteServiceCategory [Version : V5] function deletes the service category passed.
+func DeleteServiceCategory(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	tx := inf.Tx.Tx
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	name := inf.Params["name"]
+	exists, err := dbhelpers.ServiceCategoryExists(tx, name)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	if !exists {
+		api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("no service category exists for name: %s", name), nil)
+		return
+
+	}
+
+	assignedDeliveryService := 0
+	if err := inf.Tx.Get(&assignedDeliveryService, "SELECT count(service_category) FROM deliveryservice d WHERE d.service_category=$1", name); err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("service category delete, counting assigned Delivery Service(s): %w", err))
+		return
+	} else if assignedDeliveryService != 0 {
+		api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("can not delete a service category with %d assigned Delivery Service(s)", assignedDeliveryService), nil)
+		return
+	}
+
+	res, err := tx.Exec("DELETE FROM service_category AS sc WHERE sc.name=$1", name)
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+	rowsAffected, err := res.RowsAffected()
+	if err != nil {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("determining rows affected for delete service_category: %w", err))
+		return
+	}
+	if rowsAffected == 0 {
+		api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("no rows deleted for service_category"))
+		return
+	}
+
+	alertMessage := fmt.Sprintf("%s was deleted.", name)
+	alerts := tc.CreateAlerts(tc.SuccessLevel, alertMessage)
+	api.WriteAlerts(w, r, http.StatusOK, alerts)
+	return
+}
+
+func readAndValidateJsonStruct(r *http.Request) (tc.ServiceCategoryV5, error) {
+	var sc tc.ServiceCategoryV5
+	if err := json.NewDecoder(r.Body).Decode(&sc); err != nil {
+		userErr := fmt.Errorf("error decoding POST request body into ServiceCategoryV5 struct %w", err)
+		return sc, userErr
+	}
+
+	// validate JSON body
+	rule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters")
+	errs := tovalidate.ToErrors(validation.Errors{
+		"name": validation.Validate(sc.Name, validation.Required, rule),
+	})
+	if len(errs) > 0 {
+		userErr := util.JoinErrs(errs)
+		return sc, userErr
+	}
+	return sc, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go
new file mode 100644
index 0000000000..889457be2f
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/servicecategory/servicecategories_test.go
@@ -0,0 +1,146 @@
+package servicecategory
+
+/*
+ * 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"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-rfc"
+
+	"github.com/jmoiron/sqlx"
+	"gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func TestTryIfModifiedSinceQuery(t *testing.T) {
+
+	type testStruct struct {
+		ifModifiedSince  time.Time
+		setHeader        bool
+		setImsDateHeader bool
+		expected         bool
+	}
+
+	var testData = []testStruct{
+
+		// When header is not set, runSecond must be true
+		{time.Time{}, false, false, true},
+
+		// When header set but header[If-Modified-Since] is not set, runSecond must be true
+		{time.Time{}, true, false, true},
+
+		// When header set and header[If-Modified-Since] is set, but incorrect time is given then runSecond must be true
+		{time.Time{}, true, true, true},
+
+		// When header set and header[If-Modified-Since] is set with correct time, and If-Modified_since < Max(last_Updated) then runSecond must be false
+		{time.Now().AddDate(0, 00, 01), true, true, false},
+
+		// When header set and header[If-Modified-Since] is set with correct time, and If-Modified_since > Max(last_Updated) then runSecond must be true
+		{time.Now().AddDate(0, 00, -01), true, true, true},
+	}
+
+	var header http.Header
+	lastUpdated := time.Now()
+	for i, _ := range testData {
+
+		mockDB, mock, err := sqlmock.New()
+		if err != nil {
+			t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+		}
+		defer mockDB.Close()
+
+		db := sqlx.NewDb(mockDB, "sqlmock")
+		defer db.Close()
+
+		if testData[i].setHeader {
+			header = make(http.Header)
+		}
+
+		if testData[i].setImsDateHeader {
+			timeValue := testData[i].ifModifiedSince.Format("Mon, 02 Jan 2006 15:04:05 MST")
+			header.Set(rfc.IfModifiedSince, timeValue)
+		}
+
+		mock.ExpectBegin()
+		rows := sqlmock.NewRows([]string{"t"})
+		rows.AddRow(lastUpdated)
+		mock.ExpectQuery("SELECT").WithArgs().WillReturnRows(rows)
+
+		where := ""
+		queryValues := map[string]interface{}{}
+
+		runSecond, _ := TryIfModifiedSinceQuery(header, db.MustBegin(), where, queryValues)
+
+		if testData[i].expected != runSecond {
+			t.Errorf("Expected runSecond result doesn't match, got: %t; expected: %t", runSecond, testData[i].expected)
+		}
+
+	}
+
+}
+
+func TestGetServiceCategory(t *testing.T) {
+
+	type testStruct struct {
+		useIms   bool
+		expected int
+	}
+
+	var testData = []testStruct{
+		// When useIMS is set to false in system Config
+		{false, 200},
+		// When useIMS is set to true in system Config
+		{true, 200},
+	}
+
+	var header http.Header
+	lastUpdated := time.Now()
+	params := map[string]string{}
+
+	for i, _ := range testData {
+		mockDB, mock, err := sqlmock.New()
+		if err != nil {
+			t.Fatalf("an error '%v' was not expected when opening a stub database connection", err)
+		}
+		defer mockDB.Close()
+
+		db := sqlx.NewDb(mockDB, "sqlmock")
+		defer db.Close()
+
+		header = make(http.Header)
+		ifModifiedSince := time.Now().AddDate(0, 00, 01)
+		timeValue := ifModifiedSince.Format("Mon, 02 Jan 2006 15:04:05 MST")
+		header.Set(rfc.IfModifiedSince, timeValue)
+
+		mock.ExpectBegin()
+		rows := sqlmock.NewRows([]string{"name", "last_updated"})
+		rows.AddRow("testObj1", lastUpdated.AddDate(0, 0, -5))
+		mock.ExpectQuery("SELECT name, last_updated FROM service_category").WithArgs().WillReturnRows(rows)
+
+		_, _, code, _, _ := GetServiceCategory(db.MustBegin(), params, testData[i].useIms, header)
+
+		if testData[i].expected != code {
+			t.Errorf("Expected status code result doesn't match, got: %v; expected: %v", code, testData[i].expected)
+		}
+
+	}
+
+}
diff --git a/traffic_ops/v5-client/serviceCategory.go b/traffic_ops/v5-client/serviceCategory.go
index 31cb964040..1bd32f295e 100644
--- a/traffic_ops/v5-client/serviceCategory.go
+++ b/traffic_ops/v5-client/serviceCategory.go
@@ -28,15 +28,14 @@ import (
 const apiServiceCategories = "/service_categories"
 
 // CreateServiceCategory creates the given Service Category.
-func (to *Session) CreateServiceCategory(serviceCategory tc.ServiceCategory, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateServiceCategory(serviceCategory tc.ServiceCategoryV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	var alerts tc.Alerts
 	reqInf, err := to.post(apiServiceCategories, opts, serviceCategory, &alerts)
 	return alerts, reqInf, err
 }
 
-// UpdateServiceCategory replaces the Service Category with the given Name with
-// the one provided.
-func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.ServiceCategory, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
+// UpdateServiceCategory replaces the Service Category with the given Name with the one provided.
+func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.ServiceCategoryV5, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
 	route := fmt.Sprintf("%s/%s", apiServiceCategories, url.PathEscape(name))
 	var alerts tc.Alerts
 	reqInf, err := to.put(route, opts, serviceCategory, &alerts)
@@ -44,8 +43,8 @@ func (to *Session) UpdateServiceCategory(name string, serviceCategory tc.Service
 }
 
 // GetServiceCategories retrieves Service Categories from Traffic Ops.
-func (to *Session) GetServiceCategories(opts RequestOptions) (tc.ServiceCategoriesResponse, toclientlib.ReqInf, error) {
-	var data tc.ServiceCategoriesResponse
+func (to *Session) GetServiceCategories(opts RequestOptions) (tc.ServiceCategoriesResponseV5, toclientlib.ReqInf, error) {
+	var data tc.ServiceCategoriesResponseV5
 	reqInf, err := to.get(apiServiceCategories, opts, &data)
 	return data, reqInf, err
 }