| package v4 |
| |
| /* |
| |
| Licensed 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 ( |
| "encoding/json" |
| "net/http" |
| "net/url" |
| "strconv" |
| "testing" |
| "time" |
| |
| "github.com/apache/trafficcontrol/lib/go-rfc" |
| "github.com/apache/trafficcontrol/lib/go-tc" |
| "github.com/apache/trafficcontrol/traffic_ops/testing/api/assert" |
| "github.com/apache/trafficcontrol/traffic_ops/testing/api/utils" |
| "github.com/apache/trafficcontrol/traffic_ops/toclientlib" |
| client "github.com/apache/trafficcontrol/traffic_ops/v4-client" |
| ) |
| |
| func TestCacheGroups(t *testing.T) { |
| WithObjs(t, []TCObj{Types, Parameters, CacheGroups, CDNs, Profiles, Statuses, Divisions, Regions, PhysLocations, Servers, Topologies}, func() { |
| |
| tomorrow := time.Now().AddDate(0, 0, 1).Format(time.RFC1123) |
| currentTime := time.Now().UTC().Add(-15 * time.Second) |
| currentTimeRFC := currentTime.Format(time.RFC1123) |
| |
| methodTests := utils.V4TestCase{ |
| "GET": { |
| "OK when VALID NAME parameter AND Lat/Long are 0": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"nullLatLongCG"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(1), ValidateResponseFields()), |
| }, |
| "NOT MODIFIED when NO CHANGES made": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {tomorrow}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)), |
| }, |
| "NOT MODIFIED when VALID NAME parameter when NO CHANGES made": { |
| ClientSession: TOSession, |
| RequestOpts: client.RequestOptions{ |
| Header: http.Header{rfc.IfModifiedSince: {tomorrow}}, |
| QueryParameters: url.Values{"name": {"originCachegroup"}}, |
| }, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)), |
| }, |
| "NOT MODIFIED when VALID SHORTNAME parameter when NO CHANGES made": { |
| ClientSession: TOSession, |
| RequestOpts: client.RequestOptions{ |
| Header: http.Header{rfc.IfModifiedSince: {tomorrow}}, |
| QueryParameters: url.Values{"shortName": {"mog1"}}, |
| }, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)), |
| }, |
| "OK when VALID request": { |
| ClientSession: TOSession, Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), |
| }, |
| "OK when VALID NAME parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"name": {"parentCachegroup"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(1), |
| ValidateExpectedField("Name", "parentCachegroup")), |
| }, |
| "OK when VALID SHORTNAME parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"shortName": {"pg2"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(1), |
| ValidateExpectedField("ShortName", "pg2")), |
| }, |
| "OK when VALID TOPOLOGY parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"topology": {"mso-topology"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), |
| }, |
| "OK when VALID TYPE parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"type": {"ORG_LOC"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1), |
| ValidateExpectedField("TypeName", "ORG_LOC")), |
| }, |
| "EMPTY RESPONSE when INVALID ID parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"id": {"10000"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(0)), |
| }, |
| "EMPTY RESPONSE when INVALID TYPE parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"type": {"10000"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(0)), |
| }, |
| "FIRST RESULT when LIMIT=1": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), ValidatePagination("limit")), |
| }, |
| "SECOND RESULT when LIMIT=1 OFFSET=1": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}, "offset": {"1"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), ValidatePagination("offset")), |
| }, |
| "SECOND RESULT when LIMIT=1 PAGE=2": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"orderby": {"id"}, "limit": {"1"}, "page": {"2"}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), ValidatePagination("page")), |
| }, |
| "BAD REQUEST when INVALID LIMIT parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"limit": {"-2"}}}, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)), |
| }, |
| "BAD REQUEST when INVALID OFFSET parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"limit": {"1"}, "offset": {"0"}}}, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)), |
| }, |
| "BAD REQUEST when INVALID PAGE parameter": { |
| ClientSession: TOSession, RequestOpts: client.RequestOptions{QueryParameters: url.Values{"limit": {"1"}, "page": {"0"}}}, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)), |
| }, |
| "UNAUTHORIZED when NOT LOGGED IN": { |
| ClientSession: NoAuthTOSession, Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusUnauthorized)), |
| }, |
| }, |
| "POST": { |
| "UNAUTHORIZED when NOT LOGGED IN": { |
| ClientSession: NoAuthTOSession, Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusUnauthorized)), |
| }, |
| }, |
| "PUT": { |
| "OK when VALID request": { |
| EndpointId: GetCacheGroupId(t, "cachegroup1"), ClientSession: TOSession, |
| RequestBody: map[string]interface{}{ |
| "latitude": 17.5, |
| "longitude": 17.5, |
| "name": "cachegroup1", |
| "shortName": "newShortName", |
| "localizationMethods": []string{"CZ"}, |
| "fallbacks": []string{"fallback1"}, |
| "typeName": "EDGE_LOC", |
| "typeId": -1, |
| }, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), |
| }, |
| "OK when updating CG with null Lat/Long": { |
| EndpointId: GetCacheGroupId(t, "nullLatLongCG"), ClientSession: TOSession, |
| RequestBody: map[string]interface{}{ |
| "name": "nullLatLongCG", |
| "shortName": "null-ll", |
| "typeName": "EDGE_LOC", |
| "fallbacks": []string{"fallback1"}, |
| "typeId": -1, |
| }, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), |
| }, |
| "BAD REQUEST when updating TYPE of CG in TOPOLOGY": { |
| EndpointId: GetCacheGroupId(t, "topology-edge-cg-01"), ClientSession: TOSession, |
| RequestBody: map[string]interface{}{ |
| "id": -1, |
| "latitude": 0, |
| "longitude": 0, |
| "name": "topology-edge-cg-01", |
| "shortName": "te1", |
| "typeName": "MID_LOC", |
| "typeId": -1, |
| }, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)), |
| }, |
| "PRECONDITION FAILED when updating with IMS & IUS Headers": { |
| EndpointId: GetCacheGroupId(t, "cachegroup1"), ClientSession: TOSession, |
| RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: {currentTimeRFC}}}, |
| RequestBody: map[string]interface{}{ |
| "name": "cachegroup1", |
| "shortName": "changeName", |
| "typeName": "EDGE_LOC", |
| "typeId": -1, |
| }, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)), |
| }, |
| "PRECONDITION FAILED when updating with IFMATCH ETAG Header": { |
| EndpointId: GetCacheGroupId(t, "cachegroup1"), ClientSession: TOSession, |
| RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfMatch: {rfc.ETag(currentTime)}}}, |
| RequestBody: map[string]interface{}{ |
| "name": "cachegroup1", |
| "shortName": "changeName", |
| "typeName": "EDGE_LOC", |
| "typeId": -1, |
| }, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusPreconditionFailed)), |
| }, |
| "UNAUTHORIZED when NOT LOGGED IN": { |
| EndpointId: GetCacheGroupId(t, "cachegroup1"), ClientSession: NoAuthTOSession, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusUnauthorized)), |
| }, |
| }, |
| "DELETE": { |
| "NOT FOUND when INVALID ID parameter": { |
| EndpointId: func() int { return 111111 }, ClientSession: TOSession, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusNotFound)), |
| }, |
| "UNAUTHORIZED when NOT LOGGED IN": { |
| EndpointId: GetCacheGroupId(t, "cachegroup1"), ClientSession: NoAuthTOSession, |
| Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusUnauthorized)), |
| }, |
| }, |
| "GET AFTER CHANGES": { |
| "OK when CHANGES made": { |
| ClientSession: TOSession, |
| RequestOpts: client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {currentTimeRFC}}}, |
| Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), |
| }, |
| }, |
| } |
| |
| for method, testCases := range methodTests { |
| t.Run(method, func(t *testing.T) { |
| for name, testCase := range testCases { |
| cg := tc.CacheGroupNullable{} |
| |
| if testCase.RequestOpts.QueryParameters.Has("type") { |
| val := testCase.RequestOpts.QueryParameters.Get("type") |
| if _, err := strconv.Atoi(val); err != nil { |
| testCase.RequestOpts.QueryParameters.Set("type", strconv.Itoa(GetTypeId(t, val))) |
| } |
| } |
| |
| if testCase.RequestBody != nil { |
| if _, ok := testCase.RequestBody["id"]; ok { |
| testCase.RequestBody["id"] = testCase.EndpointId() |
| } |
| if typeId, ok := testCase.RequestBody["typeId"]; ok { |
| if typeId == -1 { |
| if typeName, ok := testCase.RequestBody["typeName"]; ok { |
| testCase.RequestBody["typeId"] = GetTypeId(t, typeName.(string)) |
| } |
| } |
| } |
| dat, err := json.Marshal(testCase.RequestBody) |
| assert.NoError(t, err, "Error occurred when marshalling request body: %v", err) |
| err = json.Unmarshal(dat, &cg) |
| assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err) |
| } |
| |
| switch method { |
| case "GET", "GET AFTER CHANGES": |
| t.Run(name, func(t *testing.T) { |
| resp, reqInf, err := testCase.ClientSession.GetCacheGroups(testCase.RequestOpts) |
| for _, check := range testCase.Expectations { |
| check(t, reqInf, resp.Response, resp.Alerts, err) |
| } |
| }) |
| case "POST": |
| t.Run(name, func(t *testing.T) { |
| resp, reqInf, err := testCase.ClientSession.CreateCacheGroup(cg, testCase.RequestOpts) |
| for _, check := range testCase.Expectations { |
| check(t, reqInf, resp.Response, resp.Alerts, err) |
| } |
| }) |
| case "PUT": |
| t.Run(name, func(t *testing.T) { |
| resp, reqInf, err := testCase.ClientSession.UpdateCacheGroup(testCase.EndpointId(), cg, testCase.RequestOpts) |
| for _, check := range testCase.Expectations { |
| check(t, reqInf, resp.Response, resp.Alerts, err) |
| } |
| }) |
| case "DELETE": |
| t.Run(name, func(t *testing.T) { |
| alerts, reqInf, err := testCase.ClientSession.DeleteCacheGroup(testCase.EndpointId(), testCase.RequestOpts) |
| for _, check := range testCase.Expectations { |
| check(t, reqInf, nil, alerts, err) |
| } |
| }) |
| } |
| } |
| }) |
| } |
| }) |
| } |
| |
| func ValidateExpectedField(field string, expected string) utils.CkReqFunc { |
| return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) { |
| cgResp := resp.([]tc.CacheGroupNullable) |
| cg := cgResp[0] |
| switch field { |
| case "Name": |
| assert.Equal(t, expected, *cg.Name, "Expected name to be %v, but got %v", expected, *cg.Name) |
| case "ShortName": |
| assert.Equal(t, expected, *cg.ShortName, "Expected shortName to be %v, but got %v", expected, *cg.ShortName) |
| case "TypeName": |
| assert.Equal(t, expected, *cg.Type, "Expected type to be %v, but got %v", expected, *cg.Type) |
| default: |
| t.Errorf("Expected field: %v, does not exist in response", field) |
| } |
| } |
| } |
| |
| func ValidateResponseFields() utils.CkReqFunc { |
| return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) { |
| cgResp := resp.([]tc.CacheGroupNullable) |
| cg := cgResp[0] |
| assert.NotNil(t, cg.ID, "Expected response id to not be nil") |
| assert.NotNil(t, cg.Latitude, "Expected latitude to not be nil") |
| assert.NotNil(t, cg.Longitude, "Expected longitude to not be nil") |
| assert.Equal(t, 0.0, *cg.Longitude, "Expected Longitude to be 0, but got %v", cg.Longitude) |
| assert.Equal(t, 0.0, *cg.Latitude, "Expected Latitude to be 0, but got %v", cg.Latitude) |
| } |
| } |
| |
| func ValidatePagination(paginationParam string) utils.CkReqFunc { |
| return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) { |
| paginationResp := resp.([]tc.CacheGroupNullable) |
| |
| opts := client.NewRequestOptions() |
| opts.QueryParameters.Set("orderby", "id") |
| respBase, _, err := TOSession.GetCacheGroups(opts) |
| assert.RequireNoError(t, err, "cannot get Cache Groups: %v - alerts: %+v", err, respBase.Alerts) |
| |
| cachegroup := respBase.Response |
| assert.RequireGreaterOrEqual(t, len(cachegroup), 3, "Need at least 3 Cache Groups in Traffic Ops to test pagination support, found: %d", len(cachegroup)) |
| switch paginationParam { |
| case "limit:": |
| assert.Exactly(t, cachegroup[:1], paginationResp, "expected GET Cachegroups with limit = 1 to return first result") |
| case "offset": |
| assert.Exactly(t, cachegroup[1:2], paginationResp, "expected GET cachegroup with limit = 1, offset = 1 to return second result") |
| case "page": |
| assert.Exactly(t, cachegroup[1:2], paginationResp, "expected GET cachegroup with limit = 1, page = 2 to return second result") |
| } |
| } |
| } |
| |
| func GetTypeId(t *testing.T, typeName string) int { |
| opts := client.NewRequestOptions() |
| opts.QueryParameters.Set("name", typeName) |
| resp, _, err := TOSession.GetTypes(opts) |
| |
| assert.RequireNoError(t, err, "Get Types Request failed with error: %v", err) |
| assert.RequireEqual(t, 1, len(resp.Response), "Expected response object length 1, but got %d", len(resp.Response)) |
| assert.RequireNotNil(t, &resp.Response[0].ID, "Expected id to not be nil") |
| |
| return resp.Response[0].ID |
| } |
| |
| func GetCacheGroupId(t *testing.T, cacheGroupName string) func() int { |
| return func() int { |
| opts := client.NewRequestOptions() |
| opts.QueryParameters.Set("name", cacheGroupName) |
| |
| resp, _, err := TOSession.GetCacheGroups(opts) |
| assert.RequireNoError(t, err, "Get Cache Groups Request failed with error: %v", err) |
| assert.RequireEqual(t, len(resp.Response), 1, "Expected response object length 1, but got %d", len(resp.Response)) |
| assert.RequireNotNil(t, resp.Response[0].ID, "Expected id to not be nil") |
| |
| return *resp.Response[0].ID |
| } |
| } |
| |
| func CreateTestCacheGroups(t *testing.T) { |
| for _, cg := range testData.CacheGroups { |
| |
| resp, _, err := TOSession.CreateCacheGroup(cg, client.RequestOptions{}) |
| if err != nil { |
| t.Errorf("could not create Cache Group: %v - alerts: %+v", err, resp.Alerts) |
| continue |
| } |
| |
| // Testing 'join' fields during create |
| if cg.ParentName != nil && resp.Response.ParentName == nil { |
| t.Error("Parent cachegroup is null in response when it should have a value") |
| } |
| if cg.SecondaryParentName != nil && resp.Response.SecondaryParentName == nil { |
| t.Error("Secondary parent cachegroup is null in response when it should have a value") |
| } |
| if cg.Type != nil && resp.Response.Type == nil { |
| t.Error("Type is null in response when it should have a value") |
| } |
| assert.NotNil(t, resp.Response.LocalizationMethods, "Localization methods are null") |
| assert.NotNil(t, resp.Response.Fallbacks, "Fallbacks are null") |
| } |
| } |
| |
| func DeleteTestCacheGroups(t *testing.T) { |
| var parentlessCacheGroups []tc.CacheGroupNullable |
| opts := client.NewRequestOptions() |
| |
| // delete the edge caches. |
| for _, cg := range testData.CacheGroups { |
| if cg.Name == nil { |
| t.Error("Found a Cache Group with null or undefined name") |
| continue |
| } |
| |
| // Retrieve the CacheGroup by name so we can get the id for Deletion |
| opts.QueryParameters.Set("name", *cg.Name) |
| resp, _, err := TOSession.GetCacheGroups(opts) |
| assert.NoError(t, err, "Cannot GET CacheGroup by name '%s': %v - alerts: %+v", *cg.Name, err, resp.Alerts) |
| |
| if len(resp.Response) < 1 { |
| t.Errorf("Could not find test data Cache Group '%s' in Traffic Ops", *cg.Name) |
| continue |
| } |
| cg = resp.Response[0] |
| |
| // Cachegroups that are parents (usually mids but sometimes edges) |
| // need to be deleted only after the children cachegroups are deleted. |
| if cg.ParentCachegroupID == nil && cg.SecondaryParentCachegroupID == nil { |
| parentlessCacheGroups = append(parentlessCacheGroups, cg) |
| continue |
| } |
| |
| if cg.ID == nil { |
| t.Error("Traffic Ops returned a Cache Group with null or undefined ID") |
| continue |
| } |
| |
| alerts, _, err := TOSession.DeleteCacheGroup(*cg.ID, client.RequestOptions{}) |
| assert.NoError(t, err, "Cannot delete Cache Group: %v - alerts: %+v", err, alerts) |
| |
| // Retrieve the CacheGroup to see if it got deleted |
| opts.QueryParameters.Set("name", *cg.Name) |
| cgs, _, err := TOSession.GetCacheGroups(opts) |
| assert.NoError(t, err, "Error deleting Cache Group by name: %v - alerts: %+v", err, cgs.Alerts) |
| assert.Equal(t, 0, len(cgs.Response), "Expected CacheGroup name: %s to be deleted", *cg.Name) |
| } |
| |
| opts = client.NewRequestOptions() |
| // now delete the parentless cachegroups |
| for _, cg := range parentlessCacheGroups { |
| // nil check for cg.Name occurs prior to insertion into parentlessCacheGroups |
| opts.QueryParameters.Set("name", *cg.Name) |
| // Retrieve the CacheGroup by name so we can get the id for Deletion |
| resp, _, err := TOSession.GetCacheGroups(opts) |
| assert.NoError(t, err, "Cannot get Cache Group by name '%s': %v - alerts: %+v", *cg.Name, err, resp.Alerts) |
| |
| if len(resp.Response) < 1 { |
| t.Errorf("Cache Group '%s' somehow stopped existing since the last time we ask Traffic Ops about it", *cg.Name) |
| continue |
| } |
| |
| respCG := resp.Response[0] |
| if respCG.ID == nil { |
| t.Errorf("Traffic Ops returned Cache Group '%s' with null or undefined ID", *cg.Name) |
| continue |
| } |
| delResp, _, err := TOSession.DeleteCacheGroup(*respCG.ID, client.RequestOptions{}) |
| assert.NoError(t, err, "Cannot delete Cache Group '%s': %v - alerts: %+v", *respCG.Name, err, delResp.Alerts) |
| |
| // Retrieve the CacheGroup to see if it got deleted |
| opts.QueryParameters.Set("name", *cg.Name) |
| cgs, _, err := TOSession.GetCacheGroups(opts) |
| assert.NoError(t, err, "Error attempting to fetch Cache Group '%s' after deletion: %v - alerts: %+v", *cg.Name, err, cgs.Alerts) |
| assert.Equal(t, 0, len(cgs.Response), "Expected Cache Group '%s' to be deleted", *cg.Name) |
| } |
| } |