228 lines
5.4 KiB
Go
228 lines
5.4 KiB
Go
|
|
package zlog
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"net/netip"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
||
|
|
"github.com/rs/zerolog"
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
"tailscale.com/tailcfg"
|
||
|
|
"tailscale.com/types/key"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestSafeHostinfo_MarshalZerologObject(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
hostinfo *tailcfg.Hostinfo
|
||
|
|
wantFields map[string]any
|
||
|
|
wantAbsent []string // Fields that should NOT be present
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "nil hostinfo",
|
||
|
|
hostinfo: nil,
|
||
|
|
wantFields: map[string]any{},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "basic hostinfo",
|
||
|
|
hostinfo: &tailcfg.Hostinfo{
|
||
|
|
Hostname: "myhost",
|
||
|
|
OS: "linux",
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Hostname: "myhost",
|
||
|
|
zf.OS: "linux",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "hostinfo with routes and tags",
|
||
|
|
hostinfo: &tailcfg.Hostinfo{
|
||
|
|
Hostname: "router",
|
||
|
|
OS: "linux",
|
||
|
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
||
|
|
RequestTags: []string{"tag:server"},
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Hostname: "router",
|
||
|
|
zf.OS: "linux",
|
||
|
|
zf.RoutableIPCount: float64(1),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "hostinfo with netinfo",
|
||
|
|
hostinfo: &tailcfg.Hostinfo{
|
||
|
|
Hostname: "myhost",
|
||
|
|
OS: "windows",
|
||
|
|
NetInfo: &tailcfg.NetInfo{
|
||
|
|
PreferredDERP: 1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Hostname: "myhost",
|
||
|
|
zf.OS: "windows",
|
||
|
|
zf.DERP: float64(1),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "sensitive fields are NOT logged",
|
||
|
|
hostinfo: &tailcfg.Hostinfo{
|
||
|
|
Hostname: "myhost",
|
||
|
|
OS: "linux",
|
||
|
|
OSVersion: "5.15.0-generic", // Should NOT be logged
|
||
|
|
DeviceModel: "ThinkPad X1", // Should NOT be logged
|
||
|
|
IPNVersion: "1.50.0", // Should NOT be logged
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Hostname: "myhost",
|
||
|
|
zf.OS: "linux",
|
||
|
|
},
|
||
|
|
wantAbsent: []string{"os_version", "device_model", "ipn_version", "OSVersion", "DeviceModel", "IPNVersion"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
|
||
|
|
log := zerolog.New(&buf)
|
||
|
|
|
||
|
|
log.Info().EmbedObject(Hostinfo(tt.hostinfo)).Msg("test")
|
||
|
|
|
||
|
|
var result map[string]any
|
||
|
|
|
||
|
|
err := json.Unmarshal(buf.Bytes(), &result)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Check expected fields are present
|
||
|
|
for key, wantVal := range tt.wantFields {
|
||
|
|
assert.Equal(t, wantVal, result[key], "field %s", key)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check sensitive fields are absent
|
||
|
|
for _, key := range tt.wantAbsent {
|
||
|
|
_, exists := result[key]
|
||
|
|
assert.False(t, exists, "sensitive field %s should not be logged", key)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSafeMapRequest_MarshalZerologObject(t *testing.T) {
|
||
|
|
nodeKey := key.NewNode().Public()
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
req *tailcfg.MapRequest
|
||
|
|
wantFields map[string]any
|
||
|
|
wantAbsent []string
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "nil request",
|
||
|
|
req: nil,
|
||
|
|
wantFields: map[string]any{},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "basic request",
|
||
|
|
req: &tailcfg.MapRequest{
|
||
|
|
Stream: true,
|
||
|
|
OmitPeers: false,
|
||
|
|
Version: 100,
|
||
|
|
NodeKey: nodeKey,
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Stream: true,
|
||
|
|
zf.OmitPeers: false,
|
||
|
|
zf.Version: float64(100),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "request with endpoints - only count logged",
|
||
|
|
req: &tailcfg.MapRequest{
|
||
|
|
Stream: false,
|
||
|
|
OmitPeers: true,
|
||
|
|
Version: 100,
|
||
|
|
NodeKey: nodeKey,
|
||
|
|
Endpoints: []netip.AddrPort{
|
||
|
|
netip.MustParseAddrPort("192.168.1.100:41641"),
|
||
|
|
netip.MustParseAddrPort("10.0.0.50:41641"),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
wantFields: map[string]any{
|
||
|
|
zf.Stream: false,
|
||
|
|
zf.OmitPeers: true,
|
||
|
|
zf.EndpointsCount: float64(2),
|
||
|
|
},
|
||
|
|
wantAbsent: []string{"endpoints", "Endpoints"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range tests {
|
||
|
|
t.Run(tt.name, func(t *testing.T) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
|
||
|
|
log := zerolog.New(&buf)
|
||
|
|
|
||
|
|
log.Info().EmbedObject(MapRequest(tt.req)).Msg("test")
|
||
|
|
|
||
|
|
var result map[string]any
|
||
|
|
|
||
|
|
err := json.Unmarshal(buf.Bytes(), &result)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
// Check expected fields are present
|
||
|
|
for key, wantVal := range tt.wantFields {
|
||
|
|
assert.Equal(t, wantVal, result[key], "field %s", key)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check node.key is a short string (not full key)
|
||
|
|
if tt.req != nil {
|
||
|
|
nodeKeyStr, ok := result[zf.NodeKey].(string)
|
||
|
|
if ok {
|
||
|
|
// Short keys are truncated, full keys are 64+ chars
|
||
|
|
assert.Less(t, len(nodeKeyStr), 20, "node key should be short form")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check sensitive fields are absent
|
||
|
|
for _, key := range tt.wantAbsent {
|
||
|
|
_, exists := result[key]
|
||
|
|
assert.False(t, exists, "sensitive field %s should not be logged", key)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFieldConstants(t *testing.T) {
|
||
|
|
// Verify field constants follow the expected naming pattern
|
||
|
|
fieldTests := []struct {
|
||
|
|
constant string
|
||
|
|
expected string
|
||
|
|
}{
|
||
|
|
{zf.NodeID, "node.id"},
|
||
|
|
{zf.NodeName, "node.name"},
|
||
|
|
{zf.NodeKey, "node.key"},
|
||
|
|
{zf.MachineKey, "machine.key"},
|
||
|
|
{zf.NodeTags, "node.tags"},
|
||
|
|
{zf.NodeIsTagged, "node.is_tagged"},
|
||
|
|
{zf.NodeOnline, "node.online"},
|
||
|
|
{zf.NodeExpired, "node.expired"},
|
||
|
|
{zf.UserID, "user.id"},
|
||
|
|
{zf.UserName, "user.name"},
|
||
|
|
{zf.PAKID, "pak.id"},
|
||
|
|
{zf.PAKPrefix, "pak.prefix"},
|
||
|
|
{zf.APIKeyID, "api_key.id"},
|
||
|
|
{zf.APIKeyPrefix, "api_key.prefix"},
|
||
|
|
{zf.OmitPeers, "omit_peers"},
|
||
|
|
{zf.Stream, "stream"},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tt := range fieldTests {
|
||
|
|
t.Run(tt.expected, func(t *testing.T) {
|
||
|
|
assert.Equal(t, tt.expected, tt.constant)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|