Files
fleet/client/orbit_client_test.go
T
Victor Lyuboslavsky a7eb747faf Flag to bypass end user auth (#49683)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #46644 

Demo video: https://www.youtube.com/watch?v=svCaA-820yc
Docs: https://github.com/fleetdm/fleet/pull/49713/changes

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [x] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [x] Verified that fleetd runs on macOS, Linux and Windows
  - Did not verify macOS.
- [x] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added `--bypass-end-user-auth` to `fleetctl package` and Orbit.
* Generated Linux and Windows installers can skip the end-user
authentication prompt during enrollment.
* Added `ORBIT_BYPASS_END_USER_AUTH` for environment-based
configuration.
* End-user authentication remains enabled when a supported EUA token is
provided.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-22 15:32:58 -05:00

297 lines
7.9 KiB
Go

package client
import (
"context"
"encoding/json"
"errors"
"reflect"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetConfig(t *testing.T) {
t.Run(
"config cache", func(t *testing.T) {
oc := OrbitClient{}
oc.configCache.config = &fleet.OrbitConfig{}
oc.configCache.lastUpdated = time.Now().Add(1 * time.Second)
config, err := oc.GetConfig()
require.NoError(t, err)
require.Equal(t, oc.configCache.config, config)
},
)
t.Run(
"config cache error", func(t *testing.T) {
oc := OrbitClient{}
oc.configCache.config = nil
oc.configCache.err = errors.New("test error")
oc.configCache.lastUpdated = time.Now().Add(1 * time.Second)
config, err := oc.GetConfig()
require.Error(t, err)
require.Equal(t, oc.configCache.config, config)
},
)
}
func TestNewOrbitClientBypassEndUserAuth(t *testing.T) {
newClient := func(bypassEndUserAuth bool) *OrbitClient {
oc, err := NewOrbitClient(
t.TempDir(),
"https://fleet.example.com",
"",
true,
"secret",
nil,
fleet.OrbitHostInfo{HardwareUUID: "uuid", Hostname: "host"},
nil,
nil,
"",
bypassEndUserAuth,
)
require.NoError(t, err)
return oc
}
// With bypass enabled, orbit must not advertise the end-user auth capability.
require.NotContains(t, newClient(true).ClientCapabilities, fleet.CapabilityEndUserAuth)
// Without bypass, capabilities are unchanged from the default set.
require.Equal(t, fleet.GetOrbitClientCapabilities(), newClient(false).ClientCapabilities)
}
func clientWithConfig(cfg *fleet.OrbitConfig) *OrbitClient {
ctx, cancel := context.WithCancel(context.Background())
oc := &OrbitClient{
receiverUpdateContext: ctx,
receiverUpdateCancelFunc: cancel,
}
oc.configCache.config = cfg
oc.configCache.lastUpdated = time.Now().Add(1 * time.Hour)
return oc
}
func TestConfigReceiverCalls(t *testing.T) {
var called1, called2 bool
testmsg := json.RawMessage("testing")
rfunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
if !reflect.DeepEqual(cfg.Flags, testmsg) {
return errors.New("not equal testmsg")
}
called1 = true
return nil
})
rfunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
if !reflect.DeepEqual(cfg.Flags, testmsg) {
return errors.New("not equal testmsg")
}
called2 = true
return nil
})
client := clientWithConfig(&fleet.OrbitConfig{Flags: testmsg})
client.RegisterConfigReceiver(rfunc1)
client.RegisterConfigReceiver(rfunc2)
err := client.RunConfigReceivers()
require.NoError(t, err)
require.True(t, called1)
require.True(t, called2)
}
func TestConfigReceiverErrors(t *testing.T) {
var called1, called2 bool
rfunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called1 = true
return nil
})
rfunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called2 = true
return nil
})
err1 := errors.New("error1")
err2 := errors.New("error2")
efunc1 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
return err1
})
efunc2 := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
return err2
})
// Make sure we don't get stuck or crash on receiver panic
pfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
panic("woah")
})
client := clientWithConfig(&fleet.OrbitConfig{})
client.RegisterConfigReceiver(efunc1)
client.RegisterConfigReceiver(rfunc1)
client.RegisterConfigReceiver(efunc2)
client.RegisterConfigReceiver(rfunc2)
client.RegisterConfigReceiver(pfunc)
err := client.RunConfigReceivers()
require.ErrorIs(t, err, err1)
require.ErrorIs(t, err, err2)
require.True(t, called1)
require.True(t, called2)
}
func TestExecuteConfigReceiversCancel(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
client.ReceiverUpdateInterval = 100 * time.Millisecond
var calls1, calls2 int
requiredCalls := 4
cfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
calls1++
if calls1 == requiredCalls {
client.receiverUpdateCancelFunc()
}
return nil
})
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
calls2++
return nil
})
client.RegisterConfigReceiver(cfunc)
client.RegisterConfigReceiver(rfunc)
err := client.ExecuteConfigReceivers()
require.Nil(t, err)
require.Equal(t, requiredCalls, calls1)
require.Equal(t, requiredCalls, calls2)
}
func TestExecuteConfigReceiversInterrupt(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
defer client.receiverUpdateCancelFunc()
client.ReceiverUpdateInterval = 100 * time.Millisecond
var called bool
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
called = true
return nil
})
client.RegisterConfigReceiver(rfunc)
finChan := make(chan error)
go func() {
finChan <- client.ExecuteConfigReceivers()
}()
go func() {
time.Sleep(500 * time.Millisecond)
client.receiverUpdateCancelFunc()
}()
select {
case err := <-finChan:
require.Nil(t, err)
require.True(t, called)
case <-time.NewTimer(2 * time.Second).C:
require.Fail(t, "receiver interrupt cancel didn't work")
}
}
func TestExecuteConfigReceiversBackoffOnError(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
client.ReceiverUpdateInterval = 1 * time.Second
var callTimes []time.Time
callCount := 0
// 3 failures then cancel: intervals should be ~1s (base tick), ~2s, ~4s.
targetCalls := 4
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
callTimes = append(callTimes, time.Now())
callCount++
if callCount >= targetCalls {
client.receiverUpdateCancelFunc()
return nil
}
return errors.New("server error")
})
client.RegisterConfigReceiver(rfunc)
done := make(chan error, 1)
go func() { done <- client.ExecuteConfigReceivers() }()
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(30 * time.Second):
t.Fatal("test timed out waiting for ExecuteConfigReceivers")
}
require.Equal(t, targetCalls, callCount)
// Verify each successive interval is strictly longer than the previous.
// Call 0->1 is the base tick (~1s), 1->2 should be ~2s, 2->3 should be ~4s.
require.GreaterOrEqual(t, len(callTimes), 3, "need at least 3 calls to verify growth")
for i := 1; i < len(callTimes)-1; i++ {
prev := callTimes[i].Sub(callTimes[i-1])
curr := callTimes[i+1].Sub(callTimes[i])
assert.Greater(t, curr, prev,
"interval %d->%d (%v) should be greater than %d->%d (%v)",
i, i+1, curr, i-1, i, prev)
}
}
func TestExecuteConfigReceiversResetOnSuccess(t *testing.T) {
client := clientWithConfig(&fleet.OrbitConfig{})
client.ReceiverUpdateInterval = 1 * time.Second
callCount := 0
var intervalAfterRecovery time.Duration
var recoveryStart time.Time
rfunc := fleet.OrbitConfigReceiverFunc(func(cfg *fleet.OrbitConfig) error {
callCount++
switch {
case callCount <= 2:
// First 2 calls fail -- build up backoff
return errors.New("server error")
case callCount == 3:
// Success -- should reset backoff
recoveryStart = time.Now()
return nil
case callCount == 4:
// Next call should be at base interval (~1s), not backed off
intervalAfterRecovery = time.Since(recoveryStart)
client.receiverUpdateCancelFunc()
return nil
}
return nil
})
client.RegisterConfigReceiver(rfunc)
done := make(chan error, 1)
go func() { done <- client.ExecuteConfigReceivers() }()
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(30 * time.Second):
t.Fatal("test timed out waiting for ExecuteConfigReceivers")
}
require.Equal(t, 4, callCount)
// After recovery, interval should be close to base (1s), not backed off.
// Use 2s as the upper bound: base (1s) + jitter (up to 10%) + scheduling slack.
assert.Less(t, intervalAfterRecovery, 2*time.Second,
"after success, interval should reset near base, got %v", intervalAfterRecovery)
}