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

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

commit 371979415af3ed627d8023b96e97274a36b5711e
Author: Rawlin Peters <rawlin_pet...@comcast.com>
AuthorDate: Fri Apr 6 12:11:00 2018 -0600

    Add a Location API
    
    Adding a Location API is the first step to refactoring lat/lon out of
    the Cache Group API into its own entity - Location. With the Location
    API in place, the cachegroup table can be updated to reference a
    Location, and other future entities (such as Origins) can make use of
    Locations as well.
    
    This is part of the larger "geolocation-based client steering" effort.
---
 lib/go-tc/v13/locations.go                         |  98 ++++++
 .../db/migrations/20180409000000_add_location.sql  |  31 ++
 .../traffic_ops_golang/location/locations.go       | 369 +++++++++++++++++++++
 .../traffic_ops_golang/location/locations_test.go  | 176 ++++++++++
 traffic_ops/traffic_ops_golang/routes.go           |   8 +
 5 files changed, 682 insertions(+)

diff --git a/lib/go-tc/v13/locations.go b/lib/go-tc/v13/locations.go
new file mode 100644
index 0000000..282c487
--- /dev/null
+++ b/lib/go-tc/v13/locations.go
@@ -0,0 +1,98 @@
+package v13
+
+import tc "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+
+/*
+ * 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.
+ */
+
+// A List of Locations Response
+// swagger:response LocationsResponse
+// in: body
+type LocationsResponse struct {
+       // in: body
+       Response []Location `json:"response"`
+}
+
+// A Single Location Response for Update and Create to depict what changed
+// swagger:response LocationResponse
+// in: body
+type LocationResponse struct {
+       // in: body
+       Response Location `json:"response"`
+}
+
+// Location ...
+type Location struct {
+
+       // The Location to retrieve
+       //
+       // ID of the Location
+       //
+       // required: true
+       ID int `json:"id" db:"id"`
+
+       // Name of the Location
+       //
+       // required: true
+       Name string `json:"name" db:"name"`
+
+       // the latitude of the Location
+       //
+       // required: true
+       Latitude float64 `json:"latitude" db:"latitude"`
+
+       // the latitude of the Location
+       //
+       // required: true
+       Longitude float64 `json:"longitude" db:"longitude"`
+
+       // LastUpdated
+       //
+       LastUpdated tc.TimeNoMod `json:"lastUpdated" db:"last_updated"`
+}
+
+// LocationNullable ...
+type LocationNullable struct {
+
+       // The Location to retrieve
+       //
+       // ID of the Location
+       //
+       // required: true
+       ID *int `json:"id" db:"id"`
+
+       // Name of the Location
+       //
+       // required: true
+       Name *string `json:"name" db:"name"`
+
+       // the latitude of the Location
+       //
+       // required: true
+       Latitude *float64 `json:"latitude" db:"latitude"`
+
+       // the latitude of the Location
+       //
+       // required: true
+       Longitude *float64 `json:"longitude" db:"longitude"`
+
+       // LastUpdated
+       //
+       LastUpdated *tc.TimeNoMod `json:"lastUpdated" db:"last_updated"`
+}
diff --git a/traffic_ops/app/db/migrations/20180409000000_add_location.sql 
b/traffic_ops/app/db/migrations/20180409000000_add_location.sql
new file mode 100644
index 0000000..1a259be
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20180409000000_add_location.sql
@@ -0,0 +1,31 @@
+/*
+
+    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.
+*/
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+
+CREATE TABLE location (
+    id bigserial primary key NOT NULL,
+    name text UNIQUE NOT NULL,
+    latitude numeric NOT NULL DEFAULT 0.0,
+    longitude numeric NOT NULL DEFAULT 0.0,
+    last_updated timestamp WITH time zone NOT NULL DEFAULT now()
+);
+
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON location FOR EACH 
ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+DROP TABLE location;
diff --git a/traffic_ops/traffic_ops_golang/location/locations.go 
b/traffic_ops/traffic_ops_golang/location/locations.go
new file mode 100644
index 0000000..cab72d8
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/location/locations.go
@@ -0,0 +1,369 @@
+package location
+
+/*
+ * 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 (
+       "errors"
+       "fmt"
+       "strconv"
+       "strings"
+
+       "github.com/apache/incubator-trafficcontrol/lib/go-log"
+       "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+       "github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/tovalidate"
+       validation "github.com/go-ozzo/ozzo-validation"
+       "github.com/jmoiron/sqlx"
+       "github.com/lib/pq"
+)
+
+//we need a type alias to define functions on
+type TOLocation v13.LocationNullable
+
+//the refType is passed into the handlers where a copy of its type is used to 
decode the json.
+var refType = TOLocation{}
+
+func GetRefType() *TOLocation {
+       return &refType
+}
+
+func (location TOLocation) GetKeyFieldsInfo() []api.KeyFieldInfo {
+       return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (location TOLocation) GetKeys() (map[string]interface{}, bool) {
+       if location.ID == nil {
+               return map[string]interface{}{"id": 0}, false
+       }
+       return map[string]interface{}{"id": *location.ID}, true
+}
+
+func (location TOLocation) GetAuditName() string {
+       if location.Name != nil {
+               return *location.Name
+       }
+       if location.ID != nil {
+               return strconv.Itoa(*location.ID)
+       }
+       return "0"
+}
+
+func (location TOLocation) GetType() string {
+       return "location"
+}
+
+func (location *TOLocation) SetKeys(keys map[string]interface{}) {
+       i, _ := keys["id"].(int) //this utilizes the non panicking type 
assertion, if the thrown away ok variable is false i will be the zero of the 
type, 0 here.
+       location.ID = &i
+}
+
+func isValidLocationChar(r rune) bool {
+       if r >= 'a' && r <= 'z' {
+               return true
+       }
+       if r >= 'A' && r <= 'Z' {
+               return true
+       }
+       if r >= '0' && r <= '9' {
+               return true
+       }
+       if r == '.' || r == '-' || r == '_' {
+               return true
+       }
+       return false
+}
+
+// IsValidLocationName returns true if the name contains only characters valid 
for a Location name
+func IsValidLocationName(str string) bool {
+       i := strings.IndexFunc(str, func(r rune) bool { return 
!isValidLocationChar(r) })
+       return i == -1
+}
+
+// Validate fulfills the api.Validator interface
+func (location TOLocation) Validate(db *sqlx.DB) []error {
+       validName := validation.NewStringRule(IsValidLocationName, "invalid 
characters found - Use alphanumeric . or - or _ .")
+       latitudeErr := "Must be a floating point number within the range +-90"
+       longitudeErr := "Must be a floating point number within the range +-180"
+       errs := validation.Errors{
+               "name":      validation.Validate(location.Name, 
validation.Required, validName),
+               "latitude":  validation.Validate(location.Latitude, 
validation.Min(-90.0).Error(latitudeErr), 
validation.Max(90.0).Error(latitudeErr)),
+               "longitude": validation.Validate(location.Longitude, 
validation.Min(-180.0).Error(longitudeErr), 
validation.Max(180.0).Error(longitudeErr)),
+       }
+       return tovalidate.ToErrors(errs)
+}
+
+//The TOLocation implementation of the Creator interface
+//all implementations of Creator should use transactions and return the proper 
errorType
+//ParsePQUniqueConstraintError is used to determine if a location with 
conflicting values exists
+//if so, it will return an errorType of DataConflict and the type should be 
appended to the
+//generic error message returned
+//The insert sql returns the id and lastUpdated values of the newly inserted 
location and have
+//to be added to the struct
+func (location *TOLocation) Create(db *sqlx.DB, user auth.CurrentUser) (error, 
tc.ApiErrorType) {
+       rollbackTransaction := true
+       tx, err := db.Beginx()
+       defer func() {
+               if tx == nil || !rollbackTransaction {
+                       return
+               }
+               err := tx.Rollback()
+               if err != nil {
+                       log.Errorln(errors.New("rolling back transaction: " + 
err.Error()))
+               }
+       }()
+
+       if err != nil {
+               log.Error.Printf("could not begin transaction: %v", err)
+               return tc.DBError, tc.SystemError
+       }
+       resultRows, err := tx.NamedQuery(insertQuery(), location)
+       if err != nil {
+               if pqErr, ok := err.(*pq.Error); ok {
+                       err, eType := 
dbhelpers.ParsePQUniqueConstraintError(pqErr)
+                       if eType == tc.DataConflictError {
+                               return errors.New("a location with " + 
err.Error()), eType
+                       }
+                       return err, eType
+               } else {
+                       log.Errorf("received non pq error: %++v from create 
execution", err)
+                       return tc.DBError, tc.SystemError
+               }
+       }
+       defer resultRows.Close()
+
+       var id int
+       var lastUpdated tc.TimeNoMod
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := resultRows.Scan(&id, &lastUpdated); err != nil {
+                       log.Error.Printf("could not scan id from insert: %s\n", 
err)
+                       return tc.DBError, tc.SystemError
+               }
+       }
+       if rowsAffected == 0 {
+               err = errors.New("no location was inserted, no id was returned")
+               log.Errorln(err)
+               return tc.DBError, tc.SystemError
+       } else if rowsAffected > 1 {
+               err = errors.New("too many ids returned from location insert")
+               log.Errorln(err)
+               return tc.DBError, tc.SystemError
+       }
+       location.SetKeys(map[string]interface{}{"id": id})
+       location.LastUpdated = &lastUpdated
+       err = tx.Commit()
+       if err != nil {
+               log.Errorln("Could not commit transaction: ", err)
+               return tc.DBError, tc.SystemError
+       }
+       rollbackTransaction = false
+       return nil, tc.NoError
+}
+
+func (location *TOLocation) Read(db *sqlx.DB, parameters map[string]string, 
user auth.CurrentUser) ([]interface{}, []error, tc.ApiErrorType) {
+       var rows *sqlx.Rows
+
+       // Query Parameters to Database Query column mappings
+       // see the fields mapped in the SQL query
+       queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+               "id":   dbhelpers.WhereColumnInfo{"id", api.IsInt},
+               "name": dbhelpers.WhereColumnInfo{"name", nil},
+       }
+       where, orderBy, queryValues, errs := 
dbhelpers.BuildWhereAndOrderBy(parameters, queryParamsToQueryCols)
+       if len(errs) > 0 {
+               return nil, errs, tc.DataConflictError
+       }
+
+       query := selectQuery() + where + orderBy
+       log.Debugln("Query is ", query)
+
+       rows, err := db.NamedQuery(query, queryValues)
+       if err != nil {
+               log.Errorf("Error querying Location: %v", err)
+               return nil, []error{tc.DBError}, tc.SystemError
+       }
+       defer rows.Close()
+
+       Locations := []interface{}{}
+       for rows.Next() {
+               var s TOLocation
+               if err = rows.StructScan(&s); err != nil {
+                       log.Errorf("error parsing Location rows: %v", err)
+                       return nil, []error{tc.DBError}, tc.SystemError
+               }
+               Locations = append(Locations, s)
+       }
+
+       return Locations, []error{}, tc.NoError
+}
+
+//The TOLocation implementation of the Updater interface
+//all implementations of Updater should use transactions and return the proper 
errorType
+//ParsePQUniqueConstraintError is used to determine if a location with 
conflicting values exists
+//if so, it will return an errorType of DataConflict and the type should be 
appended to the
+//generic error message returned
+func (location *TOLocation) Update(db *sqlx.DB, user auth.CurrentUser) (error, 
tc.ApiErrorType) {
+       rollbackTransaction := true
+       tx, err := db.Beginx()
+       defer func() {
+               if tx == nil || !rollbackTransaction {
+                       return
+               }
+               err := tx.Rollback()
+               if err != nil {
+                       log.Errorln(errors.New("rolling back transaction: " + 
err.Error()))
+               }
+       }()
+
+       if err != nil {
+               log.Error.Printf("could not begin transaction: %v", err)
+               return tc.DBError, tc.SystemError
+       }
+       log.Debugf("about to run exec query: %s with location: %++v", 
updateQuery(), location)
+       resultRows, err := tx.NamedQuery(updateQuery(), location)
+       if err != nil {
+               if pqErr, ok := err.(*pq.Error); ok {
+                       err, eType := 
dbhelpers.ParsePQUniqueConstraintError(pqErr)
+                       if eType == tc.DataConflictError {
+                               return errors.New("a location with " + 
err.Error()), eType
+                       }
+                       return err, eType
+               } else {
+                       log.Errorf("received error: %++v from update 
execution", err)
+                       return tc.DBError, tc.SystemError
+               }
+       }
+       defer resultRows.Close()
+
+       var lastUpdated tc.TimeNoMod
+       rowsAffected := 0
+       for resultRows.Next() {
+               rowsAffected++
+               if err := resultRows.Scan(&lastUpdated); err != nil {
+                       log.Error.Printf("could not scan lastUpdated from 
insert: %s\n", err)
+                       return tc.DBError, tc.SystemError
+               }
+       }
+       log.Debugf("lastUpdated: %++v", lastUpdated)
+       location.LastUpdated = &lastUpdated
+       if rowsAffected != 1 {
+               if rowsAffected < 1 {
+                       return errors.New("no location found with this id"), 
tc.DataMissingError
+               } else {
+                       return fmt.Errorf("this update affected too many rows: 
%d", rowsAffected), tc.SystemError
+               }
+       }
+       err = tx.Commit()
+       if err != nil {
+               log.Errorln("Could not commit transaction: ", err)
+               return tc.DBError, tc.SystemError
+       }
+       rollbackTransaction = false
+       return nil, tc.NoError
+}
+
+//The Location implementation of the Deleter interface
+//all implementations of Deleter should use transactions and return the proper 
errorType
+func (location *TOLocation) Delete(db *sqlx.DB, user auth.CurrentUser) (error, 
tc.ApiErrorType) {
+       rollbackTransaction := true
+       tx, err := db.Beginx()
+       defer func() {
+               if tx == nil || !rollbackTransaction {
+                       return
+               }
+               err := tx.Rollback()
+               if err != nil {
+                       log.Errorln(errors.New("rolling back transaction: " + 
err.Error()))
+               }
+       }()
+
+       if err != nil {
+               log.Error.Printf("could not begin transaction: %v", err)
+               return tc.DBError, tc.SystemError
+       }
+       log.Debugf("about to run exec query: %s with location: %++v", 
deleteQuery(), location)
+       result, err := tx.NamedExec(deleteQuery(), location)
+       if err != nil {
+               log.Errorf("received error: %++v from delete execution", err)
+               return tc.DBError, tc.SystemError
+       }
+       rowsAffected, err := result.RowsAffected()
+       if err != nil {
+               return tc.DBError, tc.SystemError
+       }
+       if rowsAffected != 1 {
+               if rowsAffected < 1 {
+                       return errors.New("no location with that id found"), 
tc.DataMissingError
+               } else {
+                       return fmt.Errorf("this delete affected too many rows: 
%d", rowsAffected), tc.SystemError
+               }
+       }
+       err = tx.Commit()
+       if err != nil {
+               log.Errorln("Could not commit transaction: ", err)
+               return tc.DBError, tc.SystemError
+       }
+       rollbackTransaction = false
+       return nil, tc.NoError
+}
+
+func selectQuery() string {
+       query := `SELECT
+id,
+latitude,
+longitude,
+last_updated,
+name
+
+FROM location l`
+       return query
+}
+
+func updateQuery() string {
+       query := `UPDATE
+location SET
+latitude=:latitude,
+longitude=:longitude,
+name=:name
+WHERE id=:id RETURNING last_updated`
+       return query
+}
+
+func insertQuery() string {
+       query := `INSERT INTO location (
+latitude,
+longitude,
+name) VALUES (
+:latitude,
+:longitude,
+:name) RETURNING id,last_updated`
+       return query
+}
+
+func deleteQuery() string {
+       query := `DELETE FROM location
+WHERE id=:id`
+       return query
+}
diff --git a/traffic_ops/traffic_ops_golang/location/locations_test.go 
b/traffic_ops/traffic_ops_golang/location/locations_test.go
new file mode 100644
index 0000000..4ca6291
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/location/locations_test.go
@@ -0,0 +1,176 @@
+package location
+
+/*
+ * 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 (
+       "errors"
+       "reflect"
+       "strings"
+       "testing"
+       "time"
+
+       "github.com/apache/incubator-trafficcontrol/lib/go-tc"
+       "github.com/apache/incubator-trafficcontrol/lib/go-tc/v13"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/api"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/auth"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/test"
+       "github.com/jmoiron/sqlx"
+
+       sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
+)
+
+func getTestLocations() []v13.Location {
+       locs := []v13.Location{}
+       testLoc1 := v13.Location{
+               ID:          1,
+               Name:        "location1",
+               Latitude:    38.7,
+               Longitude:   90.7,
+               LastUpdated: tc.TimeNoMod{Time: time.Now()},
+       }
+       locs = append(locs, testLoc1)
+
+       testLoc2 := v13.Location{
+               ID:          2,
+               Name:        "location2",
+               Latitude:    38.7,
+               Longitude:   90.7,
+               LastUpdated: tc.TimeNoMod{Time: time.Now()},
+       }
+       locs = append(locs, testLoc2)
+
+       return locs
+}
+
+func TestReadLocations(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()
+
+       refType := GetRefType()
+
+       testLocs := getTestLocations()
+       cols := test.ColsFromStructByTag("db", v13.Location{})
+       rows := sqlmock.NewRows(cols)
+
+       for _, ts := range testLocs {
+               rows = rows.AddRow(
+                       ts.ID,
+                       ts.Name,
+                       ts.Latitude,
+                       ts.Longitude,
+                       ts.LastUpdated,
+               )
+       }
+       mock.ExpectQuery("SELECT").WillReturnRows(rows)
+       v := map[string]string{"id": "1"}
+
+       locations, errs, _ := refType.Read(db, v, auth.CurrentUser{})
+       if len(errs) > 0 {
+               t.Errorf("location.Read expected: no errors, actual: %v", errs)
+       }
+
+       if len(locations) != 2 {
+               t.Errorf("location.Read expected: len(locations) == 2, actual: 
%v", len(locations))
+       }
+}
+
+func TestFuncs(t *testing.T) {
+       if strings.Index(selectQuery(), "SELECT") != 0 {
+               t.Errorf("expected selectQuery to start with SELECT")
+       }
+       if strings.Index(insertQuery(), "INSERT") != 0 {
+               t.Errorf("expected insertQuery to start with INSERT")
+       }
+       if strings.Index(updateQuery(), "UPDATE") != 0 {
+               t.Errorf("expected updateQuery to start with UPDATE")
+       }
+       if strings.Index(deleteQuery(), "DELETE") != 0 {
+               t.Errorf("expected deleteQuery to start with DELETE")
+       }
+}
+
+func TestInterfaces(t *testing.T) {
+       var i interface{}
+       i = &TOLocation{}
+
+       if _, ok := i.(api.Creator); !ok {
+               t.Errorf("location must be creator")
+       }
+       if _, ok := i.(api.Reader); !ok {
+               t.Errorf("location must be reader")
+       }
+       if _, ok := i.(api.Updater); !ok {
+               t.Errorf("location must be updater")
+       }
+       if _, ok := i.(api.Deleter); !ok {
+               t.Errorf("location must be deleter")
+       }
+       if _, ok := i.(api.Identifier); !ok {
+               t.Errorf("location must be Identifier")
+       }
+}
+
+func TestValidate(t *testing.T) {
+       // invalid name, latitude, and longitude
+       id := 1
+       nm := "not!a!valid!name"
+       la := -190.0
+       lo := -190.0
+       lu := tc.TimeNoMod{Time: time.Now()}
+       c := TOLocation{ID: &id,
+               Name:        &nm,
+               Latitude:    &la,
+               Longitude:   &lo,
+               LastUpdated: &lu,
+       }
+       errs := test.SortErrors(c.Validate(nil))
+
+       expectedErrs := []error{
+               errors.New(`'latitude' Must be a floating point number within 
the range +-90`),
+               errors.New(`'longitude' Must be a floating point number within 
the range +-180`),
+               errors.New(`'name' invalid characters found - Use alphanumeric 
. or - or _ .`),
+       }
+
+       if !reflect.DeepEqual(expectedErrs, errs) {
+               t.Errorf("expected %s, got %s", expectedErrs, errs)
+       }
+
+       //  valid name, latitude, longitude
+       nm = "This.is.2.a-Valid---Location."
+       la = 90.0
+       lo = 90.0
+       c = TOLocation{ID: &id,
+               Name:        &nm,
+               Latitude:    &la,
+               Longitude:   &lo,
+               LastUpdated: &lu,
+       }
+       expectedErrs = []error{}
+       errs = c.Validate(nil)
+       if !reflect.DeepEqual(expectedErrs, errs) {
+               t.Errorf("expected %s, got %s", expectedErrs, errs)
+       }
+}
diff --git a/traffic_ops/traffic_ops_golang/routes.go 
b/traffic_ops/traffic_ops_golang/routes.go
index 3453d19..01a830f 100644
--- a/traffic_ops/traffic_ops_golang/routes.go
+++ b/traffic_ops/traffic_ops_golang/routes.go
@@ -41,6 +41,7 @@ import (
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice/request/comment"
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/division"
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/hwinfo"
+       
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/location"
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/parameter"
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/physlocation"
        
"github.com/apache/incubator-trafficcontrol/traffic_ops/traffic_ops_golang/ping"
@@ -206,6 +207,13 @@ func Routes(d ServerData) ([]Route, []RawRoute, 
http.Handler, error) {
                {1.3, http.MethodPut, `deliveryservices/{xmlID}/urisignkeys$`, 
saveDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, 
Authenticated, nil},
                {1.3, http.MethodDelete, 
`deliveryservices/{xmlID}/urisignkeys$`, 
removeDeliveryServiceURIKeysHandler(d.DB, d.Config), auth.PrivLevelAdmin, 
Authenticated, nil},
 
+               //Locations
+               {1.3, http.MethodGet, `locations/?(\.json)?$`, 
api.ReadHandler(location.GetRefType(), d.DB), auth.PrivLevelReadOnly, 
Authenticated, nil},
+               {1.3, http.MethodGet, `locations/?$`, 
api.ReadHandler(location.GetRefType(), d.DB), auth.PrivLevelReadOnly, 
Authenticated, nil},
+               {1.3, http.MethodPut, `locations/?$`, 
api.UpdateHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, 
Authenticated, nil},
+               {1.3, http.MethodPost, `locations/?$`, 
api.CreateHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, 
Authenticated, nil},
+               {1.3, http.MethodDelete, `locations/?$`, 
api.DeleteHandler(location.GetRefType(), d.DB), auth.PrivLevelOperations, 
Authenticated, nil},
+
                //Servers
                {1.3, http.MethodPost, `servers/{id}/deliveryservices$`, 
server.AssignDeliveryServicesToServerHandler(d.DB), auth.PrivLevelOperations, 
Authenticated, nil},
                {1.3, http.MethodGet, `servers/{host_name}/update_status$`, 
server.GetServerUpdateStatusHandler(d.DB), auth.PrivLevelReadOnly, 
Authenticated, nil},

-- 
To stop receiving notification emails like this one, please contact
dewr...@apache.org.

Reply via email to