<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #48721 Part 2 of https://github.com/fleetdm/fleet/issues/43488 # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Database migrations - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Windows devices can now create and securely escrow managed local account passwords during enrollment. * Added Windows managed local account status and password availability to host details. * Device-reported setup errors are surfaced with helpful details. * Account creation is automatically requested when supported by the device, plan, and configuration. * **Bug Fixes** * Windows accounts are excluded from password rotation workflows. * Re-enrollment correctly triggers account creation when needed. * Passwords remain available when settings change after enrollment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
265 lines
6.0 KiB
Go
265 lines
6.0 KiB
Go
package str
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSplitAndTrim(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
testString string
|
|
delimiter string
|
|
removeEmpty bool
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "basic comma split",
|
|
testString: "a,b,c",
|
|
delimiter: ",",
|
|
removeEmpty: false,
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "trims whitespace",
|
|
testString: " a , b , c ",
|
|
delimiter: ",",
|
|
removeEmpty: false,
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "keeps empty parts when removeEmpty is false",
|
|
testString: "a,,b,,c",
|
|
delimiter: ",",
|
|
removeEmpty: false,
|
|
expected: []string{"a", "", "b", "", "c"},
|
|
},
|
|
{
|
|
name: "removes empty parts when removeEmpty is true",
|
|
testString: "a,,b,,c",
|
|
delimiter: ",",
|
|
removeEmpty: true,
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "removes whitespace-only parts when removeEmpty is true",
|
|
testString: "a, ,b, ,c",
|
|
delimiter: ",",
|
|
removeEmpty: true,
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
name: "empty string",
|
|
testString: "",
|
|
delimiter: ",",
|
|
removeEmpty: true,
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "empty string without removeEmpty",
|
|
testString: "",
|
|
delimiter: ",",
|
|
removeEmpty: false,
|
|
expected: []string{""},
|
|
},
|
|
{
|
|
name: "no delimiter found",
|
|
testString: "abc",
|
|
delimiter: ",",
|
|
removeEmpty: false,
|
|
expected: []string{"abc"},
|
|
},
|
|
{
|
|
name: "multi-char delimiter",
|
|
testString: "a::b::c",
|
|
delimiter: "::",
|
|
removeEmpty: false,
|
|
expected: []string{"a", "b", "c"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := SplitAndTrim(tt.testString, tt.delimiter, tt.removeEmpty)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseUintList(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []uint
|
|
}{
|
|
{
|
|
name: "empty string returns nil",
|
|
input: "",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "single value",
|
|
input: "42",
|
|
expected: []uint{42},
|
|
},
|
|
{
|
|
name: "multiple values",
|
|
input: "1,2,3",
|
|
expected: []uint{1, 2, 3},
|
|
},
|
|
{
|
|
name: "trims whitespace",
|
|
input: " 1 , 2 , 3 ",
|
|
expected: []uint{1, 2, 3},
|
|
},
|
|
{
|
|
name: "skips non-numeric values",
|
|
input: "1,abc,2,,3",
|
|
expected: []uint{1, 2, 3},
|
|
},
|
|
{
|
|
name: "skips negative values",
|
|
input: "1,-2,3",
|
|
expected: []uint{1, 3},
|
|
},
|
|
{
|
|
name: "all invalid returns empty slice",
|
|
input: "a,b,c",
|
|
expected: []uint{},
|
|
},
|
|
{
|
|
name: "zero is valid",
|
|
input: "0,1",
|
|
expected: []uint{0, 1},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ParseUintList(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncateErrorResponse(t *testing.T) {
|
|
t.Run("short string passes through unchanged", func(t *testing.T) {
|
|
require.Equal(t, "hello", TruncateErrorResponse("hello"))
|
|
})
|
|
|
|
t.Run("exactly at limit passes through unchanged", func(t *testing.T) {
|
|
s := strings.Repeat("x", MaxErrorResponseBytes)
|
|
result := TruncateErrorResponse(s)
|
|
require.Equal(t, s, result)
|
|
require.False(t, strings.HasSuffix(result, " [truncated]"))
|
|
})
|
|
|
|
t.Run("one byte over limit is truncated", func(t *testing.T) {
|
|
s := strings.Repeat("x", MaxErrorResponseBytes+1)
|
|
result := TruncateErrorResponse(s)
|
|
require.True(t, strings.HasSuffix(result, " [truncated]"))
|
|
require.LessOrEqual(t, len(result), MaxErrorResponseBytes+len(" [truncated]"))
|
|
})
|
|
|
|
t.Run("result is always valid UTF-8", func(t *testing.T) {
|
|
// Build a string that is over the limit and ends with a partial multi-byte rune
|
|
// at the cut point. U+1F600 (😀) encodes as 4 bytes; place it straddling the limit.
|
|
prefix := strings.Repeat("a", MaxErrorResponseBytes-1)
|
|
s := prefix + "😀" + strings.Repeat("b", 100)
|
|
result := TruncateErrorResponse(s)
|
|
assert.True(t, utf8.ValidString(result), "result must be valid UTF-8")
|
|
assert.True(t, strings.HasSuffix(result, " [truncated]"))
|
|
})
|
|
|
|
t.Run("empty string passes through unchanged", func(t *testing.T) {
|
|
require.Empty(t, TruncateErrorResponse(""))
|
|
})
|
|
}
|
|
|
|
func TestParseStringList(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "empty string returns nil",
|
|
input: "",
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "single value",
|
|
input: "foo",
|
|
expected: []string{"foo"},
|
|
},
|
|
{
|
|
name: "multiple values",
|
|
input: "foo,bar,baz",
|
|
expected: []string{"foo", "bar", "baz"},
|
|
},
|
|
{
|
|
name: "trims whitespace",
|
|
input: " foo , bar , baz ",
|
|
expected: []string{"foo", "bar", "baz"},
|
|
},
|
|
{
|
|
name: "drops empty values",
|
|
input: "foo,,bar, ,baz",
|
|
expected: []string{"foo", "bar", "baz"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := ParseStringList(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTruncateRunes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
maxRunes int
|
|
expected string
|
|
}{
|
|
{
|
|
name: "exactly the limit is unchanged",
|
|
input: "hello",
|
|
maxRunes: 5,
|
|
expected: "hello",
|
|
},
|
|
{
|
|
name: "longer ASCII is cut to the limit",
|
|
input: "hello world",
|
|
maxRunes: 5,
|
|
expected: "hello",
|
|
},
|
|
{
|
|
// The byte length exceeds the limit while the character count does not, which is what a
|
|
// byte-based truncation would get wrong.
|
|
name: "counts characters, not bytes, so multi-byte text is not cut early",
|
|
input: "héllo",
|
|
maxRunes: 5,
|
|
expected: "héllo",
|
|
},
|
|
{
|
|
name: "cuts multi-byte text on a character boundary",
|
|
input: strings.Repeat("é", 10),
|
|
maxRunes: 4,
|
|
expected: strings.Repeat("é", 4),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, TruncateRunes(tt.input, tt.maxRunes))
|
|
})
|
|
}
|
|
}
|