integration: harden ACL test ergonomics

tsic.Curl returned ("", nil) when curl exited 0 with a zero-byte body —
the usual signature of a mid-stream reset — so EventuallyWithT could
not retry. Return an error on empty body instead.

Replace the 56 inline curl + assert.Len(13) blocks with
assertCurlDockerHostname so the empty-body fix benefits every callsite
without further touch-ups.

Gate ACL waits on actual filter visibility: snapshotClientFilters +
waitForClientFilterChange ensure the new PacketFilter has reached the
client before assertions fire; SyncOption + WithPreBarrier feeds a
server-side policy-loaded check into WaitForTailscaleSyncPerUser.

Move advertise-routes mutation out of EventuallyWithT in route_test
(cmd/hi/README forbids retrying state-mutating calls). Pace the
TestNodeOnlineStatus outer loop with a Ticker, not a Sleep.
This commit is contained in:
Kristoffer Dalby 2026-05-13 13:19:49 +00:00
parent 78fd6efb38
commit dfcc96d808
6 changed files with 321 additions and 300 deletions

View file

@ -8,6 +8,7 @@ import (
"io"
"maps"
"net/netip"
"reflect"
"slices"
"strconv"
"strings"
@ -27,6 +28,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/tailcfg"
"tailscale.com/wgengine/filter"
)
const (
@ -558,6 +560,56 @@ func assertCurlSuccessWithCollect(c *assert.CollectT, client TailscaleClient, ur
assert.NotEmpty(c, result, msg)
}
// assertCurlDockerHostname curls url and asserts the body is the
// 13-byte Docker auto-generated container hostname (12 hex chars +
// trailing newline from /etc/hostname). For use inside EventuallyWithT.
func assertCurlDockerHostname(c *assert.CollectT, client TailscaleClient, url, msg string) {
const dockerHostnameLen = 13
result, err := client.Curl(url)
assert.NoError(c, err, msg) //nolint:testifylint // CollectT requires assert, not require
assert.Len(c, result, dockerHostnameLen, msg)
}
// snapshotClientFilters snapshots each client's current netmap
// PacketFilter keyed by hostname. Pair with waitForClientFilterChange.
func snapshotClientFilters(t *testing.T, clients []TailscaleClient) map[string][]filter.Match {
t.Helper()
out := make(map[string][]filter.Match, len(clients))
for _, c := range clients {
nm, err := c.Netmap()
require.NoError(t, err, "snapshot netmap for %s", c.Hostname())
out[c.Hostname()] = nm.PacketFilter
}
return out
}
// waitForClientFilterChange polls each client until its netmap
// PacketFilter differs from baselines[Hostname]. Use after SetPolicy
// to gate on client-side filter application before asserting
// reachability.
func waitForClientFilterChange(t *testing.T, clients []TailscaleClient, baselines map[string][]filter.Match, timeout time.Duration) {
t.Helper()
for _, client := range clients {
c := client
baseline := baselines[c.Hostname()]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
nm, err := c.Netmap()
if !assert.NoError(ct, err, "fetch netmap for %s", c.Hostname()) {
return
}
assert.False(ct, reflect.DeepEqual(baseline, nm.PacketFilter), "client %s PacketFilter unchanged since baseline", c.Hostname())
}, timeout, integrationutil.SlowPoll, "client %s PacketFilter should change after SetPolicy", c.Hostname())
}
}
// assertCurlFailWithCollect asserts that a curl request fails. Uses
// CurlFailFast internally for aggressive timeouts, avoiding wasted
// time on retries when we expect the connection to be blocked.