mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-25 17:34:22 +00:00
- store checkupdates' private DB in the agent data dir, since ProtectSystem=strict makes /tmp read-only - sync the private DB every 12h and use `checkupdates -n` in between, forcing a sync when the DB is missing so -n never reports a false 0 - set WaitDelay so a timed-out command can't hang on child processes
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
//go:build testing
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func readPackageUpdatesTestData(t *testing.T, name string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(filepath.Join("test-data", "package_updates", name))
|
|
require.NoError(t, err)
|
|
return string(data)
|
|
}
|
|
|
|
// Test data files are real command outputs captured in containers.
|
|
|
|
func TestParseAptSimulate(t *testing.T) {
|
|
tests := []struct {
|
|
file string
|
|
total, security uint16
|
|
}{
|
|
{"apt_debian12.txt", 44, 5},
|
|
{"apt_ubuntu2204.txt", 58, 45},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.file, func(t *testing.T) {
|
|
total, security := parseAptSimulate(readPackageUpdatesTestData(t, tt.file))
|
|
assert.Equal(t, tt.total, total)
|
|
assert.Equal(t, tt.security, security)
|
|
})
|
|
}
|
|
|
|
t.Run("new dependencies and trailing brackets", func(t *testing.T) {
|
|
out := `Inst linux-image-6.8.0-50-generic (6.8.0-50.51 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
|
|
Inst linux-image-generic [6.8.0-49.49] (6.8.0-50.50 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
|
|
Inst gcc-12-base [12.3.0-1ubuntu1~22.04] (12.3.0-1ubuntu1~22.04.3 Ubuntu:22.04/jammy-updates [arm64]) [libstdc++6:arm64 libgcc-s1:arm64 ]
|
|
Conf linux-image-generic (6.8.0-50.50 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64])
|
|
Remv oldpkg [1.0]`
|
|
total, security := parseAptSimulate(out)
|
|
assert.Equal(t, uint16(2), total)
|
|
assert.Equal(t, uint16(1), security)
|
|
})
|
|
|
|
t.Run("no updates", func(t *testing.T) {
|
|
total, security := parseAptSimulate("Reading package lists...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n")
|
|
assert.Zero(t, total)
|
|
assert.Zero(t, security)
|
|
})
|
|
}
|
|
|
|
func TestParseDnfCheckUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
file string
|
|
count uint16
|
|
}{
|
|
{"dnf4_rocky9_check_update.txt", 110},
|
|
{"dnf4_rocky9_check_update_security.txt", 53},
|
|
{"dnf5_fedora42_check_update.txt", 20},
|
|
{"dnf5_fedora42_check_update_security.txt", 5},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.file, func(t *testing.T) {
|
|
assert.Equal(t, tt.count, parseDnfCheckUpdate(readPackageUpdatesTestData(t, tt.file)))
|
|
})
|
|
}
|
|
|
|
t.Run("obsoletes section and notices", func(t *testing.T) {
|
|
out := `
|
|
kernel.x86_64 5.14.0-503.el9 baseos
|
|
Security: kernel-core-5.14.0-427.el9.x86_64 is an installed security update
|
|
Obsoleting Packages
|
|
grub2-tools.x86_64 1:2.06-80.el9 baseos
|
|
grub2-tools.x86_64 1:2.06-77.el9 @baseos
|
|
`
|
|
assert.Equal(t, uint16(1), parseDnfCheckUpdate(out))
|
|
})
|
|
}
|
|
|
|
func TestParseZypperTable(t *testing.T) {
|
|
tests := []struct {
|
|
file string
|
|
count uint16
|
|
}{
|
|
{"zypper_leap155_list_updates.txt", 22},
|
|
{"zypper_leap155_list_patches_security.txt", 4},
|
|
{"zypper_leap156_list_updates_none.txt", 0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.file, func(t *testing.T) {
|
|
assert.Equal(t, tt.count, parseZypperTable(readPackageUpdatesTestData(t, tt.file)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsePacmanCheckUpdates(t *testing.T) {
|
|
assert.Equal(t, uint16(4), parsePacmanCheckUpdates(readPackageUpdatesTestData(t, "pacman_checkupdates.txt")))
|
|
assert.Zero(t, parsePacmanCheckUpdates(""))
|
|
}
|
|
|
|
func TestParseApkUpgradable(t *testing.T) {
|
|
assert.Equal(t, uint16(10), parseApkUpgradable(readPackageUpdatesTestData(t, "apk_alpine320_list_upgradable.txt")))
|
|
assert.Zero(t, parseApkUpgradable(""))
|
|
}
|
|
|
|
func TestPackageUpdatesManagerCaching(t *testing.T) {
|
|
calls := make(chan struct{}, 10)
|
|
result := []uint16{3, 1}
|
|
var resultErr error
|
|
pm := &packageUpdatesManager{
|
|
interval: time.Hour,
|
|
check: func(context.Context) ([]uint16, error) {
|
|
calls <- struct{}{}
|
|
return result, resultErr
|
|
},
|
|
}
|
|
waitIdle := func() {
|
|
require.Eventually(t, func() bool {
|
|
pm.Lock()
|
|
defer pm.Unlock()
|
|
return !pm.running
|
|
}, time.Second, time.Millisecond)
|
|
}
|
|
|
|
now := time.Now()
|
|
// first call starts a background check and returns nothing yet
|
|
assert.Nil(t, pm.get(now))
|
|
waitIdle()
|
|
assert.Len(t, calls, 1)
|
|
|
|
// cached result within interval, no new check
|
|
assert.Equal(t, []uint16{3, 1}, pm.get(now.Add(time.Minute)))
|
|
assert.Len(t, calls, 1)
|
|
|
|
// stale after interval: returns cached value and refreshes in background
|
|
result, resultErr = nil, errors.New("boom")
|
|
assert.Equal(t, []uint16{3, 1}, pm.get(now.Add(2*time.Hour)))
|
|
waitIdle()
|
|
assert.Len(t, calls, 2)
|
|
|
|
// failed check clears the counts
|
|
assert.Nil(t, pm.get(time.Now()))
|
|
}
|
|
|
|
func TestPacmanCheckSync(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("requires a shell script on PATH")
|
|
}
|
|
binDir := t.TempDir()
|
|
dataDir := t.TempDir()
|
|
logFile := filepath.Join(binDir, "calls.log")
|
|
// fake checkupdates logs its args and db path, and creates the sync dir when syncing
|
|
script := `#!/bin/sh
|
|
echo "args=[$*] db=$CHECKUPDATES_DB" >> ` + logFile + `
|
|
[ "$1" = "-n" ] || mkdir -p "$CHECKUPDATES_DB/sync"
|
|
echo "linux 6.1-1 -> 6.2-1"
|
|
`
|
|
require.NoError(t, os.WriteFile(filepath.Join(binDir, "checkupdates"), []byte(script), 0o755))
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
check := newPacmanCheck(dataDir)
|
|
dbPath := filepath.Join(dataDir, "checkup-db")
|
|
readCalls := func() []string {
|
|
data, err := os.ReadFile(logFile)
|
|
require.NoError(t, err)
|
|
return strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
}
|
|
|
|
// first check syncs
|
|
counts, err := check(context.Background())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []uint16{1}, counts)
|
|
// later checks reuse the synced copy
|
|
_, err = check(context.Background())
|
|
require.NoError(t, err)
|
|
// a missing private copy forces a sync
|
|
require.NoError(t, os.RemoveAll(dbPath))
|
|
_, err = check(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, []string{
|
|
"args=[] db=" + dbPath,
|
|
"args=[-n] db=" + dbPath,
|
|
"args=[] db=" + dbPath,
|
|
}, readCalls())
|
|
}
|