headscale/hscontrol/policy/v2/sshtester_compat_test.go
Kristoffer Dalby 26eebcea5a policy/v2: add sshtester compat runner
Replays recorded policy responses for the sshTests block. 200 captures must evaluate; non-200 captures must reject with the recorded body as a substring of the headscale error. Divergences are listed in knownSSHTesterDivergences.
2026-05-13 21:10:13 +02:00

89 lines
2.4 KiB
Go

// Replay golden HuJSON captures under testdata/sshtest_results/*.hujson:
// the 200 path requires headscale's evaluateSSHTests to pass; the
// non-200 path requires headscale to reject the same input with the
// captured error body as a substring. Divergences are listed in
// knownSSHTesterDivergences with the engine gap each represents.
package v2
import (
"path/filepath"
"strings"
"testing"
"github.com/juanfont/headscale/hscontrol/types/testcapture"
"github.com/stretchr/testify/require"
)
// knownSSHTesterDivergences names the engine gap for each capture where
// headscale and upstream disagree.
var knownSSHTesterDivergences = map[string]string{
"sshtest-malformed-dst-bare-ipv6": "bare-IPv6 sshTests dst: upstream parse-accepts then engine-rejects; headscale accepts (IPv4 mirror passes both sides)",
}
func TestSSHTesterCompat(t *testing.T) {
t.Parallel()
files, err := filepath.Glob(filepath.Join("testdata", "sshtest_results", "*.hujson"))
require.NoError(t, err, "failed to glob test files")
if len(files) == 0 {
t.Skip("no sshtest captures yet")
}
users := setupSSHDataCompatUsers()
for _, file := range files {
c, err := testcapture.Read(file)
require.NoError(t, err, "reading %s", file)
t.Run(c.TestID, func(t *testing.T) {
t.Parallel()
if reason, skip := knownSSHTesterDivergences[c.TestID]; skip {
t.Skip(reason)
}
// Each capture pins its own topology IPs; build nodes
// from the capture so host-alias dsts resolve.
nodes := buildGrantsNodesFromCapture(users, c)
policyJSON := []byte(c.Input.FullPolicy)
pm, parseErr := NewPolicyManager(policyJSON, users, nodes.ViewSlice())
if c.Input.APIResponseCode == 200 {
require.NoError(t, parseErr,
"tailscale accepted this policy; headscale must parse it")
_, setErr := pm.SetPolicy(policyJSON)
require.NoError(t, setErr,
"tailscale accepted this policy; headscale sshTests must pass")
return
}
var got error
switch {
case parseErr != nil:
got = parseErr
default:
_, setErr := pm.SetPolicy(policyJSON)
got = setErr
}
require.Error(t, got, "tailscale rejected; headscale must reject too")
if c.Input.APIResponseBody == nil || c.Input.APIResponseBody.Message == "" {
return
}
want := c.Input.APIResponseBody.Message
if !strings.Contains(got.Error(), want) {
t.Errorf("error body mismatch\n tailscale wants: %q\n headscale got: %q", want, got.Error())
}
})
}
}