mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-23 05:01:49 +02:00
217 lines
7.2 KiB
Go
217 lines
7.2 KiB
Go
package agent
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/henrygd/beszel/internal/entities/probe"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestProbeTaskAggregateLockedUsesRawSamplesForShortWindows(t *testing.T) {
|
|
now := time.Date(2026, time.April, 21, 12, 0, 0, 0, time.UTC)
|
|
task := &probeTask{}
|
|
|
|
task.addSampleLocked(probeSample{responseMs: 10, timestamp: now.Add(-90 * time.Second)})
|
|
task.addSampleLocked(probeSample{responseMs: 20, timestamp: now.Add(-30 * time.Second)})
|
|
task.addSampleLocked(probeSample{responseMs: -1, timestamp: now.Add(-10 * time.Second)})
|
|
|
|
agg := task.aggregateLocked(time.Minute, now)
|
|
require.True(t, agg.hasData())
|
|
assert.Equal(t, 2, agg.totalCount)
|
|
assert.Equal(t, 1, agg.successCount)
|
|
assert.Equal(t, 20.0, agg.result()[0])
|
|
assert.Equal(t, 20.0, agg.result()[1])
|
|
assert.Equal(t, 20.0, agg.result()[2])
|
|
assert.Equal(t, 50.0, agg.result()[3])
|
|
}
|
|
|
|
func TestProbeTaskAggregateLockedUsesMinuteBucketsForLongWindows(t *testing.T) {
|
|
now := time.Date(2026, time.April, 21, 12, 0, 30, 0, time.UTC)
|
|
task := &probeTask{}
|
|
|
|
task.addSampleLocked(probeSample{responseMs: 10, timestamp: now.Add(-11 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: 20, timestamp: now.Add(-9 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: 40, timestamp: now.Add(-5 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: -1, timestamp: now.Add(-90 * time.Second)})
|
|
task.addSampleLocked(probeSample{responseMs: 30, timestamp: now.Add(-30 * time.Second)})
|
|
|
|
agg := task.aggregateLocked(10*time.Minute, now)
|
|
require.True(t, agg.hasData())
|
|
assert.Equal(t, 4, agg.totalCount)
|
|
assert.Equal(t, 3, agg.successCount)
|
|
assert.Equal(t, 30.0, agg.result()[0])
|
|
assert.Equal(t, 20.0, agg.result()[1])
|
|
assert.Equal(t, 40.0, agg.result()[2])
|
|
assert.Equal(t, 25.0, agg.result()[3])
|
|
}
|
|
|
|
func TestProbeTaskAddSampleLockedTrimsRawSamplesButKeepsBucketHistory(t *testing.T) {
|
|
now := time.Date(2026, time.April, 21, 12, 0, 0, 0, time.UTC)
|
|
task := &probeTask{}
|
|
|
|
task.addSampleLocked(probeSample{responseMs: 10, timestamp: now.Add(-10 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: 20, timestamp: now})
|
|
|
|
require.Len(t, task.samples, 1)
|
|
assert.Equal(t, 20.0, task.samples[0].responseMs)
|
|
|
|
agg := task.aggregateLocked(10*time.Minute, now)
|
|
require.True(t, agg.hasData())
|
|
assert.Equal(t, 2, agg.totalCount)
|
|
assert.Equal(t, 2, agg.successCount)
|
|
assert.Equal(t, 15.0, agg.result()[0])
|
|
assert.Equal(t, 10.0, agg.result()[1])
|
|
assert.Equal(t, 20.0, agg.result()[2])
|
|
assert.Equal(t, 0.0, agg.result()[3])
|
|
}
|
|
|
|
func TestProbeManagerGetResultsIncludesHourResponseRange(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
task := &probeTask{config: probe.Config{ID: "probe-1"}}
|
|
task.addSampleLocked(probeSample{responseMs: 10, timestamp: now.Add(-30 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: 20, timestamp: now.Add(-9 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: 40, timestamp: now.Add(-5 * time.Minute)})
|
|
task.addSampleLocked(probeSample{responseMs: -1, timestamp: now.Add(-90 * time.Second)})
|
|
task.addSampleLocked(probeSample{responseMs: 30, timestamp: now.Add(-30 * time.Second)})
|
|
|
|
pm := &ProbeManager{probes: map[string]*probeTask{"icmp:example.com": task}}
|
|
|
|
results := pm.GetResults(uint16(time.Minute / time.Millisecond))
|
|
result, ok := results["probe-1"]
|
|
require.True(t, ok)
|
|
require.Len(t, result, 5)
|
|
assert.Equal(t, 30.0, result[0])
|
|
assert.Equal(t, 25.0, result[1])
|
|
assert.Equal(t, 10.0, result[2])
|
|
assert.Equal(t, 40.0, result[3])
|
|
assert.Equal(t, 20.0, result[4])
|
|
}
|
|
|
|
func TestProbeManagerGetResultsIncludesLossOnlyHourData(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
task := &probeTask{config: probe.Config{ID: "probe-1"}}
|
|
task.addSampleLocked(probeSample{responseMs: -1, timestamp: now.Add(-30 * time.Second)})
|
|
task.addSampleLocked(probeSample{responseMs: -1, timestamp: now.Add(-10 * time.Second)})
|
|
|
|
pm := &ProbeManager{probes: map[string]*probeTask{"icmp:example.com": task}}
|
|
|
|
results := pm.GetResults(uint16(time.Minute / time.Millisecond))
|
|
result, ok := results["probe-1"]
|
|
require.True(t, ok)
|
|
require.Len(t, result, 5)
|
|
assert.Equal(t, 0.0, result[0])
|
|
assert.Equal(t, 0.0, result[1])
|
|
assert.Equal(t, 0.0, result[2])
|
|
assert.Equal(t, 0.0, result[3])
|
|
assert.Equal(t, 100.0, result[4])
|
|
}
|
|
|
|
func TestProbeConfigResultKeyUsesSyncedID(t *testing.T) {
|
|
cfg := probe.Config{ID: "probe-1", Target: "1.1.1.1", Protocol: "icmp", Interval: 10}
|
|
assert.Equal(t, "probe-1", cfg.ID)
|
|
}
|
|
|
|
func TestProbeManagerSyncProbesSkipsConfigsWithoutStableID(t *testing.T) {
|
|
validCfg := probe.Config{ID: "probe-1", Target: "https://example.com", Protocol: "http", Interval: 10}
|
|
invalidCfg := probe.Config{Target: "1.1.1.1", Protocol: "icmp", Interval: 10}
|
|
|
|
pm := newProbeManager()
|
|
pm.SyncProbes([]probe.Config{validCfg, invalidCfg})
|
|
defer pm.Stop()
|
|
|
|
_, validExists := pm.probes[validCfg.ID]
|
|
_, invalidExists := pm.probes[invalidCfg.ID]
|
|
assert.True(t, validExists)
|
|
assert.False(t, invalidExists)
|
|
}
|
|
|
|
func TestProbeManagerSyncProbesStopsRemovedTasksButKeepsExisting(t *testing.T) {
|
|
keepCfg := probe.Config{ID: "probe-1", Target: "https://example.com", Protocol: "http", Interval: 10}
|
|
removeCfg := probe.Config{ID: "probe-2", Target: "1.1.1.1", Protocol: "icmp", Interval: 10}
|
|
|
|
keptTask := &probeTask{config: keepCfg, cancel: make(chan struct{})}
|
|
removedTask := &probeTask{config: removeCfg, cancel: make(chan struct{})}
|
|
pm := &ProbeManager{
|
|
probes: map[string]*probeTask{
|
|
keepCfg.ID: keptTask,
|
|
removeCfg.ID: removedTask,
|
|
},
|
|
}
|
|
|
|
pm.SyncProbes([]probe.Config{keepCfg})
|
|
|
|
assert.Same(t, keptTask, pm.probes[keepCfg.ID])
|
|
_, exists := pm.probes[removeCfg.ID]
|
|
assert.False(t, exists)
|
|
|
|
select {
|
|
case <-removedTask.cancel:
|
|
default:
|
|
t.Fatal("expected removed probe task to be cancelled")
|
|
}
|
|
|
|
select {
|
|
case <-keptTask.cancel:
|
|
t.Fatal("expected existing probe task to remain active")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestProbeHTTP(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
|
|
responseMs := probeHTTP(server.Client(), server.URL)
|
|
assert.GreaterOrEqual(t, responseMs, 0.0)
|
|
})
|
|
|
|
t.Run("server error", func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "boom", http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
assert.Equal(t, -1.0, probeHTTP(server.Client(), server.URL))
|
|
})
|
|
}
|
|
|
|
func TestProbeTCP(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer listener.Close()
|
|
|
|
accepted := make(chan struct{})
|
|
go func() {
|
|
defer close(accepted)
|
|
conn, err := listener.Accept()
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
|
|
port := uint16(listener.Addr().(*net.TCPAddr).Port)
|
|
responseMs := probeTCP("127.0.0.1", port)
|
|
assert.GreaterOrEqual(t, responseMs, 0.0)
|
|
<-accepted
|
|
})
|
|
|
|
t.Run("connection failure", func(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
|
|
port := uint16(listener.Addr().(*net.TCPAddr).Port)
|
|
require.NoError(t, listener.Close())
|
|
|
|
assert.Equal(t, -1.0, probeTCP("127.0.0.1", port))
|
|
})
|
|
}
|