You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by da...@apache.org on 2018/03/09 18:42:26 UTC

[incubator-trafficcontrol] 04/19: Initial version of source for /profiles

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

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

commit 03433b35292e52a091aff586a8977c3d785decd3
Author: Dewayne Richardson <de...@apache.org>
AuthorDate: Fri Mar 2 11:29:32 2018 -0700

    Initial version of source for /profiles
---
 traffic_ops/traffic_ops_golang/profile/profiles.go | 375 +++++++++++++++++++++
 .../traffic_ops_golang/profile/profiles_test.go    | 146 ++++++++
 2 files changed, 521 insertions(+)

diff --git a/traffic_ops/traffic_ops_golang/profile/profiles.go b/traffic_ops/traffic_ops_golang/profile/profiles.go
new file mode 100644
index 0000000..46adb58
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/profile/profiles.go
@@ -0,0 +1,375 @@
+package profile
+
+/*
+ * 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"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-log"
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"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 TOProfile tc.ProfileNullable
+
+//the refType is passed into the handlers where a copy of its type is used to decode the json.
+var refType = TOProfile(tc.ProfileNullable{})
+
+func GetRefType() *TOProfile {
+	return &refType
+}
+
+//Implementation of the Identifier, Validator interface functions
+func (prof TOProfile) GetID() (int, bool) {
+	if prof.ID == nil {
+		return 0, false
+	}
+	return *prof.ID, true
+}
+
+func (prof *TOProfile) GetAuditName() string {
+	if prof.Name != nil {
+		return *prof.Name
+	}
+	if prof.ID != nil {
+		return strconv.Itoa(*prof.ID)
+	}
+	return "unknown"
+}
+
+func (prof *TOProfile) GetType() string {
+	return "physLocation"
+}
+
+func (prof *TOProfile) SetID(i int) {
+	prof.ID = &i
+}
+
+func (prof *TOProfile) Validate(db *sqlx.DB) []error {
+	errs := validation.Errors{
+		"name":        validation.Validate(prof.Name, validation.Required),
+		"description": validation.Validate(prof.Description, validation.Required),
+		"cdn":         validation.Validate(prof.CDNID, validation.Required),
+		"type":        validation.Validate(prof.Type, validation.Required),
+	}
+	if errs != nil {
+		return tovalidate.ToErrors(errs)
+	}
+	return nil
+}
+
+func (prof *TOProfile) 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{
+		"name": dbhelpers.WhereColumnInfo{"prof.name", nil},
+		"id":   dbhelpers.WhereColumnInfo{"prof.id", api.IsInt},
+	}
+	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 Profile: %v", err)
+		return nil, []error{tc.DBError}, tc.SystemError
+	}
+	defer rows.Close()
+
+	physLocations := []interface{}{}
+	for rows.Next() {
+		var s tc.ProfileNullable
+		if err = rows.StructScan(&s); err != nil {
+			log.Errorf("error parsing Profile rows: %v", err)
+			return nil, []error{tc.DBError}, tc.SystemError
+		}
+		physLocations = append(physLocations, s)
+	}
+
+	return physLocations, []error{}, tc.NoError
+
+}
+
+func selectQuery() string {
+
+	query := `SELECT
+prof.address,
+prof.city,
+prof.comments,
+prof.email,
+prof.id,
+prof.last_updated,
+prof.name,
+prof.phone,
+prof.poc,
+r.id as region,
+r.name as region_name,
+prof.short_name,
+prof.state,
+prof.zip
+FROM profile prof
+JOIN region r ON prof.region = r.id`
+
+	return query
+}
+
+//The TOProfile implementation of the Updater interface
+//all implementations of Updater should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a profile 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 (prof *TOProfile) 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 profile: %++v", updateQuery(), prof)
+	resultRows, err := tx.NamedQuery(updateQuery(), prof)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a profile with " + err.Error()), eType
+			}
+			return err, eType
+		}
+		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)
+	prof.LastUpdated = &lastUpdated
+	if rowsAffected != 1 {
+		if rowsAffected < 1 {
+			return errors.New("no profile found with this id"), tc.DataMissingError
+		}
+		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 TOProfile implementation of the Creator interface
+//all implementations of Creator should use transactions and return the proper errorType
+//ParsePQUniqueConstraintError is used to determine if a profile 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 profile and have
+//to be added to the struct
+func (prof *TOProfile) 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(), prof)
+	if err != nil {
+		if pqErr, ok := err.(*pq.Error); ok {
+			err, eType := dbhelpers.ParsePQUniqueConstraintError(pqErr)
+			if eType == tc.DataConflictError {
+				return errors.New("a profile with " + err.Error()), eType
+			}
+			return err, eType
+		}
+		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 profile was inserted, no id was returned")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+	if rowsAffected > 1 {
+		err = errors.New("too many ids returned from profile insert")
+		log.Errorln(err)
+		return tc.DBError, tc.SystemError
+	}
+
+	prof.SetID(id)
+	prof.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
+}
+
+//The Profile implementation of the Deleter interface
+//all implementations of Deleter should use transactions and return the proper errorType
+func (prof *TOProfile) 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 profile: %++v", deleteQuery(), prof)
+	result, err := tx.NamedExec(deleteQuery(), prof)
+	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 {
+		return errors.New("no profile with that id found"), tc.DataMissingError
+	}
+	if rowsAffected > 1 {
+		return fmt.Errorf("this create 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 updateQuery() string {
+	query := `UPDATE
+profile SET
+address=:address,
+city=:city,
+comments=:comments,
+email=:email,
+name=:name,
+phone=:phone,
+poc=:poc,
+region=:region,
+short_name=:short_name,
+state=:state,
+zip=:zip
+WHERE id=:id RETURNING last_updated`
+	return query
+}
+
+func insertQuery() string {
+	query := `INSERT INTO profile (
+address,
+city,
+comments,
+email,
+name,
+phone,
+poc,
+region,
+short_name,
+state,
+zip) VALUES (
+:address,
+:city,
+:comments,
+:email,
+:name,
+:phone,
+:poc,
+:region,
+:short_name,
+:state,
+:zip) RETURNING id,last_updated`
+	return query
+}
+
+func deleteQuery() string {
+	query := `DELETE FROM profile
+WHERE id=:id`
+	return query
+}
diff --git a/traffic_ops/traffic_ops_golang/profile/profiles_test.go b/traffic_ops/traffic_ops_golang/profile/profiles_test.go
new file mode 100644
index 0000000..1c4704b
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/profile/profiles_test.go
@@ -0,0 +1,146 @@
+package profile
+
+/*
+ * 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"
+	"testing"
+	"time"
+
+	"github.com/apache/incubator-trafficcontrol/lib/go-tc"
+	"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 getTestPhysLocations() []tc.PhysLocation {
+	physLocations := []tc.PhysLocation{}
+	testCase := tc.PhysLocation{
+		Address:     "1118 S. Grant St.",
+		City:        "Denver",
+		Email:       "d.t@gmail.com",
+		ID:          1,
+		LastUpdated: tc.TimeNoMod{Time: time.Now()},
+		Name:        "physLocation1",
+		Phone:       "303-210-0000",
+		POC:         "Dennis Thompson",
+		RegionID:    1,
+		RegionName:  "region1",
+		ShortName:   "pl1",
+		State:       "CO",
+		Zip:         "80210",
+	}
+	physLocations = append(physLocations, testCase)
+
+	testCase2 := testCase
+	testCase2.Name = "physLocation2"
+	physLocations = append(physLocations, testCase2)
+
+	return physLocations
+}
+
+func TestGetPhysLocations(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()
+
+	testCase := getTestPhysLocations()
+	cols := test.ColsFromStructByTag("db", tc.PhysLocation{})
+	rows := sqlmock.NewRows(cols)
+
+	for _, ts := range testCase {
+		rows = rows.AddRow(
+			ts.Address,
+			ts.City,
+			ts.Comments,
+			ts.Email,
+			ts.ID,
+			ts.LastUpdated,
+			ts.Name,
+			ts.Phone,
+			ts.POC,
+			ts.RegionID,
+			ts.RegionName,
+			ts.ShortName,
+			ts.State,
+			ts.Zip,
+		)
+	}
+	mock.ExpectQuery("SELECT").WillReturnRows(rows)
+	v := map[string]string{"dsId": "1"}
+
+	physLocations, errs, _ := refType.Read(db, v, auth.CurrentUser{})
+	if len(errs) > 0 {
+		t.Errorf("physLocation.Read expected: no errors, actual: %v", errs)
+	}
+
+	if len(physLocations) != 2 {
+		t.Errorf("physLocation.Read expected: len(physLocations) == 2, actual: %v", len(physLocations))
+	}
+
+}
+
+func TestInterfaces(t *testing.T) {
+	var i interface{}
+	i = &TOPhysLocation{}
+
+	if _, ok := i.(api.Creator); !ok {
+		t.Errorf("PhysLocation must be Creator")
+	}
+	if _, ok := i.(api.Reader); !ok {
+		t.Errorf("PhysLocation must be Reader")
+	}
+	if _, ok := i.(api.Updater); !ok {
+		t.Errorf("PhysLocation must be Updater")
+	}
+	if _, ok := i.(api.Deleter); !ok {
+		t.Errorf("PhysLocation must be Deleter")
+	}
+	if _, ok := i.(api.Identifier); !ok {
+		t.Errorf("PhysLocation must be Identifier")
+	}
+}
+
+func TestValidate(t *testing.T) {
+	p := TOPhysLocation{}
+	errs := test.SortErrors(p.Validate(nil))
+	expected := test.SortErrors([]error{
+		errors.New("'state' cannot be blank"),
+		errors.New("'zip' cannot be blank"),
+		errors.New("'address' cannot be blank"),
+		errors.New("'city' cannot be blank"),
+		errors.New("'name' cannot be blank"),
+		errors.New("'regionId' cannot be blank"),
+		errors.New("'shortName' cannot be blank"),
+	})
+
+	if !reflect.DeepEqual(expected, errs) {
+		t.Errorf("expected %++v,  got %++v", expected, errs)
+	}
+}

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