mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-05 04:59:26 +09:00
json.Marshal refuses a year outside [0,9999], so one stored expiry failed every map with that node. TailNode and key patches send UTC, the builder drops what fails and the boot scan names the node.
123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package state
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/juanfont/headscale/hscontrol/db"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGivenNameMapsToValidFQDNCheck(t *testing.T) {
|
|
cfg := &types.Config{BaseDomain: "example.com"}
|
|
|
|
_, _, ok := givenNameMapsToValidFQDN.check((&types.Node{ID: 1, GivenName: "valid"}).View(), cfg)
|
|
require.True(t, ok, "a valid given name must pass the check")
|
|
|
|
problem, fixHint, ok := givenNameMapsToValidFQDN.check((&types.Node{ID: 7, GivenName: ""}).View(), cfg)
|
|
require.False(t, ok, "an empty given name must fail the check")
|
|
require.NotEmpty(t, problem)
|
|
require.Contains(t, fixHint, "rename 7", "fix hint must name the offending node")
|
|
}
|
|
|
|
func TestExpiryIsEncodableCheck(t *testing.T) {
|
|
cfg := &types.Config{}
|
|
|
|
_, _, ok := expiryIsEncodable.check((&types.Node{ID: 1}).View(), cfg)
|
|
require.True(t, ok, "a node without expiry must pass the check")
|
|
|
|
earliest := time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
latest := time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
|
|
|
|
for _, tt := range []struct {
|
|
expiry time.Time
|
|
ok bool
|
|
}{
|
|
{earliest, true},
|
|
{earliest.Add(-time.Second), false},
|
|
{latest, true},
|
|
{latest.Add(time.Second), false},
|
|
} {
|
|
node := &types.Node{ID: 7, Expiry: &tt.expiry}
|
|
problem, fixHint, ok := expiryIsEncodable.check(node.View(), cfg)
|
|
require.Equal(t, tt.ok, ok, "expiry %s", tt.expiry)
|
|
|
|
if !ok {
|
|
require.NotEmpty(t, problem)
|
|
require.Equal(t, "headscale nodes expire --identifier 7", fixHint,
|
|
"the fix must name the node and keep an expired one expired")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestScanNodeHealthReportsInvalidNameWithoutMutating proves the boot scan
|
|
// reports a node whose stored name would break map generation (issue #3346)
|
|
// with an actionable fix, and that it never rewrites the stored name — the
|
|
// maintainer's decision is log-only, no silent mutation.
|
|
func TestScanNodeHealthReportsInvalidNameWithoutMutating(t *testing.T) {
|
|
dbPath := t.TempDir() + "/headscale.db"
|
|
cfg := persistTestConfig(dbPath)
|
|
|
|
database, err := db.NewHeadscaleDatabase(cfg)
|
|
require.NoError(t, err)
|
|
|
|
user := database.CreateUserForTest("scan-user")
|
|
bad := database.CreateRegisteredNodeForTest(user, "scan-bad")
|
|
good := database.CreateRegisteredNodeForTest(user, "scan-good")
|
|
|
|
require.NoError(t, database.DB.
|
|
Model(&types.Node{}).
|
|
Where("id = ?", bad.ID).
|
|
Update("given_name", "").Error)
|
|
require.NoError(t, database.Close())
|
|
|
|
s, err := NewState(cfg)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
|
|
findings := s.scanNodeHealth()
|
|
|
|
var badFinding *nodeHealthFinding
|
|
|
|
for i := range findings {
|
|
require.NotEqual(t, good.ID, findings[i].nodeID, "a valid node must not be reported")
|
|
|
|
if findings[i].nodeID == bad.ID {
|
|
badFinding = &findings[i]
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, badFinding, "a node with an invalid name must be reported")
|
|
require.Contains(t, badFinding.fixHint, "rename", "finding must carry an actionable fix")
|
|
|
|
// Log-only: neither the scan nor boot may rewrite the stored name.
|
|
nv, ok := s.GetNodeByID(bad.ID)
|
|
require.True(t, ok)
|
|
require.Empty(t, nv.GivenName(), "boot scan must not mutate the stored name")
|
|
}
|
|
|
|
// TestBatchSetNodeHealthUnchangedSkipsWrite ensures asking for the health
|
|
// value already stored publishes no snapshot — no peersFunc invocation, no
|
|
// peer-map rebuild.
|
|
func TestBatchSetNodeHealthUnchangedSkipsWrite(t *testing.T) {
|
|
_, s, nodeID := persistTestSetup(t)
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
|
|
// Use the State's nodeStore directly to confirm the node exists.
|
|
_, exists := s.nodeStore.GetNode(nodeID)
|
|
require.True(t, exists)
|
|
|
|
initialSnapshot := s.nodeStore.data.Load()
|
|
|
|
// The node starts with Unhealthy=false. Asking for "healthy=true"
|
|
// means Unhealthy stays false — no change.
|
|
for range 20 {
|
|
changed := s.BatchSetNodeHealth(map[types.NodeID]bool{nodeID: true})
|
|
require.False(t, changed,
|
|
"unchanged health write must not report a route-primary change")
|
|
require.Same(t, initialSnapshot, s.nodeStore.data.Load(),
|
|
"unchanged health write must not publish a new snapshot")
|
|
}
|
|
}
|