mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1493 lines
38 KiB
1493 lines
38 KiB
package structs |
|
|
|
import ( |
|
"fmt" |
|
"strings" |
|
"testing" |
|
"time" |
|
|
|
"github.com/hashicorp/go-uuid" |
|
"github.com/stretchr/testify/assert" |
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/hashicorp/consul/sdk/testutil" |
|
) |
|
|
|
func generateUUID() (ret string) { |
|
var err error |
|
if ret, err = uuid.GenerateUUID(); err != nil { |
|
panic(fmt.Sprintf("Unable to generate a UUID, %v", err)) |
|
} |
|
return ret |
|
} |
|
|
|
func TestServiceIntentionsConfigEntry(t *testing.T) { |
|
var ( |
|
testLocation = time.FixedZone("UTC-8", -8*60*60) |
|
|
|
testTimeA = time.Date(1955, 11, 5, 6, 15, 0, 0, testLocation) |
|
testTimeB = time.Date(1985, 10, 26, 1, 35, 0, 0, testLocation) |
|
testTimeC = time.Date(2015, 10, 21, 16, 29, 0, 0, testLocation) |
|
) |
|
|
|
type testcase struct { |
|
entry *ServiceIntentionsConfigEntry |
|
legacy bool |
|
normalizeErr string |
|
validateErr string |
|
// check is called between normalize and validate |
|
check func(t *testing.T, entry *ServiceIntentionsConfigEntry) |
|
} |
|
|
|
legacyIDs := []string{ |
|
generateUUID(), |
|
generateUUID(), |
|
generateUUID(), |
|
} |
|
|
|
defaultMeta := DefaultEnterpriseMetaInDefaultPartition() |
|
|
|
fooName := NewServiceName("foo", defaultMeta) |
|
|
|
cases := map[string]testcase{ |
|
"nil": { |
|
entry: nil, |
|
normalizeErr: "config entry is nil", |
|
}, |
|
"no name": { |
|
entry: &ServiceIntentionsConfigEntry{}, |
|
validateErr: "Name is required", |
|
}, |
|
"dest name has partial wildcard": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test*", |
|
}, |
|
validateErr: "Name: wildcard character '*' cannot be used with partial values", |
|
}, |
|
"empty": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
}, |
|
validateErr: "At least one source is required", |
|
}, |
|
"source specified twice": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionDeny, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[1] defines "` + fooName.String() + `" more than once`, |
|
}, |
|
"no source name": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Name is required`, |
|
}, |
|
"source name has partial wildcard": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo*", |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Name: wildcard character '*' cannot be used with partial values`, |
|
}, |
|
"description too long": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
Description: strings.Repeat("x", 513), |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Description exceeds maximum length 512`, |
|
}, |
|
"config entry meta not allowed on legacy writes": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
}, |
|
}, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
}, |
|
validateErr: `Meta must be omitted for legacy intention writes`, |
|
}, |
|
"config entry meta too many keys": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
Meta: makeStringMap(65, 5, 5), |
|
}, |
|
validateErr: `Meta exceeds maximum element count 64`, |
|
}, |
|
"config entry meta key too large": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
Meta: makeStringMap(64, 129, 5), |
|
}, |
|
validateErr: `exceeds maximum length 128`, |
|
}, |
|
"config entry meta value too large": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
Meta: makeStringMap(64, 128, 513), |
|
}, |
|
validateErr: `exceeds maximum length 512`, |
|
}, |
|
"config entry meta value just big enough": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
Meta: makeStringMap(64, 128, 512), |
|
}, |
|
}, |
|
"legacy meta not allowed": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
Description: strings.Repeat("x", 512), |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
LegacyMeta: map[string]string{ // stray Meta will be dropped |
|
"old": "data", |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: "Sources[0].LegacyMeta must be omitted", |
|
}, |
|
"legacy meta too many keys": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
LegacyMeta: makeStringMap(65, 5, 5), |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Meta exceeds maximum element count 64`, |
|
}, |
|
"legacy meta key too large": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
LegacyMeta: makeStringMap(64, 129, 5), |
|
}, |
|
}, |
|
}, |
|
validateErr: `exceeds maximum length 128`, |
|
}, |
|
"legacy meta value too large": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
LegacyMeta: makeStringMap(64, 128, 513), |
|
}, |
|
}, |
|
}, |
|
validateErr: `exceeds maximum length 512`, |
|
}, |
|
"legacy meta value just big enough": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
LegacyMeta: makeStringMap(64, 128, 512), |
|
}, |
|
}, |
|
}, |
|
}, |
|
"legacy ID is required in legacy mode": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
Description: strings.Repeat("x", 512), |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
}, |
|
}, |
|
}, |
|
validateErr: "Sources[0].LegacyID must be set", |
|
}, |
|
"action required for L4": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`, |
|
}, |
|
"action must be allow or deny for L4": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: "blah", |
|
Description: strings.Repeat("x", 512), |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Action must be set to 'allow' or 'deny'`, |
|
}, |
|
"action must not be set for L7": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{PathExact: "/"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Action must be omitted if Permissions are specified`, |
|
}, |
|
"permission action must be set": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
HTTP: &IntentionHTTPPermission{PathExact: "/"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`, |
|
}, |
|
"permission action must allow or deny": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: "blah", |
|
HTTP: &IntentionHTTPPermission{PathExact: "/"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].Action must be set to 'allow' or 'deny'`, |
|
}, |
|
"permission missing http": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP is required`, |
|
}, |
|
"permission has too many path components (1)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathExact: "/", |
|
PathPrefix: "/a", |
|
// PathRegex: "/b", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`, |
|
}, |
|
"permission has too many path components (2)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathExact: "/", |
|
// PathPrefix: "/a", |
|
PathRegex: "/b", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`, |
|
}, |
|
"permission has too many path components (3)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
// PathExact: "/", |
|
PathPrefix: "/a", |
|
PathRegex: "/b", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`, |
|
}, |
|
"permission has too many path components (4)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathExact: "/", |
|
PathPrefix: "/a", |
|
PathRegex: "/b", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should only contain at most one of PathExact, PathPrefix, or PathRegex`, |
|
}, |
|
"permission has invalid path exact": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathExact: "x", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.PathExact doesn't start with '/': "x"`, |
|
}, |
|
"permission has invalid path prefix": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathPrefix: "x", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.PathPrefix doesn't start with '/': "x"`, |
|
}, |
|
"permission header missing name": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Description: strings.Repeat("x", 512), |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{Exact: "foo"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] missing required Name field`, |
|
}, |
|
"permission header has too many parts (1)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Present: true, |
|
Exact: "foo", |
|
// Regex: "foo", |
|
// Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (2)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Present: true, |
|
// Exact: "foo", |
|
Regex: "foo", |
|
// Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (3)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Present: true, |
|
// Exact: "foo", |
|
// Regex: "foo", |
|
Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (4)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Present: true, |
|
// Exact: "foo", |
|
// Regex: "foo", |
|
// Prefix: "foo", |
|
Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (5)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
Exact: "foo", |
|
Regex: "foo", |
|
// Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (6)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
Exact: "foo", |
|
// Regex: "foo", |
|
Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (7)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
Exact: "foo", |
|
// Regex: "foo", |
|
// Prefix: "foo", |
|
Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (8)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
// Exact: "foo", |
|
Regex: "foo", |
|
Prefix: "foo", |
|
// Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (9)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
// Exact: "foo", |
|
Regex: "foo", |
|
// Prefix: "foo", |
|
Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (10)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
// Present: true, |
|
// Exact: "foo", |
|
// Regex: "foo", |
|
Prefix: "foo", |
|
Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission header has too many parts (11)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Present: true, |
|
Exact: "foo", |
|
Regex: "foo", |
|
Prefix: "foo", |
|
Suffix: "foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Header[0] should only contain one of Present, Exact, Prefix, Suffix, or Regex`, |
|
}, |
|
"permission invalid method": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Methods: []string{"YOINK"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains an invalid method "YOINK"`, |
|
}, |
|
"permission repeated method": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Methods: []string{"POST", "PUT", "POST"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP.Methods contains "POST" more than once`, |
|
}, |
|
"permission should not be empty (1)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
Header: []IntentionHTTPHeaderPermission{}, |
|
Methods: []string{}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`, |
|
}, |
|
"permission should not be empty (2)": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions[0].HTTP should not be empty`, |
|
}, |
|
"permission kitchen sink": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathPrefix: "/foo", |
|
Header: []IntentionHTTPHeaderPermission{ |
|
{ |
|
Name: "x-abc", |
|
Exact: "foo", |
|
}, |
|
{ |
|
Name: "x-xyz", |
|
Present: true, |
|
Invert: true, |
|
}, |
|
}, |
|
Methods: []string{"POST", "PUT", "GET"}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
"permissions not allowed on wildcarded destinations": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
// TODO: ent |
|
Name: WildcardSpecifier, |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionAllow, |
|
HTTP: &IntentionHTTPPermission{ |
|
PathPrefix: "/foo", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
validateErr: `Sources[0].Permissions cannot be specified on intentions with wildcarded destinations`, |
|
}, |
|
"L4 normalize": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], // stray ID will be dropped |
|
Name: WildcardSpecifier, |
|
Action: IntentionActionDeny, |
|
}, |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyCreateTime: &testTimeA, // stray times will be dropped |
|
LegacyUpdateTime: &testTimeA, |
|
}, |
|
{ |
|
Name: "bar", |
|
Action: IntentionActionDeny, |
|
}, |
|
}, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
"key2": "val2", |
|
}, |
|
}, |
|
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) { |
|
// Note the stable precedence sort has been applied here. |
|
assert.Equal(t, []*SourceIntention{ |
|
{ |
|
Name: "foo", |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionAllow, |
|
Precedence: 9, |
|
Type: IntentionSourceConsul, |
|
}, |
|
{ |
|
Name: "bar", |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionDeny, |
|
Precedence: 9, |
|
Type: IntentionSourceConsul, |
|
}, |
|
{ |
|
Name: WildcardSpecifier, |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionDeny, |
|
Precedence: 8, |
|
Type: IntentionSourceConsul, |
|
}, |
|
}, entry.Sources) |
|
assert.Equal(t, map[string]string{ |
|
"key1": "val1", |
|
"key2": "val2", |
|
}, entry.Meta) |
|
}, |
|
}, |
|
"L4 legacy normalize": { |
|
legacy: true, |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: WildcardSpecifier, |
|
Action: IntentionActionDeny, |
|
LegacyID: legacyIDs[0], |
|
LegacyCreateTime: &testTimeA, |
|
LegacyUpdateTime: &testTimeA, |
|
}, |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
LegacyID: legacyIDs[1], |
|
LegacyMeta: map[string]string{ |
|
"key1": "val1", |
|
"key2": "val2", |
|
}, |
|
LegacyCreateTime: &testTimeB, |
|
LegacyUpdateTime: &testTimeB, |
|
}, |
|
{ |
|
Name: "bar", |
|
Action: IntentionActionDeny, |
|
LegacyID: legacyIDs[2], |
|
LegacyCreateTime: &testTimeC, |
|
LegacyUpdateTime: &testTimeC, |
|
}, |
|
}, |
|
}, |
|
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) { |
|
require.Len(t, entry.Sources, 3) |
|
|
|
// assert.False(t, entry.Sources[0].LegacyCreateTime.IsZero()) |
|
// assert.False(t, entry.Sources[0].LegacyUpdateTime.IsZero()) |
|
// assert.False(t, entry.Sources[1].LegacyCreateTime.IsZero()) |
|
// assert.False(t, entry.Sources[1].LegacyUpdateTime.IsZero()) |
|
// assert.False(t, entry.Sources[2].LegacyCreateTime.IsZero()) |
|
// assert.False(t, entry.Sources[2].LegacyUpdateTime.IsZero()) |
|
|
|
assert.Equal(t, []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[1], |
|
Name: "foo", |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionAllow, |
|
Precedence: 9, |
|
Type: IntentionSourceConsul, |
|
LegacyMeta: map[string]string{ |
|
"key1": "val1", |
|
"key2": "val2", |
|
}, |
|
LegacyCreateTime: entry.Sources[0].LegacyCreateTime, |
|
LegacyUpdateTime: entry.Sources[0].LegacyUpdateTime, |
|
}, |
|
{ |
|
LegacyID: legacyIDs[2], |
|
Name: "bar", |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionDeny, |
|
Precedence: 9, |
|
Type: IntentionSourceConsul, |
|
LegacyMeta: map[string]string{}, |
|
LegacyCreateTime: entry.Sources[1].LegacyCreateTime, |
|
LegacyUpdateTime: entry.Sources[1].LegacyUpdateTime, |
|
}, |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Name: WildcardSpecifier, |
|
EnterpriseMeta: *defaultMeta, |
|
Action: IntentionActionDeny, |
|
Precedence: 8, |
|
Type: IntentionSourceConsul, |
|
LegacyMeta: map[string]string{}, |
|
LegacyCreateTime: entry.Sources[2].LegacyCreateTime, |
|
LegacyUpdateTime: entry.Sources[2].LegacyUpdateTime, |
|
}, |
|
}, entry.Sources) |
|
}, |
|
}, |
|
"L4 validate": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], // stray ID will be dropped |
|
Name: WildcardSpecifier, |
|
Action: IntentionActionDeny, |
|
}, |
|
{ |
|
Name: "foo", |
|
Action: IntentionActionAllow, |
|
}, |
|
{ |
|
Name: "bar", |
|
Action: IntentionActionDeny, |
|
}, |
|
}, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
"key2": "val2", |
|
}, |
|
}, |
|
}, |
|
"L7 normalize": { |
|
entry: &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: "test", |
|
Sources: []*SourceIntention{ |
|
{ |
|
Name: "bar", |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionDeny, |
|
HTTP: &IntentionHTTPPermission{ |
|
Methods: []string{ |
|
"get", "post", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
check: func(t *testing.T, entry *ServiceIntentionsConfigEntry) { |
|
assert.Equal(t, []*SourceIntention{ |
|
{ |
|
Name: "bar", |
|
EnterpriseMeta: *defaultMeta, |
|
Precedence: 9, |
|
Type: IntentionSourceConsul, |
|
Permissions: []*IntentionPermission{ |
|
{ |
|
Action: IntentionActionDeny, |
|
HTTP: &IntentionHTTPPermission{ |
|
Methods: []string{ |
|
"GET", "POST", |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, entry.Sources) |
|
}, |
|
}, |
|
} |
|
for name, tc := range cases { |
|
tc := tc |
|
t.Run(name, func(t *testing.T) { |
|
var err error |
|
if tc.legacy { |
|
err = tc.entry.LegacyNormalize() |
|
} else { |
|
err = tc.entry.Normalize() |
|
} |
|
if tc.normalizeErr != "" { |
|
// require.Error(t, err) |
|
// require.Contains(t, err.Error(), tc.normalizeErr) |
|
testutil.RequireErrorContains(t, err, tc.normalizeErr) |
|
return |
|
} |
|
require.NoError(t, err) |
|
|
|
if tc.check != nil { |
|
tc.check(t, tc.entry) |
|
} |
|
|
|
if tc.legacy { |
|
err = tc.entry.LegacyValidate() |
|
} else { |
|
err = tc.entry.Validate() |
|
} |
|
if tc.validateErr != "" { |
|
// require.Error(t, err) |
|
// require.Contains(t, err.Error(), tc.validateErr) |
|
testutil.RequireErrorContains(t, err, tc.validateErr) |
|
return |
|
} |
|
require.NoError(t, err) |
|
}) |
|
} |
|
} |
|
|
|
func makeStringMap(keys, keySize, valSize int) map[string]string { |
|
m := make(map[string]string) |
|
for i := 0; i < keys; i++ { |
|
base := fmt.Sprintf("%d:", i) |
|
if len(base) > keySize || len(base) > valSize { |
|
panic("makeStringMap called with incompatible inputs") |
|
} |
|
// this is not performant |
|
if keySize > valSize { |
|
base = strings.Repeat(base, keySize) |
|
} else { |
|
base = strings.Repeat(base, valSize) |
|
} |
|
|
|
m[base[0:keySize]] = base[0:valSize] |
|
} |
|
return m |
|
} |
|
|
|
func TestMigrateIntentions(t *testing.T) { |
|
type testcase struct { |
|
in Intentions |
|
expect []*ServiceIntentionsConfigEntry |
|
} |
|
|
|
legacyIDs := []string{ |
|
generateUUID(), |
|
generateUUID(), |
|
generateUUID(), |
|
} |
|
|
|
anyTime := time.Now().UTC() |
|
entMeta := DefaultEnterpriseMetaInDefaultPartition() |
|
|
|
cases := map[string]testcase{ |
|
"nil": {}, |
|
"one": { |
|
in: Intentions{ |
|
{ |
|
ID: legacyIDs[0], |
|
Description: "desc", |
|
SourceName: "foo", |
|
DestinationName: "bar", |
|
SourceType: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
Precedence: 9, |
|
CreatedAt: anyTime, |
|
UpdatedAt: anyTime, |
|
}, |
|
}, |
|
expect: []*ServiceIntentionsConfigEntry{ |
|
{ |
|
Kind: ServiceIntentions, |
|
Name: "bar", |
|
EnterpriseMeta: *entMeta, |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Description: "desc", |
|
Name: "foo", |
|
EnterpriseMeta: *entMeta, |
|
Type: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
LegacyMeta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
LegacyCreateTime: &anyTime, |
|
LegacyUpdateTime: &anyTime, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
"two in same": { |
|
in: Intentions{ |
|
{ |
|
ID: legacyIDs[0], |
|
Description: "desc", |
|
SourceName: "foo", |
|
DestinationName: "bar", |
|
SourceType: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
Precedence: 9, |
|
CreatedAt: anyTime, |
|
UpdatedAt: anyTime, |
|
}, |
|
{ |
|
ID: legacyIDs[1], |
|
Description: "desc2", |
|
SourceName: "*", |
|
DestinationName: "bar", |
|
SourceType: IntentionSourceConsul, |
|
Action: IntentionActionDeny, |
|
Meta: map[string]string{ |
|
"key2": "val2", |
|
}, |
|
Precedence: 9, |
|
CreatedAt: anyTime, |
|
UpdatedAt: anyTime, |
|
}, |
|
}, |
|
expect: []*ServiceIntentionsConfigEntry{ |
|
{ |
|
Kind: ServiceIntentions, |
|
Name: "bar", |
|
EnterpriseMeta: *entMeta, |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Description: "desc", |
|
Name: "foo", |
|
EnterpriseMeta: *entMeta, |
|
Type: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
LegacyMeta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
LegacyCreateTime: &anyTime, |
|
LegacyUpdateTime: &anyTime, |
|
}, |
|
{ |
|
LegacyID: legacyIDs[1], |
|
Description: "desc2", |
|
Name: "*", |
|
EnterpriseMeta: *entMeta, |
|
Type: IntentionSourceConsul, |
|
Action: IntentionActionDeny, |
|
LegacyMeta: map[string]string{ |
|
"key2": "val2", |
|
}, |
|
LegacyCreateTime: &anyTime, |
|
LegacyUpdateTime: &anyTime, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
"two in different": { |
|
in: Intentions{ |
|
{ |
|
ID: legacyIDs[0], |
|
Description: "desc", |
|
SourceName: "foo", |
|
DestinationName: "bar", |
|
SourceType: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
Meta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
Precedence: 9, |
|
CreatedAt: anyTime, |
|
UpdatedAt: anyTime, |
|
}, |
|
{ |
|
ID: legacyIDs[1], |
|
Description: "desc2", |
|
SourceName: "*", |
|
DestinationName: "bar2", |
|
SourceType: IntentionSourceConsul, |
|
Action: IntentionActionDeny, |
|
Meta: map[string]string{ |
|
"key2": "val2", |
|
}, |
|
Precedence: 9, |
|
CreatedAt: anyTime, |
|
UpdatedAt: anyTime, |
|
}, |
|
}, |
|
expect: []*ServiceIntentionsConfigEntry{ |
|
{ |
|
Kind: ServiceIntentions, |
|
Name: "bar", |
|
EnterpriseMeta: *entMeta, |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[0], |
|
Description: "desc", |
|
Name: "foo", |
|
EnterpriseMeta: *entMeta, |
|
Type: IntentionSourceConsul, |
|
Action: IntentionActionAllow, |
|
LegacyMeta: map[string]string{ |
|
"key1": "val1", |
|
}, |
|
LegacyCreateTime: &anyTime, |
|
LegacyUpdateTime: &anyTime, |
|
}, |
|
}, |
|
}, |
|
{ |
|
Kind: ServiceIntentions, |
|
Name: "bar2", |
|
EnterpriseMeta: *entMeta, |
|
Sources: []*SourceIntention{ |
|
{ |
|
LegacyID: legacyIDs[1], |
|
Description: "desc2", |
|
Name: "*", |
|
EnterpriseMeta: *entMeta, |
|
Type: IntentionSourceConsul, |
|
Action: IntentionActionDeny, |
|
LegacyMeta: map[string]string{ |
|
"key2": "val2", |
|
}, |
|
LegacyCreateTime: &anyTime, |
|
LegacyUpdateTime: &anyTime, |
|
}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
} |
|
|
|
for name, tc := range cases { |
|
tc := tc |
|
t.Run(name, func(t *testing.T) { |
|
got := MigrateIntentions(tc.in) |
|
require.Equal(t, tc.expect, got) |
|
}) |
|
} |
|
}
|
|
|