You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ro...@apache.org on 2019/03/21 19:17:56 UTC
[trafficcontrol] branch master updated: Cache Config Parser (#3360)
This is an automated email from the ASF dual-hosted git repository.
rob 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 ab35846 Cache Config Parser (#3360)
ab35846 is described below
commit ab35846ef711a26c69280dd654fd1594054ce7c7
Author: Matthew Allen Moltzau <Ma...@comcast.com>
AuthorDate: Thu Mar 21 13:17:51 2019 -0600
Cache Config Parser (#3360)
* Initial work of cache config parser
* Updated comments, renamed constants, and other minor edits
* Cache config parser is turning out. Restructured the config error. I have comments to check a few things in ATS. The cache_config_test is broken atm.
* Implemented time validators and cleaned up. Only a few comments now. Still fleshing out tests
* Some changes required after referring to ATS
The whole config line is case insensitive now.
Made action mandantory
* Finished positive and negative config tests
* 100% test coverage!
Finished test cases for cache config
Wrote tests for time format validators
* Added license headers
* Added case where config has comment
* Added some comments for go doc.
Added PURGE and PUSH methods.
Edited switch to allow fallthrough.
* Updated according to PR comments
* Removed dot imports
* Added additional test cases
* Fixed new test cases by using ^ and $ tokens in the regex
* Using new package name to be more idiomatic to go
* Tried to make a few things clearer
---
.../testing/api/v14/config/cachecfg/cachecfg.go | 260 +++++++++++++++++++++
.../api/v14/config/cachecfg/cachecfg_test.go | 222 ++++++++++++++++++
traffic_ops/testing/api/v14/config/common.go | 87 +++++++
traffic_ops/testing/api/v14/config/common_test.go | 110 +++++++++
traffic_ops/testing/api/v14/config/error.go | 79 +++++++
.../testing/api/v14/config/table_test_structs.go | 28 +++
6 files changed, 786 insertions(+)
diff --git a/traffic_ops/testing/api/v14/config/cachecfg/cachecfg.go b/traffic_ops/testing/api/v14/config/cachecfg/cachecfg.go
new file mode 100644
index 0000000..af632e5
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/cachecfg/cachecfg.go
@@ -0,0 +1,260 @@
+/*
+ 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.
+*/
+
+package cachecfg
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/apache/trafficcontrol/traffic_ops/testing/api/v14/config"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/test"
+ "github.com/go-ozzo/ozzo-validation/is"
+)
+
+// Parse takes a string presumed to be an ATS cache.config and validates that it is
+// syntatically correct.
+//
+// The general format of a cache config is three types of labels separated by spaces:
+//
+// primary_destination=value secondary_specifier=value action=value
+//
+// For a full description of how to format a cache config, refer to the ATS documentation
+// for the cache config:
+// https://docs.trafficserver.apache.org/en/latest/admin-guide/files/cache.config.en.html
+//
+func Parse(config string) test.Error {
+ lines := strings.Split(config, "\n")
+
+ if len(lines) == 1 {
+ return parseConfigRule(lines[0])
+ }
+
+ for i, ln := range lines {
+ err := parseConfigRule(ln)
+ if err != nil {
+ return err.Prepend("error on line %d: ", i+1)
+ }
+ }
+
+ return nil
+}
+
+func parsePrimaryDestinations(lhs string, rhs string) test.Error {
+
+ switch lhs {
+ case "dest_domain":
+ // dest_host is an alias for dest_domain
+ fallthrough
+ case "dest_host":
+ if err := is.Host.Validate(rhs); err != nil {
+ return config.ErrorContext.NewError(config.InvalidHost, `"%s" %v`, rhs, err)
+ }
+ case "dest_ip":
+ if err := is.IP.Validate(rhs); err != nil {
+ return config.ErrorContext.NewError(config.InvalidIP, `"%s" %v`, rhs, err)
+ }
+ case "host_regex":
+ fallthrough
+ case "url_regex":
+ // only makes sure the regex compiles, not that the regex generates anything valid
+ if _, err := regexp.Compile(rhs); err != nil {
+ return config.ErrorContext.NewError(config.InvalidRegex, "%v", err)
+ }
+ default:
+ return config.ErrorContext.NewError(config.InvalidLabel)
+ }
+
+ return nil
+}
+
+func parseSecondarySpecifiers(lhs string, rhs string) test.Error {
+
+ switch lhs {
+ case "port":
+ if err := is.Port.Validate(rhs); err != nil {
+ return config.ErrorContext.AddErrorCode(config.InvalidPort, err)
+ }
+ case "scheme":
+ if rhs != "http" && rhs != "https" {
+ return config.ErrorContext.NewError(config.InvalidHTTPScheme)
+ }
+ case "prefix":
+ // no clear validation to perform
+ case "suffix":
+ // examples: gif jpeg
+ // no clear validation to perform
+ case "method":
+ // assuming all methods are valid
+ // see RFC 2616-9 for list of all methods
+ // PURGE and PUSH are specific to ATS
+ switch rhs {
+ case "get":
+ case "put":
+ case "post":
+ case "delete":
+ case "trace":
+ case "options":
+ case "head":
+ case "connect":
+ case "patch":
+ case "purge":
+ case "push":
+ default:
+ return config.ErrorContext.NewError(config.UnknownMethod, `unknown method "%v"`, rhs)
+ }
+
+ case "time":
+ if err := config.Validate24HrTimeRange(rhs); err != nil {
+ return config.ErrorContext.AddErrorCode(config.InvalidTimeRange24Hr, err)
+ }
+ case "src_ip":
+ if err := is.IP.Validate(rhs); err != nil {
+ return config.ErrorContext.AddErrorCode(config.InvalidIP, err)
+ }
+ case "internal":
+ if rhs != "true" && rhs != "false" {
+ return config.ErrorContext.NewError(config.InvalidBool)
+ }
+
+ default:
+ return config.ErrorContext.NewError(config.InvalidLabel)
+ }
+
+ return nil
+}
+
+func parseActions(lhs string, rhs string) test.Error {
+
+ switch lhs {
+ case "action":
+ switch rhs {
+ case "never-cache":
+ case "ignore-no-cache":
+ case "ignore-client-no-cache":
+ case "ignore-server-no-cache":
+ default:
+ return config.ErrorContext.NewError(config.InvalidAction)
+ }
+
+ case "cache-responses-to-cookies":
+ digit := rhs[0]
+ if digit < '0' || '4' > digit || len(rhs) > 1 {
+ return config.ErrorContext.NewError(config.InvalidCacheCookieResponse)
+ }
+
+ // All of these are time formats
+ case "pin-in-cache":
+ fallthrough
+ case "revalidate":
+ fallthrough
+ case "ttl-in-cache":
+ err := config.ValidateDHMSTimeFormat(rhs)
+ if err != nil {
+ return config.ErrorContext.AddErrorCode(config.InvalidTimeFormatDHMS, err)
+ }
+ default:
+ return config.ErrorContext.NewError(config.InvalidLabel)
+ }
+
+ return nil
+}
+
+func parseConfigRule(rule string) test.Error {
+
+ var err test.Error
+
+ rule = strings.TrimSpace(rule)
+ if rule == "" || strings.HasPrefix(rule, "#") {
+ return nil
+ }
+
+ assignments := strings.Fields(rule)
+ last := len(assignments) - 1
+ if last < 1 {
+ return config.ErrorContext.NewError(config.NotEnoughAssignments)
+ }
+
+ // no individual secondary specifier label can be used twice
+ count := map[string]int{
+ "port": 0,
+ "scheme": 0,
+ "prefix": 0,
+ "suffix": 0,
+ "method": 0,
+ "time": 0,
+ "src_ip": 0,
+ "internal": 0,
+ }
+
+ // neither the rhs or lhs can contain any whitespace
+ assignment := regexp.MustCompile(`([a-z_\-\d]+)=(\S+)`)
+
+ destination := false
+ action := false
+
+ for _, elem := range assignments {
+ match := assignment.FindStringSubmatch(strings.ToLower(elem))
+ if match == nil {
+ return config.ErrorContext.NewError(config.BadAssignmentMatch, `could not match assignment: "%v"`, elem)
+ }
+
+ err = parsePrimaryDestinations(match[1], match[2])
+ if err == nil {
+ if destination {
+ return config.ErrorContext.NewError(config.ExcessLabel, "too many primary destination labels")
+ } else {
+ destination = true
+ continue
+ }
+ }
+ if err.Code() != config.InvalidLabel {
+ return err.Prepend(`coult not parse primary destination from "%s": `, match[0])
+ }
+
+ err = parseSecondarySpecifiers(match[1], match[2])
+ if err == nil {
+ if count[match[1]]++; count[match[1]] == 2 {
+ return config.ErrorContext.NewError(config.ExcessLabel, `the label "%s" can only be used once per rule`, match[1])
+ }
+ continue
+ }
+ if err.Code() != config.InvalidLabel {
+ return err.Prepend(`could not parse secondary specifier from "%s": `, match[0])
+ }
+
+ err = parseActions(match[1], match[2])
+ if err == nil {
+ action = true
+ continue
+ }
+
+ if err.Code() == config.InvalidLabel {
+ return err
+ } else {
+ return err.Prepend(`could not parse action from "%s": `, match[0])
+ }
+
+ }
+
+ if !destination {
+ return config.ErrorContext.NewError(config.MissingLabel, "missing primary destination label")
+ }
+
+ if !action {
+ return config.ErrorContext.NewError(config.MissingLabel, "missing action lablel")
+ }
+
+ return nil
+}
diff --git a/traffic_ops/testing/api/v14/config/cachecfg/cachecfg_test.go b/traffic_ops/testing/api/v14/config/cachecfg/cachecfg_test.go
new file mode 100644
index 0000000..db684a9
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/cachecfg/cachecfg_test.go
@@ -0,0 +1,222 @@
+/*
+ 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.
+*/
+
+package cachecfg_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/apache/trafficcontrol/traffic_ops/testing/api/v14/config"
+ "github.com/apache/trafficcontrol/traffic_ops/testing/api/v14/config/cachecfg"
+)
+
+var commonNegativeTests = []config.NegativeTest{
+ {
+ "too few assignments",
+ "foo1=foo",
+ config.NotEnoughAssignments,
+ },
+ {
+ "empty assignment",
+ "dest_domain= foo1=foo",
+ config.BadAssignmentMatch,
+ },
+ {
+ "missing equals in assignment",
+ "dest_domain foo1=foo",
+ config.BadAssignmentMatch,
+ },
+ {
+ "more than one primary destination",
+ "dest_domain=example dest_domain=example",
+ config.ExcessLabel,
+ },
+ {
+ "using a single secondary specifier twice",
+ "dest_domain=example scheme=http scheme=https",
+ config.ExcessLabel,
+ },
+ {
+ "missing primary destination label",
+ "port=80 method=get time=08:00-14:00",
+ config.MissingLabel,
+ },
+ {
+ "unknown primary field",
+ "foo1=foo foo2=foo foo3=foo",
+ config.InvalidLabel,
+ },
+ {
+ "good first line, but bad second",
+ fmt.Sprintf("%s\n\n%s",
+ "dest_domain=example.com suffix=js revalidate=1d",
+ "foo1=foo foo2=foo foo3=foo"),
+ config.InvalidLabel,
+ },
+}
+
+var primaryDestinationNegativeTests = []config.NegativeTest{
+ {
+ "bad url regex",
+ "url_regex=(example.com/.* foo2=foo",
+ config.InvalidRegex,
+ },
+ {
+ "bad host regex",
+ "host_regex=(example.* foo2=foo",
+ config.InvalidRegex,
+ },
+ {
+ "bad hostname",
+ "dest_domain=my%20bad%20domain.com foo1=foo",
+ config.InvalidHost,
+ },
+ {
+ "bad ip",
+ "dest_ip=bad_ip foo1=foo",
+ config.InvalidIP,
+ },
+}
+
+var secondarySpecifierNegativeTests = []config.NegativeTest{
+ {
+ "bad port",
+ "dest_domain=example port=90009000",
+ config.InvalidPort,
+ },
+ {
+ "bad scheme",
+ "dest_domain=example scheme=httpz",
+ config.InvalidHTTPScheme,
+ },
+ {
+ "bad method",
+ "dest_domain=example method=xxx",
+ config.UnknownMethod,
+ },
+ {
+ "bad time range",
+ "dest_domain=example time=16:00",
+ config.InvalidTimeRange24Hr,
+ },
+ {
+ "bad src ip",
+ "dest_domain=example src_ip=bad_ip",
+ config.InvalidIP,
+ },
+ {
+ "bad boolean value",
+ "dest_domain=example internal=xxx",
+ config.InvalidBool,
+ },
+}
+
+var actionNegativeTests = []config.NegativeTest{
+ {
+ "bad action value",
+ "dest_domain=example action=xxx",
+ config.InvalidAction,
+ },
+ {
+ "bad cache-responses-to-cookies",
+ "dest_domain=example cache-responses-to-cookies=42",
+ config.InvalidCacheCookieResponse,
+ },
+ {
+ "bad time format",
+ "dest_domain=example pin-in-cache=xxx",
+ config.InvalidTimeFormatDHMS,
+ },
+ {
+ "missing action label",
+ "dest_domain=example scheme=http",
+ config.MissingLabel,
+ },
+}
+
+var positiveTests = []config.PositiveTest{
+ {
+ "empty config",
+ "",
+ },
+ {
+ "empty multiline config",
+ "\n",
+ },
+ {
+ "empty config with whitespace",
+ "\t ",
+ },
+ {
+ "normal config returned from traffic ops",
+ fmt.Sprintf("%s\n%s",
+ "# DO NOT EDIT - Generated for ATS_EDGE_TIER_CACHE by Traffic Ops on Fri Feb 15 22:01:53 UTC 2019",
+ "dest_domain=origin.infra.ciab.test port=80 scheme=http action=never-cache"),
+ },
+ {
+ "tab-delimitted config",
+ "dest_domain=origin.infra.ciab.test\tport=80\tscheme=http\taction=never-cache",
+ },
+ {
+ "multi-space delimitted config",
+ "dest_domain=origin.infra.ciab.test port=80 scheme=http action=never-cache",
+ },
+ {
+ "multiline config with empty line",
+ fmt.Sprintf("%s\n\n%s",
+ "dest_domain=example.com suffix=js revalidate=1d",
+ "dest_domain=example.com prefix=foo suffix=js revalidate=7d"),
+ },
+ {
+ "many empty lines in config",
+ fmt.Sprintf("\n%s\n\n%s",
+ "dest_domain=example.com suffix=js revalidate=1d\n",
+ "dest_domain=example.com prefix=foo suffix=js revalidate=7d\n"),
+ },
+}
+
+func negativeTestDriver(tests []config.NegativeTest, t *testing.T) {
+ for _, test := range tests {
+ actual := cachecfg.Parse(test.Config)
+ if actual == nil || actual.Code() != test.Expected {
+ t.Errorf(`
+ config: "%v"
+ returned error: "%v"
+ error should be related to: %v`, test.Config, actual, test.Description)
+ }
+ }
+}
+
+func TestCacheConfig(t *testing.T) {
+
+ // Negative Tests
+ negativeTestDriver(commonNegativeTests, t)
+ negativeTestDriver(primaryDestinationNegativeTests, t)
+ negativeTestDriver(secondarySpecifierNegativeTests, t)
+ negativeTestDriver(actionNegativeTests, t)
+
+ // Positive Tests
+ for _, test := range positiveTests {
+ actual := cachecfg.Parse(test.Config)
+ if actual != nil {
+ t.Errorf(`
+ config: "%v"
+ returned error: "%v"
+ error should be nil
+ description: %v`, test.Config, actual, test.Description)
+ }
+
+ }
+}
diff --git a/traffic_ops/testing/api/v14/config/common.go b/traffic_ops/testing/api/v14/config/common.go
new file mode 100644
index 0000000..b21931e
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/common.go
@@ -0,0 +1,87 @@
+/*
+ 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.
+*/
+
+package config
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "time"
+)
+
+// militaryTimeFmt defines the 24hr format
+// see https://golang.org/pkg/time/#Parse
+const militaryTimeFmt = "15:04"
+
+// Validate24HrTimeRange determines whether the provided
+// string fits in a format such as "08:00-16:00".
+func Validate24HrTimeRange(rng string) error {
+ rangeFormat := regexp.MustCompile(`^(\S+)-(\S+)$`)
+ match := rangeFormat.FindStringSubmatch(rng)
+ if match == nil {
+ return fmt.Errorf("string %v is not a range", rng)
+ }
+
+ t1, err := time.Parse(militaryTimeFmt, match[1])
+ if err != nil {
+ return fmt.Errorf("time range must be a 24Hr format")
+ }
+
+ t2, err := time.Parse(militaryTimeFmt, match[2])
+ if err != nil {
+ return fmt.Errorf("second time range must be a 24Hr format")
+ }
+
+ if t1.After(t2) {
+ return fmt.Errorf("first time should be smaller than the second")
+ }
+
+ return nil
+}
+
+// ValidateDHMSTimeFormat determines whether the provided
+// string fits in a format such as "1d8h", where the valid
+// units are days, hours, minutes, and seconds.
+func ValidateDHMSTimeFormat(time string) error {
+
+ if time == "" {
+ return fmt.Errorf("time string cannot be empty")
+ }
+
+ dhms := regexp.MustCompile(`^(\d+)([dhms])(\S*)$`)
+ match := dhms.FindStringSubmatch(time)
+
+ if match == nil {
+ return fmt.Errorf("invalid time format")
+ }
+
+ var count = map[string]int{
+ "d": 0,
+ "h": 0,
+ "m": 0,
+ "s": 0,
+ }
+ for match != nil {
+ if _, err := strconv.Atoi(match[1]); err != nil {
+ return err
+ }
+ if count[match[2]]++; count[match[2]] == 2 {
+ return fmt.Errorf("%s unit specified multiple times", match[2])
+ }
+ match = dhms.FindStringSubmatch(match[3])
+ }
+
+ return nil
+}
diff --git a/traffic_ops/testing/api/v14/config/common_test.go b/traffic_ops/testing/api/v14/config/common_test.go
new file mode 100644
index 0000000..c03947d
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/common_test.go
@@ -0,0 +1,110 @@
+/*
+ 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.
+*/
+
+package config
+
+import "testing"
+
+func Test24HrTimeRange(t *testing.T) {
+
+ var tests = []struct {
+ time string
+ ok bool
+ }{
+ {"15:04", false},
+ {"16:00-08:00", false},
+ {"16:00-8:00", false},
+ {"xxx-8:00", false},
+ {"8:00-xxx", false},
+ {"8:00-16:00", true},
+ {"08:00-16:00", true},
+ {"", false},
+ {" ", false},
+ {"-", false},
+ {"--", false},
+ {" - ", false},
+ {"asdf", false},
+ {"08:00-asdf", false},
+ {"asdf-08:00", false},
+ {"-08:00", false},
+ {"08:00-", false},
+ {"08-09:00", false},
+ {"09:00-10", false},
+ {"09:00-10:0", false},
+ {"9:00-10:0", false},
+ {"08:00-32:00", false},
+ {"32:00-33:00", false},
+ {"08:00--16:00", false},
+ {"08:00-16:00-", false},
+ {"08:00-16:00-17:00", false},
+ {"08:00-09:00 16:00-17:00", false},
+ {"foo 16:00-17:00", false},
+ {"16:00-17:00 foo", false},
+ }
+
+ for _, test := range tests {
+ if err := Validate24HrTimeRange(test.time); (err == nil) != test.ok {
+ if test.ok {
+ t.Errorf(`
+ test should have passed
+ time: %v
+ err: %v`, test.time, err)
+ } else {
+ t.Errorf(`
+ test should not have passed
+ time: %v`, test.time)
+ }
+ }
+ }
+
+}
+
+func TestDHMSTimeFormat(t *testing.T) {
+
+ var tests = []struct {
+ time string
+ ok bool
+ }{
+ {"1s", true},
+ {"1m", true},
+ {"1h", true},
+ {"1d", true},
+ {"1d2h", true},
+ {"1m2s", true},
+ {"1d2h3m4s", true},
+ {"1s2h3m4d", true},
+ {"10000000000000000000000s", false},
+ {"1s2s", false},
+ {"1x", false},
+ {"1", false},
+ {"x", false},
+ {"", false},
+ }
+
+ for _, test := range tests {
+ if err := ValidateDHMSTimeFormat(test.time); (err == nil) != test.ok {
+ if test.ok {
+ t.Errorf(`
+ test should have passed
+ time: %v
+ err: %v`, test.time, err)
+ } else {
+ t.Errorf(`
+ test should not have passed
+ time: %v`, test.time)
+ }
+ }
+ }
+
+}
diff --git a/traffic_ops/testing/api/v14/config/error.go b/traffic_ops/testing/api/v14/config/error.go
new file mode 100644
index 0000000..708c821
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/error.go
@@ -0,0 +1,79 @@
+/*
+ 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.
+*/
+
+package config
+
+import "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/test"
+
+// Error codes:
+const (
+ BadAssignmentMatch = iota + 1
+ NotEnoughAssignments
+ ExcessLabel
+ InvalidLabel
+ MissingLabel
+ InvalidAction
+ InvalidBool
+ InvalidCacheCookieResponse
+ InvalidHTTPScheme
+ InvalidHost
+ InvalidIP
+ UnknownMethod
+ InvalidPort
+ InvalidRegex
+ InvalidTimeFormatDHMS
+ InvalidTimeRange24Hr
+)
+
+// ErrorContext contains the error codes mentioned above.
+// Any error made must have one of those error codes.
+var ErrorContext *test.ErrorContext
+
+func init() {
+ iterableErrorCodes := []uint{
+ BadAssignmentMatch,
+ NotEnoughAssignments,
+ ExcessLabel,
+ InvalidLabel,
+ MissingLabel,
+ InvalidAction,
+ InvalidBool,
+ InvalidCacheCookieResponse,
+ InvalidHTTPScheme,
+ InvalidHost,
+ InvalidIP,
+ UnknownMethod,
+ InvalidPort,
+ InvalidRegex,
+ InvalidTimeFormatDHMS,
+ InvalidTimeRange24Hr,
+ }
+
+ ErrorContext = test.NewErrorContext("cache config", iterableErrorCodes)
+
+ ErrorContext.SetDefaultMessageForCode(InvalidLabel,
+ "invalid label")
+ ErrorContext.SetDefaultMessageForCode(InvalidAction,
+ "invalid action")
+ ErrorContext.SetDefaultMessageForCode(NotEnoughAssignments,
+ "not enough assignments in rule")
+ ErrorContext.SetDefaultMessageForCode(InvalidHTTPScheme,
+ "invalid scheme (must be either http or https)")
+ ErrorContext.SetDefaultMessageForCode(InvalidBool,
+ "label must have a value of 'true' or 'false'")
+ ErrorContext.SetDefaultMessageForCode(InvalidCacheCookieResponse,
+ "Value for cache-responses-to-cookies must be an integer in the range 0..4")
+
+ ErrorContext.TurnPanicOn()
+}
diff --git a/traffic_ops/testing/api/v14/config/table_test_structs.go b/traffic_ops/testing/api/v14/config/table_test_structs.go
new file mode 100644
index 0000000..8ea3a0a
--- /dev/null
+++ b/traffic_ops/testing/api/v14/config/table_test_structs.go
@@ -0,0 +1,28 @@
+/*
+ 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.
+*/
+
+package config
+
+// NegativeTest is a struct for table tests.
+type NegativeTest struct {
+ Description string
+ Config string
+ Expected int
+}
+
+// PositiveTest is a struct for table tests.
+type PositiveTest struct {
+ Description string
+ Config string
+}