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 <[email protected]>
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
 }


Reply via email to