Files
Victor Lyuboslavsky 98060b08a6 Add Windows managed local account server flow (#48721) (#49924)
<!-- 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 -->
2026-08-03 09:03:27 -05:00

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))
})
}
}