b128769e19
* Add merge helper for string maps * structs: add statuses, MaxClientDisconnect, and helper funcs * taintedNodes: Include disconnected nodes * upsertAllocsImpl: don't use existing ClientStatus when upserting unknown * allocSet: update filterByTainted and add delayByMaxClientDisconnect * allocReconciler: support disconnecting and reconnecting allocs * GenericScheduler: upsert unknown and queue reconnecting Co-authored-by: Tim Gross <tgross@hashicorp.com>
480 lines
12 KiB
Go
480 lines
12 KiB
Go
package helper
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSliceStringIsSubset(t *testing.T) {
|
|
l := []string{"a", "b", "c"}
|
|
s := []string{"d"}
|
|
|
|
sub, offending := SliceStringIsSubset(l, l[:1])
|
|
if !sub || len(offending) != 0 {
|
|
t.Fatalf("bad %v %v", sub, offending)
|
|
}
|
|
|
|
sub, offending = SliceStringIsSubset(l, s)
|
|
if sub || len(offending) == 0 || offending[0] != "d" {
|
|
t.Fatalf("bad %v %v", sub, offending)
|
|
}
|
|
}
|
|
|
|
func TestSliceStringContains(t *testing.T) {
|
|
list := []string{"a", "b", "c"}
|
|
require.True(t, SliceStringContains(list, "a"))
|
|
require.True(t, SliceStringContains(list, "b"))
|
|
require.True(t, SliceStringContains(list, "c"))
|
|
require.False(t, SliceStringContains(list, "d"))
|
|
}
|
|
|
|
func TestSliceStringHasPrefix(t *testing.T) {
|
|
list := []string{"alpha", "bravo", "charlie", "definitely", "most definitely"}
|
|
// At least one string in the slice above starts with the following test prefix strings
|
|
require.True(t, SliceStringHasPrefix(list, "a"))
|
|
require.True(t, SliceStringHasPrefix(list, "b"))
|
|
require.True(t, SliceStringHasPrefix(list, "c"))
|
|
require.True(t, SliceStringHasPrefix(list, "d"))
|
|
require.True(t, SliceStringHasPrefix(list, "mos"))
|
|
require.True(t, SliceStringHasPrefix(list, "def"))
|
|
require.False(t, SliceStringHasPrefix(list, "delta"))
|
|
|
|
}
|
|
|
|
func TestStringHasPrefixInSlice(t *testing.T) {
|
|
prefixes := []string{"a", "b", "c", "definitely", "most definitely"}
|
|
// The following strings all start with at least one prefix in the slice above
|
|
require.True(t, StringHasPrefixInSlice("alpha", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("bravo", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("charlie", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("definitely", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("most definitely", prefixes))
|
|
|
|
require.False(t, StringHasPrefixInSlice("mos", prefixes))
|
|
require.False(t, StringHasPrefixInSlice("def", prefixes))
|
|
require.False(t, StringHasPrefixInSlice("delta", prefixes))
|
|
|
|
}
|
|
|
|
func TestCompareTimePtrs(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
a := (*time.Duration)(nil)
|
|
b := (*time.Duration)(nil)
|
|
require.True(t, CompareTimePtrs(a, b))
|
|
c := TimeToPtr(3 * time.Second)
|
|
require.False(t, CompareTimePtrs(a, c))
|
|
require.False(t, CompareTimePtrs(c, a))
|
|
})
|
|
|
|
t.Run("not nil", func(t *testing.T) {
|
|
a := TimeToPtr(1 * time.Second)
|
|
b := TimeToPtr(1 * time.Second)
|
|
c := TimeToPtr(2 * time.Second)
|
|
require.True(t, CompareTimePtrs(a, b))
|
|
require.False(t, CompareTimePtrs(a, c))
|
|
})
|
|
}
|
|
|
|
func TestCompareSliceSetString(t *testing.T) {
|
|
cases := []struct {
|
|
A []string
|
|
B []string
|
|
Result bool
|
|
}{
|
|
{
|
|
A: []string{},
|
|
B: []string{},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{},
|
|
B: []string{"a"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a"},
|
|
B: []string{"a"},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{"a"},
|
|
B: []string{"b"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"b"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"a"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"a", "b"},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"b", "a"},
|
|
Result: true,
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
tc := tc
|
|
t.Run(fmt.Sprintf("case-%da", i), func(t *testing.T) {
|
|
if res := CompareSliceSetString(tc.A, tc.B); res != tc.Result {
|
|
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
|
tc.Result, tc.A, tc.B, res,
|
|
)
|
|
}
|
|
})
|
|
|
|
// Function is commutative so compare B and A
|
|
t.Run(fmt.Sprintf("case-%db", i), func(t *testing.T) {
|
|
if res := CompareSliceSetString(tc.B, tc.A); res != tc.Result {
|
|
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
|
tc.Result, tc.B, tc.A, res,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMapStringStringSliceValueSet(t *testing.T) {
|
|
m := map[string][]string{
|
|
"foo": {"1", "2"},
|
|
"bar": {"3"},
|
|
"baz": nil,
|
|
}
|
|
|
|
act := MapStringStringSliceValueSet(m)
|
|
exp := []string{"1", "2", "3"}
|
|
sort.Strings(act)
|
|
if !reflect.DeepEqual(act, exp) {
|
|
t.Fatalf("Bad; got %v; want %v", act, exp)
|
|
}
|
|
}
|
|
|
|
func TestSetToSliceString(t *testing.T) {
|
|
set := map[string]struct{}{
|
|
"foo": {},
|
|
"bar": {},
|
|
"baz": {},
|
|
}
|
|
expect := []string{"foo", "bar", "baz"}
|
|
got := SetToSliceString(set)
|
|
require.ElementsMatch(t, expect, got)
|
|
}
|
|
|
|
func TestCopyMapStringSliceString(t *testing.T) {
|
|
m := map[string][]string{
|
|
"x": {"a", "b", "c"},
|
|
"y": {"1", "2", "3"},
|
|
"z": nil,
|
|
}
|
|
|
|
c := CopyMapStringSliceString(m)
|
|
if !reflect.DeepEqual(c, m) {
|
|
t.Fatalf("%#v != %#v", m, c)
|
|
}
|
|
|
|
c["x"][1] = "---"
|
|
if reflect.DeepEqual(c, m) {
|
|
t.Fatalf("Shared slices: %#v == %#v", m["x"], c["x"])
|
|
}
|
|
}
|
|
|
|
func TestCopyMapSliceInterface(t *testing.T) {
|
|
m := map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": 2,
|
|
}
|
|
|
|
c := CopyMapStringInterface(m)
|
|
require.True(t, reflect.DeepEqual(m, c))
|
|
|
|
m["foo"] = "zzz"
|
|
require.False(t, reflect.DeepEqual(m, c))
|
|
}
|
|
|
|
func TestMergeMapStringString(t *testing.T) {
|
|
type testCase struct {
|
|
map1 map[string]string
|
|
map2 map[string]string
|
|
expected map[string]string
|
|
}
|
|
|
|
cases := []testCase{
|
|
{map[string]string{"foo": "bar"}, map[string]string{"baz": "qux"}, map[string]string{"foo": "bar", "baz": "qux"}},
|
|
{map[string]string{"foo": "bar"}, nil, map[string]string{"foo": "bar"}},
|
|
{nil, map[string]string{"baz": "qux"}, map[string]string{"baz": "qux"}},
|
|
{nil, nil, map[string]string{}},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if output := MergeMapStringString(c.map1, c.map2); !CompareMapStringString(output, c.expected) {
|
|
t.Errorf("MergeMapStringString(%q, %q) -> %q != %q", c.map1, c.map2, output, c.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCleanEnvVar(t *testing.T) {
|
|
type testCase struct {
|
|
input string
|
|
expected string
|
|
}
|
|
cases := []testCase{
|
|
{"asdf", "asdf"},
|
|
{"ASDF", "ASDF"},
|
|
{"0sdf", "_sdf"},
|
|
{"asd0", "asd0"},
|
|
{"_asd", "_asd"},
|
|
{"-asd", "_asd"},
|
|
{"asd.fgh", "asd.fgh"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A______________________________Z"},
|
|
{"A\U0001f4a9Z", "A____Z"},
|
|
}
|
|
for _, c := range cases {
|
|
if output := CleanEnvVar(c.input, '_'); output != c.expected {
|
|
t.Errorf("CleanEnvVar(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkCleanEnvVar(b *testing.B) {
|
|
in := "NOMAD_ADDR_redis-cache"
|
|
replacement := byte('_')
|
|
b.SetBytes(int64(len(in)))
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
CleanEnvVar(in, replacement)
|
|
}
|
|
}
|
|
|
|
type testCase struct {
|
|
input string
|
|
expected string
|
|
}
|
|
|
|
func commonCleanFilenameCases() (cases []testCase) {
|
|
// Common set of test cases for all 3 TestCleanFilenameX functions
|
|
cases = []testCase{
|
|
{"asdf", "asdf"},
|
|
{"ASDF", "ASDF"},
|
|
{"0sdf", "0sdf"},
|
|
{"asd0", "asd0"},
|
|
{"_asd", "_asd"},
|
|
{"-asd", "-asd"},
|
|
{"asd.fgh", "asd.fgh"},
|
|
{"Linux/Forbidden", "Linux_Forbidden"},
|
|
{"Windows<>:\"/\\|?*Forbidden", "Windows_________Forbidden"},
|
|
{`Windows<>:"/\|?*Forbidden_StringLiteral`, "Windows_________Forbidden_StringLiteral"},
|
|
}
|
|
return cases
|
|
}
|
|
|
|
func TestCleanFilename(t *testing.T) {
|
|
cases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A💩Z"}, // CleanFilename allows unicode
|
|
{"A💩Z", "A💩Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range cases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilename(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilename(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanFilenameASCIIOnly(t *testing.T) {
|
|
ASCIIOnlyCases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A_Z"}, // CleanFilenameASCIIOnly does not allow unicode
|
|
{"A💩Z", "A_Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range ASCIIOnlyCases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilenameASCIIOnly(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilenameASCIIOnly(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanFilenameStrict(t *testing.T) {
|
|
strictCases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A💩Z"}, // CleanFilenameStrict allows unicode
|
|
{"A💩Z", "A💩Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A_!___%^______-_{}_____________Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range strictCases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilenameStrict(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilenameStrict(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckNamespaceScope(t *testing.T) {
|
|
cases := []struct {
|
|
desc string
|
|
provided string
|
|
requested []string
|
|
offending []string
|
|
}{
|
|
{
|
|
desc: "root ns requesting namespace",
|
|
provided: "",
|
|
requested: []string{"engineering"},
|
|
},
|
|
{
|
|
desc: "matching parent ns with child",
|
|
provided: "engineering",
|
|
requested: []string{"engineering", "engineering/sub-team"},
|
|
},
|
|
{
|
|
desc: "mismatch ns",
|
|
provided: "engineering",
|
|
requested: []string{"finance", "engineering/sub-team", "eng"},
|
|
offending: []string{"finance", "eng"},
|
|
},
|
|
{
|
|
desc: "mismatch child",
|
|
provided: "engineering/sub-team",
|
|
requested: []string{"engineering/new-team", "engineering/sub-team", "engineering/sub-team/child"},
|
|
offending: []string{"engineering/new-team"},
|
|
},
|
|
{
|
|
desc: "matching prefix",
|
|
provided: "engineering",
|
|
requested: []string{"engineering/new-team", "engineering/new-team/sub-team"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
offending := CheckNamespaceScope(tc.provided, tc.requested)
|
|
require.Equal(t, offending, tc.offending)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPathEscapesSandbox(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
dir string
|
|
expected bool
|
|
}{
|
|
{
|
|
// this is the ${NOMAD_SECRETS_DIR} case
|
|
name: "ok joined absolute path inside sandbox",
|
|
path: filepath.Join("/alloc", "/secrets"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail unjoined absolute path outside sandbox",
|
|
path: "/secrets",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ok joined relative path inside sandbox",
|
|
path: filepath.Join("/alloc", "./safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail unjoined relative path outside sandbox",
|
|
path: "./safe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ok relative path traversal constrained to sandbox",
|
|
path: filepath.Join("/alloc", "../../alloc/safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ok unjoined absolute path traversal constrained to sandbox",
|
|
path: filepath.Join("/alloc", "/../alloc/safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ok unjoined absolute path traversal constrained to sandbox",
|
|
path: "/../alloc/safe",
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail joined relative path traverses outside sandbox",
|
|
path: filepath.Join("/alloc", "../../../unsafe"),
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail unjoined relative path traverses outside sandbox",
|
|
path: "../../../unsafe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail joined absolute path tries to transverse outside sandbox",
|
|
path: filepath.Join("/alloc", "/alloc/../../unsafe"),
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail unjoined absolute path tries to transverse outside sandbox",
|
|
path: "/alloc/../../unsafe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
caseMsg := fmt.Sprintf("path: %v\ndir: %v", tc.path, tc.dir)
|
|
escapes := PathEscapesSandbox(tc.dir, tc.path)
|
|
require.Equal(t, tc.expected, escapes, caseMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_NewSafeTimer(t *testing.T) {
|
|
t.Run("zero", func(t *testing.T) {
|
|
timer, stop := NewSafeTimer(0)
|
|
defer stop()
|
|
<-timer.C
|
|
})
|
|
|
|
t.Run("positive", func(t *testing.T) {
|
|
timer, stop := NewSafeTimer(1)
|
|
defer stop()
|
|
<-timer.C
|
|
})
|
|
}
|