Repository: moov-io/customers
Pull request #65 (2020-02-06 19:22:44)

customers: validate state abbreviations

codecov-io: # [Codecov](https://codecov.io/gh/moov-io/customers/pull/65?src=pr&el=h1) Report
> Merging [#65](https://codecov.io/gh/moov-io/customers/pull/65?src=pr&el=desc) into [master](https://codecov.io/gh/moov-io/customers/commit/d4cbf164b32ebd42bd41e05f12f9276d13faa619?src=pr&el=desc) will **decrease** coverage by `0.08%`.
> The diff coverage is `25%`.


```diff
@@            Coverage Diff             @@
##           master      #65      +/-   ##
==========================================
- Coverage   60.79%   60.71%   -0.09%     
==========================================
  Files          16       16              
  Lines        1732     1736       +4     
==========================================
+ Hits         1053     1054       +1     
- Misses        530      532       +2     
- Partials      149      150       +1
```


Patch:
diff --git a/cmd/server/customers.go b/cmd/server/customers.go
index e4c5933..3deaa45 100644
--- a/cmd/server/customers.go
+++ b/cmd/server/customers.go
@@ -18,6 +18,7 @@ import (
 	moovhttp "github.com/moov-io/base/http"
 	"github.com/moov-io/customers"
 	client "github.com/moov-io/customers/client"
+	"github.com/moov-io/customers/internal/usstates"
 
 	"github.com/go-kit/kit/log"
 	"github.com/gorilla/mux"
@@ -113,10 +114,17 @@ type address struct {
 	Address2   string `json:"address2,omitempty"`
 	City       string `json:"city"`
 	State      string `json:"state"`
-	PostalCode string `json:"postalCode"`
+	PostalCode string `json:"postalCode"` // TODO(adam): validate against US postal codes
 	Country    string `json:"country"`
 }
 
+func (add address) validate() error {
+	if !usstates.Valid(add.State) {
+		return fmt.Errorf("create customer: invalid state=%s", add.State)
+	}
+	return nil
+}
+
 func (req customerRequest) validate() error {
 	if req.FirstName == "" || req.LastName == "" {
 		return errors.New("create customer: empty name field(s)")
@@ -124,6 +132,11 @@ func (req customerRequest) validate() error {
 	if err := validateMetadata(req.Metadata); err != nil {
 		return fmt.Errorf("create customer: %v", err)
 	}
+	for i := range req.Addresses {
+		if err := req.Addresses[i].validate(); err != nil {
+			return fmt.Errorf("address=%v validation failed: %v", req.Addresses[i], err)
+		}
+	}
 	return nil
 }
 
diff --git a/internal/usstates/validate.go b/internal/usstates/validate.go
new file mode 100644
index 0000000..f8b291f
--- /dev/null
+++ b/internal/usstates/validate.go
@@ -0,0 +1,85 @@
+// Copyright 2020 The Moov Authors
+// Use of this source code is governed by an Apache License
+// license that can be found in the LICENSE file.
+
+package usstates
+
+import "strings"
+
+var (
+	// These abbreviations are mappings of the 50 United States of America, various
+	// territories and federal districts.
+	//
+	// https://en.wikipedia.org/wiki/List_of_states_and_territories_of_the_United_States
+	abbreviations = map[string]bool{
+		// States
+		"AL": true, // Alabama
+		"AK": true, // Alaska
+		"AZ": true, // Arizona
+		"AR": true, // Arkansas
+		"CA": true, // California
+		"CO": true, // Colorado
+		"CT": true, // Connecticut
+		"DE": true, // Delaware
+		"FL": true, // Florida
+		"GA": true, // Georgia
+		"HI": true, // Hawaii
+		"ID": true, // Idaho
+		"IL": true, // Illinois
+		"IN": true, // Indiana
+		"IA": true, // Iowa
+		"KS": true, // Kansas
+		"KY": true, // Kentucky
+		"LA": true, // Louisiana
+		"ME": true, // Maine
+		"MD": true, // Maryland
+		"MA": true, // Massachusetts
+		"MI": true, // Michigan
+		"MN": true, // Minnesota
+		"MS": true, // Mississippi
+		"MO": true, // Missouri
+		"MT": true, // Montana
+		"NE": true, // Nebraska
+		"NV": true, // Nevada
+		"NH": true, // New Hampshire
+		"NJ": true, // New Jersey
+		"NM": true, // New Mexico
+		"NY": true, // New York
+		"NC": true, // North Carolina
+		"ND": true, // North Dakota
+		"OH": true, // Ohio
+		"OK": true, // Oklahoma
+		"OR": true, // Oregon
+		"PA": true, // Pennsylvania
+		"RI": true, // Rhode Island
+		"SC": true, // South Carolina
+		"SD": true, // South Dakota
+		"TN": true, // Tennessee
+		"TX": true, // Texas
+		"UT": true, // Utah
+		"VT": true, // Vermont
+		"VA": true, // Virginia
+		"WA": true, // Washington
+		"WV": true, // West Virginia
+		"WI": true, // Wisconsin
+		"WY": true, // Wyoming
+
+		// Federal districts
+		"DC": true, // District of Columbia
+
+		// Territories
+		"AS": true, // American Samoa
+		"GU": true, // Guam
+		"MP": true, // Northern Mariana Islands
+		"PR": true, // Puerto Rico
+		"VI": true, // U.S. Virgin Islands
+	}
+)
+
+// Valid returns true if the given abbreviation matches a United States state, territory, or federal district.
+func Valid(state string) bool {
+	if v, exists := abbreviations[strings.ToUpper(state)]; v && exists {
+		return true
+	}
+	return false
+}


Test patch:
diff --git a/cmd/server/customers_test.go b/cmd/server/customers_test.go
index faeb887..1d8bdcf 100644
--- a/cmd/server/customers_test.go
+++ b/cmd/server/customers_test.go
@@ -225,6 +225,20 @@ func TestCustomers__customerRequest(t *testing.T) {
 	}
 }
 
+func TestCustomers__addressValidate(t *testing.T) {
+	add := address{}
+
+	add.State = "IA"
+	if err := add.validate(); err != nil {
+		t.Errorf("unexpected error: %v", err)
+	}
+
+	add.State = "ZZ"
+	if err := add.validate(); err == nil {
+		t.Error("expected error")
+	}
+}
+
 func TestCustomers__createCustomer(t *testing.T) {
 	w := httptest.NewRecorder()
 	phone := `{"number": "555.555.5555", "type": "mobile"}`