integration: port tests to the ogen API types

Decode CLI/HTTP output into apiv1 (ogen) types instead of the proto types,
matching the new wire format. No proto package is imported outside gen/go now.
This commit is contained in:
Kristoffer Dalby
2026-06-17 18:23:06 +00:00
parent db6b99d414
commit 112c42af0a
15 changed files with 679 additions and 641 deletions
+59 -51
View File
@@ -10,7 +10,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -324,7 +324,7 @@ func TestACLHostsInNetMapTable(t *testing.T) {
user := status.User[status.Self.UserID].LoginName
assert.Len(c, status.Peer, (testCase.want[user]))
assert.Len(c, status.Peer, testCase.want[user])
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected peer visibility")
}
})
@@ -339,7 +339,8 @@ func TestACLHostsInNetMapTable(t *testing.T) {
func TestACLAllowUser80Dst(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
ACLs: []policyv2.ACL{
{
@@ -396,7 +397,8 @@ func TestACLAllowUser80Dst(t *testing.T) {
func TestACLDenyAllPort80(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
Groups: policyv2.Groups{
policyv2.Group("group:integration-acl-test"): []policyv2.Username{policyv2.Username("user1@"), policyv2.Username("user2@")},
@@ -446,7 +448,8 @@ func TestACLDenyAllPort80(t *testing.T) {
func TestACLAllowUserDst(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
ACLs: []policyv2.ACL{
{
@@ -505,7 +508,8 @@ func TestACLAllowUserDst(t *testing.T) {
func TestACLAllowStarDst(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
ACLs: []policyv2.ACL{
{
@@ -565,7 +569,8 @@ func TestACLAllowStarDst(t *testing.T) {
func TestACLNamedHostsCanReachBySubnet(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
Hosts: policyv2.Hosts{
"all": policyv2.Prefix(netip.MustParsePrefix("100.64.0.0/24")),
@@ -740,7 +745,8 @@ func TestACLNamedHostsCanReach(t *testing.T) {
for name, testCase := range tests {
t.Run(name, func(t *testing.T) {
scenario := aclScenario(t,
scenario := aclScenario(
t,
&testCase.policy,
"acl-namedreach",
2,
@@ -1118,7 +1124,8 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) {
func TestACLAutogroupMember(t *testing.T) {
IntegrationSkip(t)
scenario := aclScenario(t,
scenario := aclScenario(
t,
&policyv2.Policy{
ACLs: []policyv2.ACL{
{
@@ -1232,10 +1239,10 @@ func TestACLAutogroupTagged(t *testing.T) {
require.NoError(t, err)
// Create two pre-auth keys per user: one tagged, one untagged
taggedAuthKey, err := scenario.CreatePreAuthKeyWithTags(user.GetId(), true, false, []string{"tag:test"})
taggedAuthKey, err := scenario.CreatePreAuthKeyWithTags(user.GetID().Or(0), true, false, []string{"tag:test"})
require.NoError(t, err)
untaggedAuthKey, err := scenario.CreatePreAuthKey(user.GetId(), true, false)
untaggedAuthKey, err := scenario.CreatePreAuthKey(user.GetID().Or(0), true, false)
require.NoError(t, err)
// Create nodes with proper naming
@@ -1247,13 +1254,13 @@ func TestACLAutogroupTagged(t *testing.T) {
if i == 0 {
// First node is tagged - use tagged PreAuthKey
authKey = taggedAuthKey.GetKey()
authKey = taggedAuthKey.GetKey().Or("")
version = "head"
t.Logf("Creating tagged node for %s", userStr)
} else {
// Second node is untagged - use untagged PreAuthKey
authKey = untaggedAuthKey.GetKey()
authKey = untaggedAuthKey.GetKey().Or("")
version = "unstable"
t.Logf("Creating untagged node for %s", userStr)
@@ -1554,7 +1561,7 @@ func TestACLAutogroupSelf(t *testing.T) {
require.NoError(t, err)
// Create a tagged PreAuthKey for the router node (tags-as-identity model)
authKey, err := scenario.CreatePreAuthKeyWithTags(routerUser.GetId(), true, false, []string{"tag:router-node"})
authKey, err := scenario.CreatePreAuthKeyWithTags(routerUser.GetID().Or(0), true, false, []string{"tag:router-node"})
require.NoError(t, err)
// Create router node (tags come from the PreAuthKey).
@@ -1577,7 +1584,7 @@ func TestACLAutogroupSelf(t *testing.T) {
err = routerClient.WaitForNeedsLogin(integrationutil.PeerSyncTimeout())
require.NoError(t, err)
err = routerClient.Login(headscale.GetEndpoint(), authKey.GetKey())
err = routerClient.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
err = routerClient.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -1947,7 +1954,8 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
networks := scenario.Networks()
require.NotEmpty(t, networks, "scenario should have at least one network")
newClient := scenario.MustAddAndLoginClient(t, "user1", "all", headscale,
newClient := scenario.MustAddAndLoginClient(
t, "user1", "all", headscale,
tsic.WithNetfilter("off"),
tsic.WithPackages("curl"),
tsic.WithWebserver(80),
@@ -2008,7 +2016,7 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
// Get the node list and find the newest node (highest ID)
var (
nodeList []*v1.Node
nodeList []*apiv1.Node
nodeToDeleteID uint64
)
@@ -2019,8 +2027,8 @@ func TestACLPolicyPropagationOverTime(t *testing.T) {
// Find the node with the highest ID (the newest one)
for _, node := range nodeList {
if node.GetId() > nodeToDeleteID {
nodeToDeleteID = node.GetId()
if node.GetID().Or(0) > nodeToDeleteID {
nodeToDeleteID = node.GetID().Or(0)
}
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "iteration %d: Phase 2b - listing nodes before deletion", iteration)
@@ -2205,7 +2213,7 @@ func TestACLTagPropagation(t *testing.T) {
nodes, err := headscale.ListNodes("user1")
require.NoError(t, err)
return user2Clients[0], user1Clients[0], nodes[0].GetId()
return user2Clients[0], user1Clients[0], nodes[0].GetID().Or(0)
},
initialAccess: false, // user2 cannot access user1 (no tag)
tagChange: []string{"tag:shared"}, // add tag:shared
@@ -2256,7 +2264,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node WITH tag:shared via PreAuthKey
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:shared"},
userMap["user1"].GetID().Or(0), false, false, []string{"tag:shared"},
)
require.NoError(t, err)
@@ -2269,11 +2277,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey().Or(""))
require.NoError(t, err)
// Create user2's node (untagged)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetID().Or(0), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2285,7 +2293,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey().Or(""))
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2295,10 +2303,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *apiv1.Node) bool { return len(n.GetTags()) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, tagged.GetID().Or(0)
},
initialAccess: true, // user2 can access user1 (has tag:shared)
tagChange: []string{"tag:other"}, // replace with tag:other
@@ -2349,7 +2357,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node with tag:team-a (user2 has NO ACL for this)
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:team-a"},
userMap["user1"].GetID().Or(0), false, false, []string{"tag:team-a"},
)
require.NoError(t, err)
@@ -2362,11 +2370,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey().Or(""))
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetID().Or(0), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2378,7 +2386,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey().Or(""))
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2388,10 +2396,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *apiv1.Node) bool { return len(n.GetTags()) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, tagged.GetID().Or(0)
},
initialAccess: false, // user2 cannot access (tag:team-a not in ACL)
tagChange: []string{"tag:team-b"}, // change to tag:team-b
@@ -2442,7 +2450,7 @@ func TestACLTagPropagation(t *testing.T) {
// Create user1's node with BOTH tags
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:web", "tag:internal"},
userMap["user1"].GetID().Or(0), false, false, []string{"tag:web", "tag:internal"},
)
require.NoError(t, err)
@@ -2455,11 +2463,11 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey().Or(""))
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetID().Or(0), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2471,7 +2479,7 @@ func TestACLTagPropagation(t *testing.T) {
tsic.WithNetfilter("off"),
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey().Or(""))
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2481,10 +2489,10 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *apiv1.Node) bool { return len(n.GetTags()) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
return user2Node, user1Node, tagged.GetId()
return user2Node, user1Node, tagged.GetID().Or(0)
},
initialAccess: true, // user2 can access (has tag:web)
tagChange: []string{"tag:internal"}, // remove tag:web, keep tag:internal
@@ -2535,7 +2543,7 @@ func TestACLTagPropagation(t *testing.T) {
nodes, err := headscale.ListNodes("user1")
require.NoError(t, err)
return user2Clients[0], user1Clients[0], nodes[0].GetId()
return user2Clients[0], user1Clients[0], nodes[0].GetID().Or(0)
},
initialAccess: false, // user2 cannot access user1 (no tag yet)
tagChange: []string{"tag:server"}, // assign tag:server
@@ -2616,7 +2624,7 @@ func TestACLTagPropagation(t *testing.T) {
allNodes, err := headscale.ListNodes()
assert.NoError(c, err)
node := findNode(allNodes, func(n *v1.Node) bool { return n.GetId() == targetNodeID })
node := findNode(allNodes, func(n *apiv1.Node) bool { return n.GetID().Or(0) == targetNodeID })
assert.NotNil(c, node, "Node should still exist")
if node != nil {
@@ -2759,7 +2767,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
// Create user1's node WITH tag:webserver
taggedKey, err := scenario.CreatePreAuthKeyWithTags(
userMap["user1"].GetId(), false, false, []string{"tag:webserver"},
userMap["user1"].GetID().Or(0), false, false, []string{"tag:webserver"},
)
require.NoError(t, err)
@@ -2773,11 +2781,11 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
)
require.NoError(t, err)
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = user1Node.Login(headscale.GetEndpoint(), taggedKey.GetKey().Or(""))
require.NoError(t, err)
// Create user2's node
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), false, false)
untaggedKey, err := scenario.CreatePreAuthKey(userMap["user2"].GetID().Or(0), false, false)
require.NoError(t, err)
user2Node, err := scenario.CreateTailscaleNode(
@@ -2789,7 +2797,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
)
require.NoError(t, err)
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey())
err = user2Node.Login(headscale.GetEndpoint(), untaggedKey.GetKey().Or(""))
require.NoError(t, err)
err = scenario.WaitForTailscaleSync()
@@ -2799,10 +2807,10 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
allNodes, err := headscale.ListNodes()
require.NoError(t, err)
tagged := findNode(allNodes, func(n *v1.Node) bool { return len(n.GetTags()) > 0 })
tagged := findNode(allNodes, func(n *apiv1.Node) bool { return len(n.GetTags()) > 0 })
require.NotNil(t, tagged, "expected a tagged node")
targetNodeID := tagged.GetId()
targetNodeID := tagged.GetID().Or(0)
targetFQDN, err := user1Node.FQDN()
require.NoError(t, err)
@@ -2828,7 +2836,7 @@ func TestACLTagPropagationPortSpecific(t *testing.T) {
allNodes, err := headscale.ListNodes()
assert.NoError(c, err) //nolint:testifylint // CollectT requires assert
node := findNode(allNodes, func(n *v1.Node) bool { return n.GetId() == targetNodeID })
node := findNode(allNodes, func(n *apiv1.Node) bool { return n.GetID().Or(0) == targetNodeID })
assert.NotNil(c, node, "Node should still exist")
if node != nil {
@@ -3070,7 +3078,7 @@ func TestACLGroupAfterUserDeletion(t *testing.T) {
nodes, err := headscale.ListNodes("user3")
require.NoError(t, err)
require.Len(t, nodes, 1, "user3 should have exactly one node")
user3NodeID := nodes[0].GetId()
user3NodeID := nodes[0].GetID().Or(0)
// Delete user3's node first (required before deleting the user)
err = headscale.DeleteNode(user3NodeID)
@@ -3081,7 +3089,7 @@ func TestACLGroupAfterUserDeletion(t *testing.T) {
require.NoError(t, err, "user3 should exist")
// Now delete user3 (after their nodes are deleted)
err = headscale.DeleteUser(user3.GetId())
err = headscale.DeleteUser(user3.GetID().Or(0))
require.NoError(t, err)
// Verify user3 is deleted
@@ -3249,13 +3257,13 @@ func TestACLGroupDeletionExactReproduction(t *testing.T) {
nodes, err := headscale.ListNodes(userToDelete)
require.NoError(t, err)
require.Len(t, nodes, 1)
err = headscale.DeleteNode(nodes[0].GetId())
err = headscale.DeleteNode(nodes[0].GetID().Or(0))
require.NoError(t, err)
userToDeleteObj, err := GetUserByName(headscale, userToDelete)
require.NoError(t, err, "user to delete should exist")
err = headscale.DeleteUser(userToDeleteObj.GetId())
err = headscale.DeleteUser(userToDeleteObj.GetID().Or(0))
require.NoError(t, err)
t.Log("Step 2: DONE - user2 deleted, ACL still has user2@ reference")
+15 -16
View File
@@ -11,13 +11,12 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
"github.com/juanfont/headscale/integration/tsic"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
)
// TestAPIAuthenticationBypass tests that the API authentication middleware
@@ -215,11 +214,11 @@ func TestAPIAuthenticationBypass(t *testing.T) {
assert.Equal(t, http.StatusOK, resp.StatusCode,
"Expected 200 status code with valid API key")
// Should be able to parse as protobuf JSON
var response v1.ListUsersResponse
// Should be able to parse as the ogen JSON list envelope
var response apiv1.ListUsersOK
err = protojson.Unmarshal(body, &response)
require.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
err = json.Unmarshal(body, &response)
require.NoError(t, err, "Response should be valid JSON with valid API key")
// Should contain our test users
users := response.GetUsers()
@@ -227,7 +226,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.GetName().Or("")
}
assert.Contains(t, userNames, "user1")
@@ -406,10 +405,10 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
"Curl with valid API key should return 200")
// Should contain user data
var response v1.ListUsersResponse
var response apiv1.ListUsersOK
err = protojson.Unmarshal([]byte(responseBody), &response)
require.NoError(t, err, "Response should be valid protobuf JSON")
err = json.Unmarshal([]byte(responseBody), &response)
require.NoError(t, err, "Response should be valid JSON")
users := response.GetUsers()
assert.Len(t, users, 2, "Should have 2 users")
@@ -522,9 +521,9 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
require.NoError(t, err,
"gRPC connection with valid API key should succeed, output: %s", output)
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
// CLI outputs the users array directly as JSON
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
var users []*v1.User
var users []*apiv1.User
err = json.Unmarshal([]byte(output), &users)
require.NoError(t, err, "Response should be valid JSON array")
@@ -532,7 +531,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.GetName().Or("")
}
assert.Contains(t, userNames, "grpcuser1")
@@ -681,9 +680,9 @@ cli:
require.NoError(t, err,
"CLI with valid API key should succeed")
// CLI outputs the users array directly, not wrapped in [v1.ListUsersResponse]
// CLI outputs the users array directly as JSON
// Parse as JSON array (CLI uses [json.Marshal], not protojson)
var users []*v1.User
var users []*apiv1.User
err = json.Unmarshal([]byte(output), &users)
require.NoError(t, err, "Response should be valid JSON array")
@@ -691,7 +690,7 @@ cli:
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
userNames[i] = u.GetName().Or("")
}
assert.Contains(t, userNames, "cliuser1")
+29 -27
View File
@@ -8,7 +8,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -74,7 +74,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
}
var (
listNodes []*v1.Node
listNodes []*apiv1.Node
nodeCountBeforeLogout int
)
@@ -135,12 +135,12 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
require.NoError(t, err)
for _, userName := range spec.Users {
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(userMap[userName].GetID().Or(0), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey().Or(""))
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -232,7 +232,8 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{},
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("keyrelognewuser"),
)
requireNoErrHeadscaleEnv(t, err)
@@ -256,7 +257,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var (
listNodes []*v1.Node
listNodes []*apiv1.Node
nodeCountBeforeLogout int
)
@@ -290,7 +291,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
require.NoError(t, err)
// Create a new authkey for user1, to be used for all clients
key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(userMap["user1"].GetID().Or(0), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user1: %s", err)
}
@@ -298,13 +299,13 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Log in all clients as user1, iterating over the spec only returns the
// clients, not the usernames.
for _, userName := range spec.Users {
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey().Or(""))
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
}
var user1Nodes []*v1.Node
var user1Nodes []*apiv1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -318,7 +319,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Collect expected node IDs for user1 after relogin
expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes))
for _, node := range user1Nodes {
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetId()))
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetID().Or(0)))
}
// Validate connection state after relogin as user1
@@ -328,7 +329,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// Validate that user2 still has their original nodes after user1's re-authentication
// When nodes re-authenticate with a different user's pre-auth key, NEW nodes are created
// for the new user. The original nodes remain with the original user.
var user2Nodes []*v1.Node
var user2Nodes []*apiv1.Node
t.Logf("Validating user2 node persistence after user1 relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -402,7 +403,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var (
listNodes []*v1.Node
listNodes []*apiv1.Node
nodeCountBeforeLogout int
)
@@ -446,7 +447,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
require.NoError(t, err)
for _, userName := range spec.Users {
key, err := scenario.CreatePreAuthKey(userMap[userName].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(userMap[userName].GetID().Or(0), true, false)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
@@ -458,12 +459,13 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
"preauthkeys",
"expire",
"--id",
strconv.FormatUint(key.GetId(), 10),
})
strconv.FormatUint(key.GetID().Or(0), 10),
},
)
require.NoError(t, err)
require.NoError(t, err)
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey().Or(""))
assert.ErrorContains(t, err, "authkey expired")
}
})
@@ -498,14 +500,14 @@ func TestAuthKeyDeleteKey(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap["user1"].GetId()
userID := userMap["user1"].GetID().Or(0)
// Create a pre-auth key - we keep the full key string before it gets redacted
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
require.NoError(t, err)
authKeyString := authKey.GetKey()
authKeyID := authKey.GetId()
authKeyString := authKey.GetKey().Or("")
authKeyID := authKey.GetID().Or(0)
t.Logf("Created pre-auth key ID %d: %s", authKeyID, authKeyString)
// Create a tailscale client and log it in with the auth key
@@ -519,7 +521,7 @@ func TestAuthKeyDeleteKey(t *testing.T) {
require.NoError(t, err)
// Wait for the node to be registered
var user1Nodes []*v1.Node
var user1Nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -529,8 +531,8 @@ func TestAuthKeyDeleteKey(t *testing.T) {
assert.Len(c, user1Nodes, 1)
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for node to be registered")
nodeID := user1Nodes[0].GetId()
nodeName := user1Nodes[0].GetName()
nodeID := user1Nodes[0].GetID().Or(0)
nodeName := user1Nodes[0].GetName().Or("")
t.Logf("Node %d (%s) created successfully with auth_key_id=%d", nodeID, nodeName, authKeyID)
// Verify node is online
@@ -640,7 +642,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
// Step 1: Verify initial route is advertised, approved, and SERVING
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
var initialNode *v1.Node
var initialNode *apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -662,7 +664,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
require.NotNil(t, initialNode, "Initial node should be found")
initialNodeID := initialNode.GetId()
initialNodeID := initialNode.GetID().Or(0)
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
@@ -694,12 +696,12 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
key, err := scenario.CreatePreAuthKey(userMap[user].GetId(), true, false)
key, err := scenario.CreatePreAuthKey(userMap[user].GetID().Or(0), true, false)
require.NoError(t, err)
// Re-login - the container already has extraLoginArgs with --advertise-routes
// from the initial setup, so routes will be advertised on re-login
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey().Or(""))
require.NoError(t, err)
// Wait for client to be running
@@ -738,7 +740,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) {
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
// Also verify node ID was preserved (same node, not new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.GetID().Or(0),
"Node ID should be preserved after same-user relogin")
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
+196 -196
View File
@@ -11,7 +11,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -86,38 +86,38 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
listUsers, err := headscale.ListUsers()
require.NoError(t, err)
want := []*v1.User{
want := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@test.no",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@test.no"),
},
{
Id: 2,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
{
Id: 3,
Name: "user2",
Email: "user2@test.no",
ID: apiv1.NewOptUint64(3),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@test.no"),
},
{
Id: 4,
Name: "user2",
Email: "", // Unverified
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user2",
ID: apiv1.NewOptUint64(4),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString(""), // Unverified
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user2"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
}
@@ -245,38 +245,38 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified bool
cliUsers []string
oidcUsers []string
want func(iss string) []*v1.User
want func(iss string) []*apiv1.User
}{
{
name: "no-migration-verified-email",
emailVerified: true,
cliUsers: []string{"user1", "user2"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*apiv1.User {
return []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@test.no",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@test.no"),
},
{
Id: 2,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: iss + "/user1",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user1"),
},
{
Id: 3,
Name: "user2",
Email: "user2@test.no",
ID: apiv1.NewOptUint64(3),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@test.no"),
},
{
Id: 4,
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
ProviderId: iss + "/user2",
ID: apiv1.NewOptUint64(4),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user2"),
},
}
},
@@ -286,29 +286,29 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified: false,
cliUsers: []string{"user1", "user2"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*apiv1.User {
return []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@test.no",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@test.no"),
},
{
Id: 2,
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user1"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user1"),
},
{
Id: 3,
Name: "user2",
Email: "user2@test.no",
ID: apiv1.NewOptUint64(3),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@test.no"),
},
{
Id: 4,
Name: "user2",
Provider: "oidc",
ProviderId: iss + "/user2",
ID: apiv1.NewOptUint64(4),
Name: apiv1.NewOptString("user2"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user2"),
},
}
},
@@ -318,29 +318,29 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified: false,
cliUsers: []string{"user1.headscale.net", "user2.headscale.net"},
oidcUsers: []string{"user1", "user2"},
want: func(iss string) []*v1.User {
return []*v1.User{
want: func(iss string) []*apiv1.User {
return []*apiv1.User{
{
Id: 1,
Name: "user1.headscale.net",
Email: "user1.headscale.net@test.no",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1.headscale.net"),
Email: apiv1.NewOptString("user1.headscale.net@test.no"),
},
{
Id: 2,
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user1"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user1"),
},
{
Id: 3,
Name: "user2.headscale.net",
Email: "user2.headscale.net@test.no",
ID: apiv1.NewOptUint64(3),
Name: apiv1.NewOptString("user2.headscale.net"),
Email: apiv1.NewOptString("user2.headscale.net@test.no"),
},
{
Id: 4,
Name: "user2",
Provider: "oidc",
ProviderId: iss + "/user2",
ID: apiv1.NewOptUint64(4),
Name: apiv1.NewOptString("user2"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(iss + "/user2"),
},
}
},
@@ -393,10 +393,10 @@ func TestOIDC024UserCreation(t *testing.T) {
require.NoError(t, err)
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users: %s", diff)
}
})
@@ -509,28 +509,28 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var listNodes []*v1.Node
var listNodes []*apiv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -593,33 +593,33 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users after user2 login")
assert.Len(ct, listUsers, 2, "Expected exactly 2 users after user2 login, got %d users", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
{
Id: 2,
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user2",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user2"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after user2 login - expected both user1 and user2: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating both user1 and user2 exist after second OIDC login")
var listNodesAfterNewUserLogin []*v1.Node
var listNodesAfterNewUserLogin []*apiv1.Node
// First, wait for the new node to be created
t.Logf("Waiting for user2 node creation at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -640,9 +640,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
if len(listNodesAfterNewUserLogin) >= 2 {
// Machine key is the same as the "machine" has not changed,
// but Node key is not as it is a new node
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Machine key should be preserved from original node")
assert.Equal(ct, listNodesAfterNewUserLogin[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "Both nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].GetNodeKey(), listNodesAfterNewUserLogin[1].GetNodeKey(), "Node keys should be different between user1 and user2 nodes")
assert.Equal(ct, listNodes[0].GetMachineKey().Or(""), listNodesAfterNewUserLogin[0].GetMachineKey().Or(""), "Machine key should be preserved from original node")
assert.Equal(ct, listNodesAfterNewUserLogin[0].GetMachineKey().Or(""), listNodesAfterNewUserLogin[1].GetMachineKey().Or(""), "Both nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].GetNodeKey().Or(""), listNodesAfterNewUserLogin[1].GetNodeKey().Or(""), "Node keys should be different between user1 and user2 nodes")
}
}, integrationutil.PolicyPropagationTimeout, 2*time.Second, "waiting for node count stabilization at exactly 2 nodes after user2 login")
@@ -650,9 +650,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
var activeUser2NodeID types.NodeID
for _, node := range listNodesAfterNewUserLogin {
if node.GetUser().GetId() == 2 { // user2
activeUser2NodeID = types.NodeID(node.GetId())
t.Logf("Active user2 node: %d (User: %s)", node.GetId(), node.GetUser().GetName())
if node.GetUser().Value.ID.Or(0) == 2 { // user2
activeUser2NodeID = types.NodeID(node.GetID().Or(0))
t.Logf("Active user2 node: %d (User: %s)", node.GetID().Or(0), node.GetUser().Value.Name.Or(""))
break
}
@@ -685,9 +685,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Validate node stability - ensure no phantom nodes
for i, node := range currentNodes {
assert.NotNil(ct, node.GetUser(), "Node %d should have a valid user before logout", i)
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should have a valid machine key before logout", i)
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
assert.True(ct, node.GetUser().IsSet(), "Node %d should have a valid user before logout", i)
assert.NotEmpty(ct, node.GetMachineKey().Or(""), "Node %d should have a valid machine key before logout", i)
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().Value.Name.Or(""), node.GetMachineKey().Or("")[:16]+"...")
}
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating stable node count and integrity before user2 logout")
@@ -730,9 +730,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Ensure both nodes are still valid (not cleaned up incorrectly)
for i, node := range currentNodes {
assert.NotNil(ct, node.GetUser(), "Node %d should still have a valid user after user2 logout", i)
assert.NotEmpty(ct, node.GetMachineKey(), "Node %d should still have a valid machine key after user2 logout", i)
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().GetName(), node.GetMachineKey()[:16]+"...")
assert.True(ct, node.GetUser().IsSet(), "Node %d should still have a valid user after user2 logout", i)
assert.NotEmpty(ct, node.GetMachineKey().Or(""), "Node %d should still have a valid machine key after user2 logout", i)
t.Logf("Post-logout node %d: User=%s, MachineKey=%s", i, node.GetUser().Value.Name.Or(""), node.GetMachineKey().Or("")[:16]+"...")
}
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence and integrity after user2 logout")
@@ -761,33 +761,33 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 2, "Should still have exactly 2 users after user1 relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
{
Id: 2,
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user2",
ID: apiv1.NewOptUint64(2),
Name: apiv1.NewOptString("user2"),
Email: apiv1.NewOptString("user2@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user2"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - both users should persist after relogin cycle: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)")
var listNodesAfterLoggingBackIn []*v1.Node
var listNodesAfterLoggingBackIn []*apiv1.Node
// Wait for login to complete and nodes to stabilize
t.Logf("Final node validation: checking node stability after user1 relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -806,24 +806,24 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Validate that the machine we had when we logged in the first time, has the same
// machine key, but a different ID than the newly logged in version of the same
// machine.
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[0].GetMachineKey(), "Original user1 machine key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetNodeKey(), listNodesAfterNewUserLogin[0].GetNodeKey(), "Original user1 node key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[0].GetId(), "Original user1 node ID should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterNewUserLogin[1].GetMachineKey(), "User1 and user2 nodes should share the same machine key")
assert.NotEqual(ct, listNodes[0].GetId(), listNodesAfterNewUserLogin[1].GetId(), "User1 and user2 nodes should have different node IDs")
assert.NotEqual(ct, listNodes[0].GetUser().GetId(), listNodesAfterNewUserLogin[1].GetUser().GetId(), "User1 and user2 nodes should belong to different users")
assert.Equal(ct, listNodes[0].GetMachineKey().Or(""), listNodesAfterNewUserLogin[0].GetMachineKey().Or(""), "Original user1 machine key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetNodeKey().Or(""), listNodesAfterNewUserLogin[0].GetNodeKey().Or(""), "Original user1 node key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetID().Or(0), listNodesAfterNewUserLogin[0].GetID().Or(0), "Original user1 node ID should match user1 node after user switch")
assert.Equal(ct, listNodes[0].GetMachineKey().Or(""), listNodesAfterNewUserLogin[1].GetMachineKey().Or(""), "User1 and user2 nodes should share the same machine key")
assert.NotEqual(ct, listNodes[0].GetID().Or(0), listNodesAfterNewUserLogin[1].GetID().Or(0), "User1 and user2 nodes should have different node IDs")
assert.NotEqual(ct, listNodes[0].GetUser().Value.ID.Or(0), listNodesAfterNewUserLogin[1].GetUser().Value.ID.Or(0), "User1 and user2 nodes should belong to different users")
// Even tho we are logging in again with the same user, the previous key has been expired
// and a new one has been generated. The node entry in the database should be the same
// as the user + machinekey still matches.
assert.Equal(ct, listNodes[0].GetMachineKey(), listNodesAfterLoggingBackIn[0].GetMachineKey(), "Machine key should remain consistent after user1 relogin")
assert.NotEqual(ct, listNodes[0].GetNodeKey(), listNodesAfterLoggingBackIn[0].GetNodeKey(), "Node key should be regenerated after user1 relogin")
assert.Equal(ct, listNodes[0].GetId(), listNodesAfterLoggingBackIn[0].GetId(), "Node ID should be preserved for user1 after relogin")
assert.Equal(ct, listNodes[0].GetMachineKey().Or(""), listNodesAfterLoggingBackIn[0].GetMachineKey().Or(""), "Machine key should remain consistent after user1 relogin")
assert.NotEqual(ct, listNodes[0].GetNodeKey().Or(""), listNodesAfterLoggingBackIn[0].GetNodeKey().Or(""), "Node key should be regenerated after user1 relogin")
assert.Equal(ct, listNodes[0].GetID().Or(0), listNodesAfterLoggingBackIn[0].GetID().Or(0), "Node ID should be preserved for user1 after relogin")
// The "logged back in" machine should have the same machinekey but a different nodekey
// than the version logged in with a different user.
assert.Equal(ct, listNodesAfterLoggingBackIn[0].GetMachineKey(), listNodesAfterLoggingBackIn[1].GetMachineKey(), "Both final nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].GetNodeKey(), listNodesAfterLoggingBackIn[1].GetNodeKey(), "Final nodes should have different node keys for different users")
assert.Equal(ct, listNodesAfterLoggingBackIn[0].GetMachineKey().Or(""), listNodesAfterLoggingBackIn[1].GetMachineKey().Or(""), "Both final nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].GetNodeKey().Or(""), listNodesAfterLoggingBackIn[1].GetNodeKey().Or(""), "Final nodes should have different node keys for different users")
t.Logf("Final validation complete - node counts and key relationships verified at %s", time.Now().Format(TimestampFormat))
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after complete user1->user2->user1 relogin cycle with detailed key validation")
@@ -832,9 +832,9 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
var activeUser1NodeID types.NodeID
for _, node := range listNodesAfterLoggingBackIn {
if node.GetUser().GetId() == 1 { // user1
activeUser1NodeID = types.NodeID(node.GetId())
t.Logf("Active user1 node after relogin: %d (User: %s)", node.GetId(), node.GetUser().GetName())
if node.GetUser().Value.ID.Or(0) == 1 { // user1
activeUser1NodeID = types.NodeID(node.GetID().Or(0))
t.Logf("Active user1 node after relogin: %d (User: %s)", node.GetID().Or(0), node.GetUser().Value.Name.Or(""))
break
}
@@ -942,27 +942,27 @@ func TestOIDCFollowUpUrl(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
}
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(apiv1.User{}),
cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1051,27 +1051,27 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
}
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(apiv1.User{}),
cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1159,28 +1159,28 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("User validation failed after first login - unexpected users: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var initialNodes []*v1.Node
var initialNodes []*apiv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -1211,9 +1211,9 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
validateInitialConnection(t, headscale, expectedNodes)
// Store initial node keys for comparison
initialMachineKey := initialNodes[0].GetMachineKey()
initialNodeKey := initialNodes[0].GetNodeKey()
initialNodeID := initialNodes[0].GetId()
initialMachineKey := initialNodes[0].GetMachineKey().Or("")
initialNodeKey := initialNodes[0].GetNodeKey().Or("")
initialNodeID := initialNodes[0].GetID().Or(0)
// Logout user1
err = ts.Logout()
@@ -1262,26 +1262,26 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 1, "Should still have exactly 1 user after same-user relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
wantUsers := []*apiv1.User{
{
Id: 1,
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
ID: apiv1.NewOptUint64(1),
Name: apiv1.NewOptString("user1"),
Email: apiv1.NewOptString("user1@headscale.net"),
Provider: apiv1.NewOptString("oidc"),
ProviderId: apiv1.NewOptString(scenario.mockOIDC.Issuer() + "/user1"),
},
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return listUsers[i].GetID().Or(0) < listUsers[j].GetID().Or(0)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" {
ct.Errorf("Final user validation failed - user1 should persist after same-user relogin: %s", diff)
}
}, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")
var finalNodes []*v1.Node
var finalNodes []*apiv1.Node
t.Logf("Final node validation: checking node stability after same-user relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -1293,19 +1293,19 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
finalNode := finalNodes[0]
// Machine key should be preserved (same physical machine)
assert.Equal(ct, initialMachineKey, finalNode.GetMachineKey(), "Machine key should be preserved for same user same node relogin")
assert.Equal(ct, initialMachineKey, finalNode.GetMachineKey().Or(""), "Machine key should be preserved for same user same node relogin")
// Node ID should be preserved (same user, same machine)
assert.Equal(ct, initialNodeID, finalNode.GetId(), "Node ID should be preserved for same user same node relogin")
assert.Equal(ct, initialNodeID, finalNode.GetID().Or(0), "Node ID should be preserved for same user same node relogin")
// Node key should be regenerated (new session after logout)
assert.NotEqual(ct, initialNodeKey, finalNode.GetNodeKey(), "Node key should be regenerated after logout/relogin even for same user")
assert.NotEqual(ct, initialNodeKey, finalNode.GetNodeKey().Or(""), "Node key should be regenerated after logout/relogin even for same user")
t.Logf("Final validation complete - same user relogin key relationships verified at %s", time.Now().Format(TimestampFormat))
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after same-user OIDC relogin cycle with key preservation validation")
// Security validation: user1's node should be active after relogin
activeUser1NodeID := types.NodeID(finalNodes[0].GetId())
activeUser1NodeID := types.NodeID(finalNodes[0].GetID().Or(0))
t.Logf("Validating user1 node is online after same-user relogin at %s", time.Now().Format(TimestampFormat))
require.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -1389,10 +1389,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
assert.Len(ct, nodes, 1)
node := nodes[0]
assert.NotNil(ct, node.GetExpiry(), "Expiry should be set after OIDC login")
assert.True(ct, node.GetExpiry().IsSet(), "Expiry should be set after OIDC login")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.GetExpiry().IsSet() {
expiryTime := node.GetExpiry().Value
assert.False(ct, expiryTime.IsZero(), "Expiry should not be zero time")
initialExpiry = expiryTime
@@ -1431,10 +1431,10 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
assert.Len(ct, nodes, 1, "Should still have exactly 1 node after restart")
node := nodes[0]
assert.NotNil(ct, node.GetExpiry(), "Expiry should NOT be nil after restart")
assert.True(ct, node.GetExpiry().IsSet(), "Expiry should NOT be nil after restart")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.GetExpiry().IsSet() {
expiryTime := node.GetExpiry().Value
// This is the bug check - expiry should NOT be zero time
assert.False(ct, expiryTime.IsZero(),
@@ -1567,7 +1567,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.Len(ct, nodes, 1)
gatewayNode := nodes[0]
gatewayNodeID = gatewayNode.GetId()
gatewayNodeID = gatewayNode.GetID().Or(0)
assert.Len(ct, gatewayNode.GetAvailableRoutes(), 1)
assert.Contains(ct, gatewayNode.GetAvailableRoutes(), advertiseRoute)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route advertisement should propagate to headscale")
@@ -1707,10 +1707,10 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.GreaterOrEqual(ct, len(users), 2, "Should have at least 2 users (gateway CLI user + oidcuser)")
// Find gateway CLI user
var gatewayUser *v1.User
var gatewayUser *apiv1.User
for _, user := range users {
if user.GetName() == "gateway" && user.GetProvider() == "" {
if user.GetName().Or("") == "gateway" && user.GetProvider().Or("") == "" {
gatewayUser = user
break
}
@@ -1719,14 +1719,14 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.NotNil(ct, gatewayUser, "Should have gateway CLI user")
if gatewayUser != nil {
assert.Equal(ct, "gateway", gatewayUser.GetName())
assert.Equal(ct, "gateway", gatewayUser.GetName().Or(""))
}
// Find OIDC user
var oidcUserFound *v1.User
var oidcUserFound *apiv1.User
for _, user := range users {
if user.GetName() == "oidcuser" && user.GetProvider() == "oidc" {
if user.GetName().Or("") == "oidcuser" && user.GetProvider().Or("") == "oidc" {
oidcUserFound = user
break
}
@@ -1735,8 +1735,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.NotNil(ct, oidcUserFound, "Should have OIDC user")
if oidcUserFound != nil {
assert.Equal(ct, "oidcuser", oidcUserFound.GetName())
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.GetEmail())
assert.Equal(ct, "oidcuser", oidcUserFound.GetName().Or(""))
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.GetEmail().Or(""))
}
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "headscale should have correct users and nodes")
@@ -1834,7 +1834,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
// Step 1: Verify initial route is advertised, approved, and SERVING
t.Logf("Step 1: Verifying initial route is advertised, approved, and SERVING at %s", time.Now().Format(TimestampFormat))
var initialNode *v1.Node
var initialNode *apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -1856,7 +1856,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
require.NotNil(t, initialNode, "Initial node should be found")
initialNodeID := initialNode.GetId()
initialNodeID := initialNode.GetID().Or(0)
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
@@ -1927,7 +1927,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
"BUG #2896: Subnet routes should contain %s after relogin", advertiseRoute)
// Also verify node ID was preserved (same node, not new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.GetID().Or(0),
"Node ID should be preserved after same-user relogin")
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,
+6 -6
View File
@@ -6,7 +6,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
@@ -98,7 +98,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
// Validate initial connection state
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
var listNodes []*apiv1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -256,7 +256,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Validate initial connection state
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
var listNodes []*apiv1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -316,7 +316,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
t.Logf("all clients logged back in as user1")
var user1Nodes []*v1.Node
var user1Nodes []*apiv1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -330,7 +330,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Collect expected node IDs for user1 after relogin
expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes))
for _, node := range user1Nodes {
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetId()))
expectedUser1Nodes = append(expectedUser1Nodes, types.NodeID(node.GetID().Or(0)))
}
// Validate connection state after relogin as user1
@@ -338,7 +338,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Validate that user2's old nodes still exist in database (but are expired/offline)
// When CLI registration creates new nodes for user1, user2's old nodes remain
var user2Nodes []*v1.Node
var user2Nodes []*apiv1.Node
t.Logf("Validating user2 old nodes remain in database after CLI registration to user1 at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
+11 -11
View File
@@ -3,7 +3,7 @@ package integration
import (
"net/netip"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
@@ -24,19 +24,19 @@ type ControlServer interface {
GetEndpoint() string
WaitForRunning() error
Restart() error
CreateUser(user string) (*v1.User, error)
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error)
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error)
CreateUser(user string) (*apiv1.User, error)
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*apiv1.PreAuthKey, error)
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*apiv1.PreAuthKey, error)
CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*apiv1.PreAuthKey, error)
DeleteAuthKey(id uint64) error
ListNodes(users ...string) ([]*v1.Node, error)
ListNodes(users ...string) ([]*apiv1.Node, error)
DeleteNode(nodeID uint64) error
NodesByUser() (map[string][]*v1.Node, error)
NodesByName() (map[string]*v1.Node, error)
ListUsers() ([]*v1.User, error)
MapUsers() (map[string]*v1.User, error)
NodesByUser() (map[string][]*apiv1.Node, error)
NodesByName() (map[string]*apiv1.Node, error)
ListUsers() ([]*apiv1.User, error)
MapUsers() (map[string]*apiv1.User, error)
DeleteUser(userID uint64) error
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*v1.Node, error)
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*apiv1.Node, error)
SetNodeTags(nodeID uint64, tags []string) error
GetCert() []byte
GetHostname() string
+57 -55
View File
@@ -10,7 +10,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/integrationutil"
@@ -169,12 +169,12 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
}
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
key, err := scenario.CreatePreAuthKey(user.GetID().Or(0), true, true)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey().Or(""))
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -247,12 +247,12 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) {
t.Fatalf("failed to create tailscale nodes in user %s: %s", userName, err)
}
key, err := scenario.CreatePreAuthKey(user.GetId(), true, true)
key, err := scenario.CreatePreAuthKey(user.GetID().Or(0), true, true)
if err != nil {
t.Fatalf("failed to create pre-auth key for user %s: %s", userName, err)
}
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey())
err = scenario.RunTailscaleUp(userName, headscale.GetEndpoint(), key.GetKey().Or(""))
if err != nil {
t.Fatalf("failed to run tailscale up for user %s: %s", userName, err)
}
@@ -386,7 +386,8 @@ func TestTaildrop(t *testing.T) {
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{},
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("taildrop"),
)
requireNoErrHeadscaleEnv(t, err)
@@ -402,7 +403,7 @@ func TestTaildrop(t *testing.T) {
network := networks[0]
// Create untagged nodes for user1 using all test versions
user1Key, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), true, false)
user1Key, err := scenario.CreatePreAuthKey(userMap["user1"].GetID().Or(0), true, false)
require.NoError(t, err)
var user1Clients []TailscaleClient
@@ -414,7 +415,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user1Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user1Key.GetKey().Or(""))
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -425,7 +426,7 @@ func TestTaildrop(t *testing.T) {
}
// Create untagged nodes for user2 using all test versions
user2Key, err := scenario.CreatePreAuthKey(userMap["user2"].GetId(), true, false)
user2Key, err := scenario.CreatePreAuthKey(userMap["user2"].GetID().Or(0), true, false)
require.NoError(t, err)
var user2Clients []TailscaleClient
@@ -437,7 +438,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey().Or(""))
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -449,7 +450,7 @@ func TestTaildrop(t *testing.T) {
// Create a tagged device (tags-as-identity: tags come from PreAuthKey)
// Use "head" version to test latest behavior
taggedKey, err := scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), true, false, []string{"tag:server"})
taggedKey, err := scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetID().Or(0), true, false, []string{"tag:server"})
require.NoError(t, err)
taggedClient, err := scenario.CreateTailscaleNode(
@@ -458,7 +459,7 @@ func TestTaildrop(t *testing.T) {
)
require.NoError(t, err)
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.GetKey())
err = taggedClient.Login(headscale.GetEndpoint(), taggedKey.GetKey().Or(""))
require.NoError(t, err)
err = taggedClient.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -767,8 +768,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Pre-rewrite these were rejected by ApplyHostnameFromHostInfo with
// "invalid characters" and the node was stuck on an invalid-<rand>
// GivenName with the HostName update dropped. The assertions below
// verify both raw preservation ([v1.Node.Name]) and SaaS-matching sanitisation
// ([v1.Node.GivenName]) for each awkward input.
// verify both raw preservation ([apiv1.Node.Name]) and SaaS-matching sanitisation
// ([apiv1.Node.GivenName]) for each awkward input.
hostnames := map[string]string{
"1": "Joe's Mac mini",
"2": "Test@Host",
@@ -814,7 +815,7 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Wait for nodestore batch processing to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 1 second
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err := executeAndUnmarshal(
headscale,
@@ -831,18 +832,18 @@ func TestUpdateHostnameFromClient(t *testing.T) {
assert.Len(ct, nodes, 3, "Should have 3 nodes after hostname updates")
for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
assert.Equal(ct, hostname, node.GetName(), "Node name should match hostname")
hostname := hostnames[strconv.FormatUint(node.GetID().Or(0), 10)]
assert.Equal(ct, hostname, node.GetName().Or(""), "Node name should match hostname")
// GivenName is sanitised via [dnsname.SanitizeHostname] (SaaS algorithm).
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GetGivenName(),
assert.Equal(ct, dnsname.SanitizeHostname(hostname), node.GetGivenName().Or(""),
"Given name should match SaaS hostname-sanitisation rules")
}
}, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second)
// Rename givenName in nodes
for _, node := range nodes {
givenName := fmt.Sprintf("%d-givenname", node.GetId())
givenName := fmt.Sprintf("%d-givenname", node.GetID().Or(0))
_, err = headscale.Execute(
[]string{
"headscale",
@@ -850,8 +851,9 @@ func TestUpdateHostnameFromClient(t *testing.T) {
"rename",
givenName,
"--identifier",
strconv.FormatUint(node.GetId(), 10),
})
strconv.FormatUint(node.GetID().Or(0), 10),
},
)
require.NoError(t, err)
}
@@ -860,8 +862,8 @@ func TestUpdateHostnameFromClient(t *testing.T) {
// Build a map of expected DNSNames by node ID
expectedDNSNames := make(map[string]string)
for _, node := range nodes {
nodeID := strconv.FormatUint(node.GetId(), 10)
expectedDNSNames[nodeID] = fmt.Sprintf("%d-givenname.headscale.net.", node.GetId())
nodeID := strconv.FormatUint(node.GetID().Or(0), 10)
expectedDNSNames[nodeID] = fmt.Sprintf("%d-givenname.headscale.net.", node.GetID().Or(0))
}
// Verify from each client's perspective
@@ -931,9 +933,9 @@ func TestUpdateHostnameFromClient(t *testing.T) {
}
for _, node := range nodes {
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
givenName := fmt.Sprintf("%d-givenname", node.GetId())
if node.GetName() != hostname+"NEW" || node.GetGivenName() != givenName {
hostname := hostnames[strconv.FormatUint(node.GetID().Or(0), 10)]
givenName := fmt.Sprintf("%d-givenname", node.GetID().Or(0))
if node.GetName().Or("") != hostname+"NEW" || node.GetGivenName().Or("") != givenName {
return false
}
}
@@ -993,15 +995,15 @@ func TestExpireNode(t *testing.T) {
})
require.NoError(t, err)
var node v1.Node
var node apiv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
var expiredNodeKey key.NodePublic
err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey()))
err = expiredNodeKey.UnmarshalText([]byte(node.GetNodeKey().Or("")))
require.NoError(t, err)
t.Logf("Node %s with node_key %s has been expired", node.GetName(), expiredNodeKey.String())
t.Logf("Node %s with node_key %s has been expired", node.GetName().Or(""), expiredNodeKey.String())
// Verify that the expired node has been marked in all peers list.
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
@@ -1009,7 +1011,7 @@ func TestExpireNode(t *testing.T) {
status, err := client.Status()
assert.NoError(ct, err)
if client.Hostname() != node.GetName() {
if client.Hostname() != node.GetName().Or("") {
// Check if the expired node appears as expired in this client's peer list
for key, peer := range status.Peer {
if key == expiredNodeKey {
@@ -1025,7 +1027,7 @@ func TestExpireNode(t *testing.T) {
// Verify that the expired node has been marked in all peers list.
for _, client := range allClients {
if client.Hostname() == node.GetName() {
if client.Hostname() == node.GetName().Or("") {
continue
}
@@ -1060,11 +1062,11 @@ func TestExpireNode(t *testing.T) {
peerStatus.Expired,
)
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName()})
_, stderr, _ := client.Execute([]string{"tailscale", "ping", node.GetName().Or("")})
if !strings.Contains(stderr, "node key has expired") {
c.Errorf(
"expected to be unable to ping expired host %q from %q",
node.GetName(),
node.GetName().Or(""),
client.Hostname(),
)
}
@@ -1111,19 +1113,19 @@ func TestSetNodeExpiryInFuture(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node apiv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
require.True(t, node.GetExpiry().AsTime().After(time.Now()))
require.WithinDuration(t, targetExpiry, node.GetExpiry().AsTime(), 2*time.Second)
require.True(t, node.GetExpiry().Value.After(time.Now()))
require.WithinDuration(t, targetExpiry, node.GetExpiry().Value, 2*time.Second)
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText([]byte(node.GetNodeKey()))
err = nodeKey.UnmarshalText([]byte(node.GetNodeKey().Or("")))
require.NoError(t, err)
for _, client := range allClients {
if client.Hostname() == node.GetName() {
if client.Hostname() == node.GetName().Or("") {
continue
}
@@ -1208,10 +1210,10 @@ func TestDisableNodeExpiry(t *testing.T) {
)
require.NoError(t, err)
var node v1.Node
var node apiv1.Node
err = json.Unmarshal([]byte(result), &node)
require.NoError(t, err)
require.NotNil(t, node.GetExpiry(), "node should have an expiry set")
require.True(t, node.GetExpiry().IsSet(), "node should have an expiry set")
// Now disable the expiry.
result, err = headscale.Execute(
@@ -1224,23 +1226,23 @@ func TestDisableNodeExpiry(t *testing.T) {
)
require.NoError(t, err)
var nodeDisabled v1.Node
var nodeDisabled apiv1.Node
err = json.Unmarshal([]byte(result), &nodeDisabled)
require.NoError(t, err)
// Expiry should be nil (or zero time) when disabled.
if nodeDisabled.GetExpiry() != nil {
require.True(t, nodeDisabled.GetExpiry().AsTime().IsZero(),
if nodeDisabled.GetExpiry().IsSet() {
require.True(t, nodeDisabled.GetExpiry().Value.IsZero(),
"node expiry should be zero/nil after disabling")
}
var nodeKey key.NodePublic
err = nodeKey.UnmarshalText([]byte(nodeDisabled.GetNodeKey()))
err = nodeKey.UnmarshalText([]byte(nodeDisabled.GetNodeKey().Or("")))
require.NoError(t, err)
// Verify peers see the node as not expired.
for _, client := range allClients {
if client.Hostname() == nodeDisabled.GetName() {
if client.Hostname() == nodeDisabled.GetName().Or("") {
continue
}
@@ -1332,7 +1334,7 @@ func TestNodeOnlineStatus(t *testing.T) {
return
}
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
result, err := headscale.Execute([]string{
"headscale", "nodes", "list", "--output", "json",
@@ -1347,9 +1349,9 @@ func TestNodeOnlineStatus(t *testing.T) {
// All nodes should be online
assert.Truef(
ct,
node.GetOnline(),
node.GetOnline().Or(false),
"expected %s to have online status in Headscale, marked as offline %s after start",
node.GetName(),
node.GetName().Or(""),
time.Since(start),
)
}
@@ -1537,7 +1539,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
require.NoError(t, err)
// Test list all nodes after added otherUser
var nodeList []v1.Node
var nodeList []apiv1.Node
err = executeAndUnmarshal(
headscale,
[]string{
@@ -1551,8 +1553,8 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
)
require.NoError(t, err)
assert.Len(t, nodeList, 2)
assert.True(t, nodeList[0].GetOnline())
assert.True(t, nodeList[1].GetOnline())
assert.True(t, nodeList[0].GetOnline().Or(false))
assert.True(t, nodeList[1].GetOnline().Or(false))
// Delete the first node, which is online
_, err = headscale.Execute(
@@ -1562,7 +1564,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
"delete",
"--identifier",
// Delete the last added machine
fmt.Sprintf("%d", nodeList[0].GetId()),
fmt.Sprintf("%d", nodeList[0].GetID().Or(0)),
"--output",
"json",
"--force",
@@ -1571,7 +1573,7 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
require.NoError(t, err)
// Ensure that the node has been deleted, this did not occur due to a panic.
var nodeListAfter []v1.Node
var nodeListAfter []apiv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
err = executeAndUnmarshal(
headscale,
@@ -1601,6 +1603,6 @@ func Test2118DeletingOnlineNodePanics(t *testing.T) {
)
require.NoError(t, err)
assert.Len(t, nodeListAfter, 1)
assert.True(t, nodeListAfter[0].GetOnline())
assert.Equal(t, nodeList[1].GetId(), nodeListAfter[0].GetId())
assert.True(t, nodeListAfter[0].GetOnline().Or(false))
assert.Equal(t, nodeList[1].GetID().Or(0), nodeListAfter[0].GetID().Or(0))
}
+28 -21
View File
@@ -151,7 +151,8 @@ func TestGrantCapRelay(t *testing.T) {
require.NoError(t, err)
// --- Create Relay R on usernet3, dual-homed to usernet1+usernet2 ---
relayR, err := scenario.CreateTailscaleNode("head",
relayR, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet3),
)
require.NoError(t, err)
@@ -159,10 +160,10 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = relayR.Shutdown() }()
pakRelay, err := scenario.CreatePreAuthKeyWithTags(
userMap["relay"].GetId(), false, false, []string{"tag:relay"},
userMap["relay"].GetID().Or(0), false, false, []string{"tag:relay"},
)
require.NoError(t, err)
err = relayR.Login(headscale.GetEndpoint(), pakRelay.GetKey())
err = relayR.Login(headscale.GetEndpoint(), pakRelay.GetKey().Or(""))
require.NoError(t, err)
err = relayR.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -184,7 +185,8 @@ func TestGrantCapRelay(t *testing.T) {
require.NoError(t, err)
// --- Create Client A on usernet1 only ---
clientA, err := scenario.CreateTailscaleNode("head",
clientA, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -192,16 +194,17 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = clientA.Shutdown() }()
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
userMap["clienta"].GetId(), false, false, []string{"tag:client-a"},
userMap["clienta"].GetID().Or(0), false, false, []string{"tag:client-a"},
)
require.NoError(t, err)
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey().Or(""))
require.NoError(t, err)
err = clientA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// --- Create Client B on usernet2 only ---
clientB, err := scenario.CreateTailscaleNode("head",
clientB, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet2),
)
require.NoError(t, err)
@@ -209,10 +212,10 @@ func TestGrantCapRelay(t *testing.T) {
defer func() { _, _, _ = clientB.Shutdown() }()
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
userMap["clientb"].GetId(), false, false, []string{"tag:client-b"},
userMap["clientb"].GetID().Or(0), false, false, []string{"tag:client-b"},
)
require.NoError(t, err)
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey().Or(""))
require.NoError(t, err)
err = clientB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -629,7 +632,8 @@ func TestGrantCapDrive(t *testing.T) {
require.NoError(t, err)
// --- Create Sharer node ---
sharer, err := scenario.CreateTailscaleNode("head",
sharer, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -637,16 +641,17 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = sharer.Shutdown() }()
pakSharer, err := scenario.CreatePreAuthKeyWithTags(
userMap["sharer"].GetId(), false, false, []string{"tag:sharer"},
userMap["sharer"].GetID().Or(0), false, false, []string{"tag:sharer"},
)
require.NoError(t, err)
err = sharer.Login(headscale.GetEndpoint(), pakSharer.GetKey())
err = sharer.Login(headscale.GetEndpoint(), pakSharer.GetKey().Or(""))
require.NoError(t, err)
err = sharer.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// --- Create RW client node ---
rwClient, err := scenario.CreateTailscaleNode("head",
rwClient, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -654,16 +659,17 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = rwClient.Shutdown() }()
pakRW, err := scenario.CreatePreAuthKeyWithTags(
userMap["rwclient"].GetId(), false, false, []string{"tag:rw-client"},
userMap["rwclient"].GetID().Or(0), false, false, []string{"tag:rw-client"},
)
require.NoError(t, err)
err = rwClient.Login(headscale.GetEndpoint(), pakRW.GetKey())
err = rwClient.Login(headscale.GetEndpoint(), pakRW.GetKey().Or(""))
require.NoError(t, err)
err = rwClient.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// --- Create RO client node ---
roClient, err := scenario.CreateTailscaleNode("head",
roClient, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -671,16 +677,17 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = roClient.Shutdown() }()
pakRO, err := scenario.CreatePreAuthKeyWithTags(
userMap["roclient"].GetId(), false, false, []string{"tag:ro-client"},
userMap["roclient"].GetID().Or(0), false, false, []string{"tag:ro-client"},
)
require.NoError(t, err)
err = roClient.Login(headscale.GetEndpoint(), pakRO.GetKey())
err = roClient.Login(headscale.GetEndpoint(), pakRO.GetKey().Or(""))
require.NoError(t, err)
err = roClient.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// --- Create No-access node ---
noAccess, err := scenario.CreateTailscaleNode("head",
noAccess, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -688,10 +695,10 @@ func TestGrantCapDrive(t *testing.T) {
defer func() { _, _, _ = noAccess.Shutdown() }()
pakNA, err := scenario.CreatePreAuthKeyWithTags(
userMap["noaccess"].GetId(), false, false, []string{"tag:no-access"},
userMap["noaccess"].GetID().Or(0), false, false, []string{"tag:no-access"},
)
require.NoError(t, err)
err = noAccess.Login(headscale.GetEndpoint(), pakNA.GetKey())
err = noAccess.Login(headscale.GetEndpoint(), pakNA.GetKey().Or(""))
require.NoError(t, err)
err = noAccess.WaitForRunning(30 * time.Second)
require.NoError(t, err)
+10 -10
View File
@@ -18,7 +18,7 @@ import (
"github.com/cenkalti/backoff/v5"
"github.com/google/go-cmp/cmp"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -538,15 +538,15 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// assertLastSeenSet validates that a node has a non-nil LastSeen timestamp.
// Critical for ensuring node activity tracking is functioning properly.
func assertLastSeenSet(t *testing.T, node *v1.Node) {
func assertLastSeenSet(t *testing.T, node *apiv1.Node) {
t.Helper()
assert.NotNil(t, node)
assert.NotNil(t, node.GetLastSeen())
assert.True(t, node.GetLastSeen().Set)
}
func assertLastSeenSetWithCollect(c *assert.CollectT, node *v1.Node) {
func assertLastSeenSetWithCollect(c *assert.CollectT, node *apiv1.Node) {
assert.NotNil(c, node)
assert.NotNil(c, node.GetLastSeen())
assert.True(c, node.GetLastSeen().Set)
}
// assertCurlSuccessWithCollect asserts that a curl request succeeds with
@@ -1071,14 +1071,14 @@ func oidcMockUser(username string, emailVerified bool) mockoidc.MockUser {
// GetUserByName retrieves a user by name from the headscale server.
// This is a common pattern used when creating preauth keys or managing users.
func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
func GetUserByName(headscale ControlServer, username string) (*apiv1.User, error) {
users, err := headscale.ListUsers()
if err != nil {
return nil, fmt.Errorf("listing users: %w", err)
}
for _, u := range users {
if u.GetName() == username {
if u.GetName().Or("") == username {
return u, nil
}
}
@@ -1088,7 +1088,7 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
// findNode returns the first node in nodes for which match returns true,
// or nil if no node matches.
func findNode(nodes []*v1.Node, match func(*v1.Node) bool) *v1.Node {
func findNode(nodes []*apiv1.Node, match func(*apiv1.Node) bool) *apiv1.Node {
for _, n := range nodes {
if match(n) {
return n
@@ -1177,13 +1177,13 @@ func (s *Scenario) AddAndLoginClient(
return nil, fmt.Errorf("getting user: %w", err)
}
authKey, err := s.CreatePreAuthKey(user.GetId(), true, false)
authKey, err := s.CreatePreAuthKey(user.GetID().Or(0), true, false)
if err != nil {
return nil, fmt.Errorf("creating preauth key: %w", err)
}
// Login the new client
err = newClient.Login(headscale.GetEndpoint(), authKey.GetKey())
err = newClient.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
return nil, fmt.Errorf("logging in new client: %w", err)
}
+26 -25
View File
@@ -24,7 +24,7 @@ import (
"time"
"github.com/davecgh/go-spew/spew"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
@@ -1095,7 +1095,7 @@ func (t *HeadscaleInContainer) WaitForRunning() error {
// CreateUser adds a new user to the Headscale instance.
func (t *HeadscaleInContainer) CreateUser(
user string,
) (*v1.User, error) {
) (*apiv1.User, error) {
command := []string{
binHeadscale,
"users",
@@ -1115,7 +1115,7 @@ func (t *HeadscaleInContainer) CreateUser(
return nil, err
}
var u v1.User
var u apiv1.User
err = json.Unmarshal([]byte(result), &u)
if err != nil {
@@ -1140,7 +1140,7 @@ type AuthKeyOptions struct {
// CreateAuthKeyWithOptions creates a new "authorisation key" with the specified options.
// This supports both user-owned and tags-only auth keys.
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) {
func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*apiv1.PreAuthKey, error) {
command := []string{
binHeadscale,
}
@@ -1181,7 +1181,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v
return nil, fmt.Errorf("executing create auth key command: %w", err)
}
var preAuthKey v1.PreAuthKey
var preAuthKey apiv1.PreAuthKey
err = json.Unmarshal([]byte(result), &preAuthKey)
if err != nil {
@@ -1197,7 +1197,7 @@ func (t *HeadscaleInContainer) CreateAuthKey(
user uint64,
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
) (*apiv1.PreAuthKey, error) {
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
User: &user,
Reusable: reusable,
@@ -1212,7 +1212,7 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
reusable bool,
ephemeral bool,
tags []string,
) (*v1.PreAuthKey, error) {
) (*apiv1.PreAuthKey, error) {
return t.CreateAuthKeyWithOptions(AuthKeyOptions{
User: &user,
Reusable: reusable,
@@ -1252,8 +1252,8 @@ func (t *HeadscaleInContainer) DeleteAuthKey(
// specific users.
func (t *HeadscaleInContainer) ListNodes(
users ...string,
) ([]*v1.Node, error) {
var ret []*v1.Node
) ([]*apiv1.Node, error) {
var ret []*apiv1.Node
execUnmarshal := func(command []string) error {
result, _, err := dockertestutil.ExecuteCommand(
@@ -1265,7 +1265,7 @@ func (t *HeadscaleInContainer) ListNodes(
return fmt.Errorf("executing list node command: %w", err)
}
var nodes []*v1.Node
var nodes []*apiv1.Node
err = json.Unmarshal([]byte(result), &nodes)
if err != nil {
@@ -1293,8 +1293,8 @@ func (t *HeadscaleInContainer) ListNodes(
}
}
slices.SortFunc(ret, func(a, b *v1.Node) int {
return cmp.Compare(a.GetId(), b.GetId())
slices.SortFunc(ret, func(a, b *apiv1.Node) int {
return cmp.Compare(a.GetID().Or(0), b.GetID().Or(0))
})
return ret, nil
@@ -1324,37 +1324,38 @@ func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
return nil
}
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*v1.Node, error) {
func (t *HeadscaleInContainer) NodesByUser() (map[string][]*apiv1.Node, error) {
nodes, err := t.ListNodes()
if err != nil {
return nil, err
}
userMap := make(map[string][]*v1.Node)
userMap := make(map[string][]*apiv1.Node)
for _, node := range nodes {
name := node.GetUser().GetName()
user := node.GetUser().Value
name := user.GetName().Or("")
userMap[name] = append(userMap[name], node)
}
return userMap, nil
}
func (t *HeadscaleInContainer) NodesByName() (map[string]*v1.Node, error) {
func (t *HeadscaleInContainer) NodesByName() (map[string]*apiv1.Node, error) {
nodes, err := t.ListNodes()
if err != nil {
return nil, err
}
var nameMap map[string]*v1.Node
var nameMap map[string]*apiv1.Node
for _, node := range nodes {
mak.Set(&nameMap, node.GetName(), node)
mak.Set(&nameMap, node.GetName().Or(""), node)
}
return nameMap, nil
}
// ListUsers returns a list of users from Headscale.
func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
func (t *HeadscaleInContainer) ListUsers() ([]*apiv1.User, error) {
command := []string{binHeadscale, "users", "list", flagOutput, "json"}
result, _, err := dockertestutil.ExecuteCommand(
@@ -1366,7 +1367,7 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
return nil, fmt.Errorf("executing list node command: %w", err)
}
var users []*v1.User
var users []*apiv1.User
err = json.Unmarshal([]byte(result), &users)
if err != nil {
@@ -1378,15 +1379,15 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
// MapUsers returns a map of users from Headscale. It is keyed by the
// user name.
func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) {
func (t *HeadscaleInContainer) MapUsers() (map[string]*apiv1.User, error) {
users, err := t.ListUsers()
if err != nil {
return nil, err
}
var userMap map[string]*v1.User
var userMap map[string]*apiv1.User
for _, user := range users {
mak.Set(&userMap, user.GetName(), user)
mak.Set(&userMap, user.GetName().Or(""), user)
}
return userMap, nil
@@ -1545,7 +1546,7 @@ func (h *HeadscaleInContainer) Restart() error {
}
// ApproveRoutes approves routes for a node.
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*v1.Node, error) {
func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (*apiv1.Node, error) {
command := []string{
binHeadscale, "nodes", "approve-routes",
flagOutput, "json",
@@ -1567,7 +1568,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
)
}
var node *v1.Node
var node *apiv1.Node
err = json.Unmarshal([]byte(result), &node)
if err != nil {
+148 -129
View File
@@ -15,7 +15,7 @@ import (
cmpdiff "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
@@ -54,7 +54,8 @@ func TestEnablingRoutes(t *testing.T) {
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{tsic.WithAcceptRoutes()},
hsic.WithTestName("rt-enable"))
hsic.WithTestName("rt-enable"),
)
requireNoErrHeadscaleEnv(t, err)
allClients, err := scenario.ListTailscaleClients()
@@ -87,7 +88,7 @@ func TestEnablingRoutes(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -119,7 +120,7 @@ func TestEnablingRoutes(t *testing.T) {
for _, node := range nodes {
_, err := headscale.ApproveRoutes(
node.GetId(),
node.GetID().Or(0),
util.MustStringsToPrefixes(node.GetAvailableRoutes()),
)
require.NoError(t, err)
@@ -181,11 +182,11 @@ func TestEnablingRoutes(t *testing.T) {
assert.NoError(c, err)
for _, node := range nodes {
if node.GetId() == 1 {
if node.GetID().Or(0) == 1 {
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.0.0/24
assert.Len(c, node.GetApprovedRoutes(), 1) // 10.0.1.0/24
assert.Empty(c, node.GetSubnetRoutes())
} else if node.GetId() == 2 {
} else if node.GetID().Or(0) == 2 {
assert.Len(c, node.GetAvailableRoutes(), 1) // 10.0.1.0/24
assert.Empty(c, node.GetApprovedRoutes())
assert.Empty(c, node.GetSubnetRoutes())
@@ -333,7 +334,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
requireNoErrSync(t, err)
// Wait for route configuration changes after advertising routes
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -400,7 +401,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Client can access webservice through router 1 only")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -450,7 +451,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
if srs1PeerStatus.PrimaryRoutes != nil {
t.Logf("got list: %v, want in: %v", srs1PeerStatus.PrimaryRoutes.AsSlice(), pref)
assert.Contains(c,
assert.Contains(
c,
srs1PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -479,11 +481,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 is primary
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
// Note: Router 2 and 3 are available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 1 should be primary for route "+pref.String())
@@ -498,7 +500,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: HA is now active - if router 1 fails, router 2 can take over")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -549,7 +551,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
if srs1PeerStatus.PrimaryRoutes != nil {
t.Logf("got list: %v, want in: %v", srs1PeerStatus.PrimaryRoutes.AsSlice(), pref)
assert.Contains(c,
assert.Contains(
c,
srs1PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -559,12 +562,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 still primary, router 2 approved but standby
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
// Note: Router 3 is available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 1 should remain primary after router 2 approval")
@@ -594,12 +597,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 primary, router 2 approved (standby)
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
// Note: Router 3 is available but not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 1 primary with router 2 as standby")
@@ -615,7 +618,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Full HA configuration with 1 PRIMARY + 2 STANDBY routers")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter3.Hostname(), nodes).GetId(),
MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
@@ -664,7 +667,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
if srs1PeerStatus.PrimaryRoutes != nil {
t.Logf("got list: %v, want in: %v", srs1PeerStatus.PrimaryRoutes.AsSlice(), pref)
assert.Contains(c,
assert.Contains(
c,
srs1PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -702,12 +706,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - all 3 routers approved, router 1 still primary
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 1 primary with all 3 routers approved")
@@ -755,7 +759,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
requirePeerSubnetRoutesWithCollect(c, srs3PeerStatus, nil)
if srs2PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs2PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -782,11 +787,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Router 1 is disconnected, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 2 should be primary after router 1 failure")
@@ -854,10 +859,10 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Routers 1 and 2 are disconnected, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 3 should be primary after router 2 failure")
@@ -905,7 +910,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
requirePeerSubnetRoutesWithCollect(c, srs3PeerStatus, []netip.Prefix{pref})
if srs3PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs3PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -931,12 +937,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 3 remains primary after router 1 comes back
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
// Router 2 is still disconnected
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 3 should remain primary after router 1 recovery")
@@ -986,7 +992,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
requirePeerSubnetRoutesWithCollect(c, srs3PeerStatus, []netip.Prefix{pref})
if srs3PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs3PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -1012,12 +1019,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 3 remains primary after all routers back online
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 3 should remain primary after full recovery")
@@ -1030,7 +1037,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 1 (%s) should become new PRIMARY (lowest ID with approved route)", subRouter1.Hostname())
t.Logf(" Expected: Router 2 (%s) remains STANDBY", subRouter2.Hostname())
t.Logf(" Expected: Router 3 (%s) goes to advertised-only state (no longer serving)", subRouter3.Hostname())
_, err = headscale.ApproveRoutes(MustFindNode(subRouter3.Hostname(), nodes).GetId(), []netip.Prefix{})
_, err = headscale.ApproveRoutes(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0), []netip.Prefix{})
// Wait for nodestore batch processing and route state changes to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
@@ -1071,7 +1078,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
requirePeerSubnetRoutesWithCollect(c, srs3PeerStatus, nil)
if srs1PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs1PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -1097,12 +1105,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state - router 1 is primary after router 3 route disabled
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
// Router 3's route is no longer approved, so not in AvailableRoutes
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 1 should be primary after router 3 route disabled")
@@ -1116,7 +1124,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 (%s) should become new PRIMARY (only remaining approved route)", subRouter2.Hostname())
t.Logf(" Expected: Router 1 (%s) goes to advertised-only state", subRouter1.Hostname())
t.Logf(" Expected: Router 3 (%s) remains advertised-only", subRouter3.Hostname())
_, err = headscale.ApproveRoutes(MustFindNode(subRouter1.Hostname(), nodes).GetId(), []netip.Prefix{})
_, err = headscale.ApproveRoutes(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0), []netip.Prefix{})
// Wait for nodestore batch processing and route state changes to complete
// [state.NodeStore] batching timeout is 500ms, so we wait up to 10 seconds for route failover
@@ -1157,7 +1165,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
requirePeerSubnetRoutesWithCollect(c, srs3PeerStatus, nil)
if srs2PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs2PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -1184,11 +1193,11 @@ func TestHASubnetRouterFailover(t *testing.T) {
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
// Router 1's route is no longer approved, so not in AvailableRoutes
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
// Router 3's route is still not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 2 should be primary after router 1 route disabled")
@@ -1205,7 +1214,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
r1Node := MustFindNode(subRouter1.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r1Node.GetId(),
r1Node.GetID().Or(0),
util.MustStringsToPrefixes(r1Node.GetAvailableRoutes()),
)
@@ -1242,7 +1251,8 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.Nil(c, srs3PeerStatus.PrimaryRoutes)
if srs2PeerStatus.PrimaryRoutes != nil {
assert.Contains(c,
assert.Contains(
c,
srs2PeerStatus.PrimaryRoutes.AsSlice(),
pref,
)
@@ -1268,12 +1278,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state after router 1 re-approval
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
// Router 3 route is still not approved
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 2 should remain primary after router 1 re-approval")
@@ -1290,7 +1300,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
r3Node := MustFindNode(subRouter3.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r3Node.GetId(),
r3Node.GetID().Or(0),
util.MustStringsToPrefixes(r3Node.GetAvailableRoutes()),
)
@@ -1310,12 +1320,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Validate primary routes table state after router 3 re-approval
validatePrimaryRoutes(t, headscale, &types.DebugRoutes{
AvailableRoutes: map[types.NodeID][]netip.Prefix{
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetId()): {pref},
types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)): {pref},
types.NodeID(MustFindNode(subRouter3.Hostname(), nodes).GetID().Or(0)): {pref},
},
PrimaryRoutes: map[string]types.NodeID{
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId()),
pref.String(): types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0)),
},
}, "Router 2 should remain primary after router 3 re-approval")
@@ -1426,7 +1436,7 @@ func TestSubnetRouteACL(t *testing.T) {
requireNoErrSync(t, err)
// Wait for route advertisements to propagate to the server
var nodes []*v1.Node
var nodes []*apiv1.Node
require.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -1437,12 +1447,12 @@ func TestSubnetRouteACL(t *testing.T) {
// Find the node that should have the route by checking node IDs
var (
routeNode *v1.Node
otherNode *v1.Node
routeNode *apiv1.Node
otherNode *apiv1.Node
)
for _, node := range nodes {
nodeIDStr := strconv.FormatUint(node.GetId(), 10)
nodeIDStr := strconv.FormatUint(node.GetID().Or(0), 10)
if _, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
routeNode = node
} else {
@@ -1623,7 +1633,7 @@ func TestEnablingExitRoutes(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
@@ -1654,12 +1664,12 @@ func TestEnablingExitRoutes(t *testing.T) {
// Enable all routes, but do v4 on one and v6 on other to ensure they
// are both added since they are exit routes.
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
nodes[0].GetID().Or(0),
[]netip.Prefix{tsaddr.AllIPv4()},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
nodes[1].GetId(),
nodes[1].GetID().Or(0),
[]netip.Prefix{tsaddr.AllIPv6()},
)
require.NoError(t, err)
@@ -1754,7 +1764,7 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
// so the standard WaitForTailscaleSync wait would deadlock here —
// the post-approval [assert.EventuallyWithT] block below covers the peer
// state we actually care about.
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -1770,12 +1780,12 @@ func TestExitRoutesWithAutogroupInternetACL(t *testing.T) {
// alice's exit. The bug fix is about visibility, not which node
// is chosen.
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
nodes[0].GetID().Or(0),
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
nodes[1].GetId(),
nodes[1].GetID().Or(0),
[]netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
)
require.NoError(t, err)
@@ -1859,7 +1869,8 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{tsic.WithAcceptRoutes()},
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{tsic.WithAcceptRoutes()},
hsic.WithTestName("rt-multinet"),
)
requireNoErrHeadscaleEnv(t, err)
@@ -1902,7 +1913,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
_, _, err = user1c.Execute(command)
require.NoErrorf(t, err, "failed to advertise route: %s", err)
var nodes []*v1.Node
var nodes []*apiv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -1929,7 +1940,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
// Enable route
_, err = headscale.ApproveRoutes(
nodes[0].GetId(),
nodes[0].GetID().Or(0),
[]netip.Prefix{*pref},
)
require.NoError(t, err)
@@ -2010,7 +2021,8 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnv([]tsic.Option{},
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("rt-multinetexit"),
)
requireNoErrHeadscaleEnv(t, err)
@@ -2058,7 +2070,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
_, _, err = user1c.Execute(command)
require.NoErrorf(t, err, "failed to advertise routes: %s", err)
var nodes []*v1.Node
var nodes []*apiv1.Node
// Wait for route advertisements to propagate (3 routes: v4 exit + v6 exit + subnet).
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -2084,7 +2096,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
}, integrationutil.ScaledTimeout(5*time.Second), integrationutil.FastPoll, "Verifying no routes sent to client before approval")
// Approve exit routes and subnet route.
_, err = headscale.ApproveRoutes(nodes[0].GetId(), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
_, err = headscale.ApproveRoutes(nodes[0].GetID().Or(0), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6(), *route})
require.NoError(t, err)
// Wait for route state changes to propagate.
@@ -2151,9 +2163,9 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
}, 10*time.Second, 200*time.Millisecond, "user2 traceroute should go through user1 exit node")
}
func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
func MustFindNode(hostname string, nodes []*apiv1.Node) *apiv1.Node {
for _, node := range nodes {
if node.GetName() == hostname {
if node.GetName().Or("") == hostname {
return node
}
}
@@ -2447,7 +2459,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
var nodes []*v1.Node
var nodes []*apiv1.Node
opts := []hsic.Option{
hsic.WithTestName("autoapprovemulti"),
@@ -2477,7 +2489,8 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
}
}
err = scenario.createHeadscaleEnvWithTags(tt.withURL, tsOpts, preAuthKeyTags, webauthTagUser,
err = scenario.createHeadscaleEnvWithTags(
tt.withURL, tsOpts, preAuthKeyTags, webauthTagUser,
opts...,
)
requireNoErrHeadscaleEnv(t, err)
@@ -2518,7 +2531,8 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
require.NoError(t, err)
if advertiseDuringUp {
tsOpts = append(tsOpts,
tsOpts = append(
tsOpts,
tsic.WithExtraLoginArgs([]string{"--advertise-routes=" + route.String()}),
)
}
@@ -2565,16 +2579,16 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// If the approver is a tag, create a tagged PreAuthKey
// (tags-as-identity model: tags come from PreAuthKey, not --advertise-tags)
var pak *v1.PreAuthKey
var pak *apiv1.PreAuthKey
if strings.HasPrefix(tt.approver, "tag:") {
pak, err = scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), false, false, []string{tt.approver})
pak, err = scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetID().Or(0), false, false, []string{tt.approver})
} else {
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetID().Or(0), false, false)
}
require.NoError(t, err)
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey().Or(""))
require.NoError(t, err)
}
// extra creation end.
@@ -2646,7 +2660,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
t.Logf("Initial auto-approval check - Router node %s: announced=%v, approved=%v, subnet=%v",
routerNode.GetName(),
routerNode.GetName().Or(""),
routerNode.GetAvailableRoutes(),
routerNode.GetApprovedRoutes(),
routerNode.GetSubnetRoutes())
@@ -2740,7 +2754,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
routerNode := MustFindNode(routerUsernet1.Hostname(), nodes)
t.Logf("After policy removal - Router node %s: announced=%v, approved=%v, subnet=%v",
routerNode.GetName(),
routerNode.GetName().Or(""),
routerNode.GetAvailableRoutes(),
routerNode.GetApprovedRoutes(),
routerNode.GetSubnetRoutes())
@@ -2791,7 +2805,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// Disable the route, making it unavailable since it is no longer auto-approved
_, err = headscale.ApproveRoutes(
MustFindNode(routerUsernet1.Hostname(), nodes).GetId(),
MustFindNode(routerUsernet1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{},
)
require.NoError(t, err)
@@ -3078,10 +3092,10 @@ func requirePeerSubnetRoutesWithCollect(c *assert.CollectT, status *ipnstate.Pee
}
}
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *v1.Node, announced, approved, subnet int) {
assert.Lenf(c, node.GetAvailableRoutes(), announced, "expected %q announced routes(%v) to have %d route, had %d", node.GetName(), node.GetAvailableRoutes(), announced, len(node.GetAvailableRoutes()))
assert.Lenf(c, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName(), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
assert.Lenf(c, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName(), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
func requireNodeRouteCountWithCollect(c *assert.CollectT, node *apiv1.Node, announced, approved, subnet int) {
assert.Lenf(c, node.GetAvailableRoutes(), announced, "expected %q announced routes(%v) to have %d route, had %d", node.GetName().Or(""), node.GetAvailableRoutes(), announced, len(node.GetAvailableRoutes()))
assert.Lenf(c, node.GetApprovedRoutes(), approved, "expected %q approved routes(%v) to have %d route, had %d", node.GetName().Or(""), node.GetApprovedRoutes(), approved, len(node.GetApprovedRoutes()))
assert.Lenf(c, node.GetSubnetRoutes(), subnet, "expected %q subnet routes(%v) to have %d route, had %d", node.GetName().Or(""), node.GetSubnetRoutes(), subnet, len(node.GetSubnetRoutes()))
}
// TestSubnetRouteACLFiltering tests that a node can only access subnet routes
@@ -3159,9 +3173,10 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
err = json.Unmarshal([]byte(aclPolicyStr), aclPolicy)
require.NoError(t, err)
err = scenario.CreateHeadscaleEnv([]tsic.Option{
tsic.WithAcceptRoutes(),
}, hsic.WithTestName("routeaclfilter"),
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{
tsic.WithAcceptRoutes(),
}, hsic.WithTestName("routeaclfilter"),
hsic.WithACLPolicy(aclPolicy),
hsic.WithPolicyMode(types.PolicyModeDB), // test updates policy at runtime via CLI
)
@@ -3218,7 +3233,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var routerNode, nodeNode *v1.Node
var routerNode, nodeNode *apiv1.Node
// Wait for route advertisements to propagate to [state.NodeStore]
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
// List nodes and verify the router has 3 available routes
@@ -3240,7 +3255,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
// Approve all routes for the router
_, err = headscale.ApproveRoutes(
routerNode.GetId(),
routerNode.GetID().Or(0),
util.MustStringsToPrefixes(routerNode.GetAvailableRoutes()),
)
require.NoError(t, err)
@@ -3401,7 +3416,8 @@ func TestGrantViaSubnetSteering(t *testing.T) {
// co-router visibility the HA primary's subnet appears in the
// co-router's AllowedIPs, and --accept-routes would install a
// system route that conflicts with local subnet forwarding.
routerA, err := scenario.CreateTailscaleNode("head",
routerA, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -3409,16 +3425,17 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = routerA.Shutdown() }()
pakRouterA, err := scenario.CreatePreAuthKeyWithTags(
userMap["router"].GetId(), false, false, []string{"tag:router-a"},
userMap["router"].GetID().Or(0), false, false, []string{"tag:router-a"},
)
require.NoError(t, err)
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.GetKey())
err = routerA.Login(headscale.GetEndpoint(), pakRouterA.GetKey().Or(""))
require.NoError(t, err)
err = routerA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// Create Router B (tag:router-b) on usernet1.
routerB, err := scenario.CreateTailscaleNode("head",
routerB, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet1),
)
require.NoError(t, err)
@@ -3426,16 +3443,17 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = routerB.Shutdown() }()
pakRouterB, err := scenario.CreatePreAuthKeyWithTags(
userMap["router"].GetId(), false, false, []string{"tag:router-b"},
userMap["router"].GetID().Or(0), false, false, []string{"tag:router-b"},
)
require.NoError(t, err)
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.GetKey())
err = routerB.Login(headscale.GetEndpoint(), pakRouterB.GetKey().Or(""))
require.NoError(t, err)
err = routerB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// Create Client A (tag:group-a) on usernet2.
clientA, err := scenario.CreateTailscaleNode("head",
clientA, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet2),
tsic.WithAcceptRoutes(),
)
@@ -3444,16 +3462,17 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = clientA.Shutdown() }()
pakClientA, err := scenario.CreatePreAuthKeyWithTags(
userMap["client"].GetId(), false, false, []string{"tag:group-a"},
userMap["client"].GetID().Or(0), false, false, []string{"tag:group-a"},
)
require.NoError(t, err)
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey())
err = clientA.Login(headscale.GetEndpoint(), pakClientA.GetKey().Or(""))
require.NoError(t, err)
err = clientA.WaitForRunning(30 * time.Second)
require.NoError(t, err)
// Create Client B (tag:group-b) on usernet2.
clientB, err := scenario.CreateTailscaleNode("head",
clientB, err := scenario.CreateTailscaleNode(
"head",
tsic.WithNetwork(usernet2),
tsic.WithAcceptRoutes(),
)
@@ -3462,10 +3481,10 @@ func TestGrantViaSubnetSteering(t *testing.T) {
defer func() { _, _, _ = clientB.Shutdown() }()
pakClientB, err := scenario.CreatePreAuthKeyWithTags(
userMap["client"].GetId(), false, false, []string{"tag:group-b"},
userMap["client"].GetID().Or(0), false, false, []string{"tag:group-b"},
)
require.NoError(t, err)
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey())
err = clientB.Login(headscale.GetEndpoint(), pakClientB.GetKey().Or(""))
require.NoError(t, err)
err = clientB.WaitForRunning(30 * time.Second)
require.NoError(t, err)
@@ -3497,7 +3516,7 @@ func TestGrantViaSubnetSteering(t *testing.T) {
routerANode := MustFindNode(routerA.Hostname(), nodes)
t.Logf("Router A %s: announced=%v, approved=%v, subnet=%v",
routerANode.GetName(),
routerANode.GetName().Or(""),
routerANode.GetAvailableRoutes(),
routerANode.GetApprovedRoutes(),
routerANode.GetSubnetRoutes())
@@ -3506,7 +3525,7 @@ func TestGrantViaSubnetSteering(t *testing.T) {
routerBNode := MustFindNode(routerB.Hostname(), nodes)
t.Logf("Router B %s: announced=%v, approved=%v, subnet=%v",
routerBNode.GetName(),
routerBNode.GetName().Or(""),
routerBNode.GetAvailableRoutes(),
routerBNode.GetApprovedRoutes(),
routerBNode.GetSubnetRoutes())
@@ -3697,7 +3716,7 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -3707,19 +3726,19 @@ func TestHASubnetRouterPingFailover(t *testing.T) {
// Approve routes on both routers.
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0))
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0))
// Wait for HA to be set up: router 1 primary, router 2 standby.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -3937,7 +3956,7 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -3947,19 +3966,19 @@ func TestHASubnetRouterFailoverBothOffline(t *testing.T) {
// Approve the route on both routers explicitly.
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0))
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0))
// Sanity: r1 starts as primary (lower NodeID).
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -4142,7 +4161,7 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -4151,18 +4170,18 @@ func TestHASubnetRouterFailoverBothOfflineCablePull(t *testing.T) {
}, propagationTime, 200*time.Millisecond, "nodes registered")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0))
// Sanity: r1 starts as primary.
assert.EventuallyWithT(t, func(c *assert.CollectT) {
@@ -4366,7 +4385,7 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
var nodes []*v1.Node
var nodes []*apiv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
@@ -4375,19 +4394,19 @@ func TestHASubnetRouterFailoverDockerDisconnect(t *testing.T) {
}, propagationTime, 200*time.Millisecond, "nodes registered")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0),
[]netip.Prefix{pref},
)
require.NoError(t, err)
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetId())
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetId())
nodeID1 := types.NodeID(MustFindNode(subRouter1.Hostname(), nodes).GetID().Or(0))
nodeID2 := types.NodeID(MustFindNode(subRouter2.Hostname(), nodes).GetID().Or(0))
// requirePrimary blocks until headscale reports want as the
// primary advertiser for pref.
+10 -10
View File
@@ -21,7 +21,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
"github.com/juanfont/headscale/hscontrol/capver"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/dockertestutil"
@@ -520,7 +520,7 @@ func (s *Scenario) CreatePreAuthKey(
user uint64,
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
) (*apiv1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
if err != nil {
@@ -535,7 +535,7 @@ func (s *Scenario) CreatePreAuthKey(
// CreatePreAuthKeyWithOptions creates a "pre authorised key" with the specified options
// to be created in the Headscale instance on behalf of the [Scenario].
func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error) {
func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*apiv1.PreAuthKey, error) {
headscale, err := s.Headscale()
if err != nil {
return nil, fmt.Errorf("creating preauth key with options: %w", errNoHeadscaleAvailable)
@@ -556,7 +556,7 @@ func (s *Scenario) CreatePreAuthKeyWithTags(
reusable bool,
ephemeral bool,
tags []string,
) (*v1.PreAuthKey, error) {
) (*apiv1.PreAuthKey, error) {
headscale, err := s.Headscale()
if err != nil {
return nil, fmt.Errorf("creating preauth key with tags: %w", errNoHeadscaleAvailable)
@@ -572,7 +572,7 @@ func (s *Scenario) CreatePreAuthKeyWithTags(
// CreateUser creates a [User] to be created in the
// Headscale instance on behalf of the [Scenario].
func (s *Scenario) CreateUser(user string) (*v1.User, error) {
func (s *Scenario) CreateUser(user string) (*apiv1.User, error) {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
u, err := headscale.CreateUser(user)
if err != nil {
@@ -925,7 +925,7 @@ func (s *Scenario) createHeadscaleEnvWithTags(
}
for _, user := range s.spec.Users {
var u *v1.User
var u *apiv1.User
if s.spec.OIDCSkipUserCreation {
// Only register locally — OIDC login will create the headscale user.
@@ -964,18 +964,18 @@ func (s *Scenario) createHeadscaleEnvWithTags(
}
} else {
// Use tagged PreAuthKey if tags are provided (tags-as-identity model)
var key *v1.PreAuthKey
var key *apiv1.PreAuthKey
if len(preAuthKeyTags) > 0 {
key, err = s.CreatePreAuthKeyWithTags(u.GetId(), true, false, preAuthKeyTags)
key, err = s.CreatePreAuthKeyWithTags(u.GetID().Or(0), true, false, preAuthKeyTags)
} else {
key, err = s.CreatePreAuthKey(u.GetId(), true, false)
key, err = s.CreatePreAuthKey(u.GetID().Or(0), true, false)
}
if err != nil {
return err
}
err = s.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey())
err = s.RunTailscaleUp(user, headscale.GetEndpoint(), key.GetKey().Or(""))
if err != nil {
return err
}
+1 -1
View File
@@ -137,7 +137,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
err = scenario.RunTailscaleUp(
user,
headscale.GetEndpoint(),
key.GetKey(),
key.GetKey().Or(""),
)
if err != nil {
t.Fatalf("failed to login: %s", err)
+78 -78
View File
@@ -5,7 +5,7 @@ import (
"testing"
"time"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
apiv1 "github.com/juanfont/headscale/gen/api/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/hsic"
@@ -67,19 +67,19 @@ func tagsEqual(actual, expected []string) bool {
}
// assertNodeHasTagsWithCollect asserts that a node has exactly the expected tags (order-independent).
func assertNodeHasTagsWithCollect(c *assert.CollectT, node *v1.Node, expectedTags []string) {
func assertNodeHasTagsWithCollect(c *assert.CollectT, node *apiv1.Node, expectedTags []string) {
actualTags := node.GetTags()
sortedActual := append([]string{}, actualTags...)
sortedExpected := append([]string{}, expectedTags...)
sort.Strings(sortedActual)
sort.Strings(sortedExpected)
assert.Equal(c, sortedExpected, sortedActual, "Node %s tags mismatch", node.GetName())
assert.Equal(c, sortedExpected, sortedActual, "Node %s tags mismatch", node.GetName().Or(""))
}
// assertNodeHasNoTagsWithCollect asserts that a node has no tags.
func assertNodeHasNoTagsWithCollect(c *assert.CollectT, node *v1.Node) {
assert.Empty(c, node.GetTags(), "Node %s should have no tags, but has: %v", node.GetName(), node.GetTags())
func assertNodeHasNoTagsWithCollect(c *assert.CollectT, node *apiv1.Node) {
assert.Empty(c, node.GetTags(), "Node %s should have no tags, but has: %v", node.GetName().Or(""), node.GetTags())
}
// assertNodeSelfHasTagsWithCollect asserts that a client's self view has exactly the expected tags.
@@ -148,7 +148,7 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -164,7 +164,7 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because the advertised tags don't match the auth key's tags
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
// Document actual behavior - we expect this to fail
if err != nil {
@@ -222,7 +222,7 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -238,7 +238,7 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) {
require.NoError(t, err)
// Login with the tagged PreAuthKey
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for node to be registered and verify it has the key's tags
@@ -294,7 +294,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -308,7 +308,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration
@@ -328,7 +328,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=tag:valid-owned,tag:second",
}
_, stderr, err := client.Execute(command)
@@ -394,7 +394,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -408,7 +408,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration
@@ -424,7 +424,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=tag:second",
}
_, stderr, err := client.Execute(command)
@@ -490,7 +490,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, true, false, []string{"tag:valid-owned"})
@@ -504,7 +504,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -516,7 +516,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -549,7 +549,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--force-reauth",
}
//nolint:errcheck // Intentionally ignoring error - we check results below
@@ -617,7 +617,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, true, false, []string{"tag:valid-owned"})
@@ -631,7 +631,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -643,7 +643,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -672,7 +672,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -739,7 +739,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -755,7 +755,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) {
require.NoError(t, err)
// Login should fail because the auth key has no tags
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 3.1 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -810,7 +810,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -824,7 +824,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) {
require.NoError(t, err)
// Login should succeed
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Verify node has no tags
@@ -879,7 +879,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -893,7 +893,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration
@@ -913,7 +913,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -978,7 +978,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -992,7 +992,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1004,7 +1004,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
assertNodeHasNoTagsWithCollect(c, nodes[0])
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1034,7 +1034,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--reset",
}
_, stderr, err := client.Execute(command)
@@ -1098,7 +1098,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -1112,7 +1112,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1124,7 +1124,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1153,7 +1153,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T)
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=",
}
_, stderr, err := client.Execute(command)
@@ -1217,7 +1217,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, true, false)
@@ -1231,7 +1231,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
require.NoError(t, err)
// Initial login
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for initial registration and get node ID
@@ -1243,7 +1243,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1272,7 +1272,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) {
command := []string{
"tailscale", "up",
"--login-server=" + headscale.GetEndpoint(),
"--authkey=" + authKey.GetKey(),
"--authkey=" + authKey.GetKey().Or(""),
"--advertise-tags=tag:valid-owned",
}
_, stderr, err := client.Execute(command)
@@ -1757,7 +1757,7 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
t.Logf("Step 1: Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1874,7 +1874,7 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -1965,7 +1965,7 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -1981,7 +1981,7 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 2.7 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2035,7 +2035,7 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey with tag:valid-owned
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2051,7 +2051,7 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 2.8 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2109,7 +2109,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -2125,7 +2125,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 3.7 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2179,7 +2179,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create an auth key WITHOUT tags
authKey, err := scenario.CreatePreAuthKey(userID, false, false)
@@ -2195,7 +2195,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 3.8 PASS: Registration correctly rejected: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -2253,7 +2253,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2266,7 +2266,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2278,7 +2278,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2325,7 +2325,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2338,7 +2338,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2350,7 +2350,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2413,7 +2413,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2426,7 +2426,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2438,7 +2438,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2560,7 +2560,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration")
@@ -2735,7 +2735,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
userMap, err := headscale.MapUsers()
require.NoError(t, err)
userID := userMap[tagTestUser].GetId()
userID := userMap[tagTestUser].GetID().Or(0)
// Create a tagged PreAuthKey to register a node
authKey, err := scenario.CreatePreAuthKeyWithTags(userID, false, false, []string{"tag:valid-owned"})
@@ -2748,7 +2748,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for registration and get node ID
@@ -2760,7 +2760,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) {
assert.Len(c, nodes, 1)
if len(nodes) == 1 {
nodeID = nodes[0].GetId()
nodeID = nodes[0].GetID().Or(0)
t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration")
@@ -2864,9 +2864,9 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
initialNodeID = node.GetId()
initialNodeID = node.GetID().Or(0)
t.Logf("Initial state - Node ID: %d, Tags: %v, User: %s",
node.GetId(), node.GetTags(), node.GetUser().GetName())
node.GetID().Or(0), node.GetTags(), node.GetUser().Value.Name.Or(""))
// Verify node has the expected tags
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned", "tag:second"})
@@ -2927,24 +2927,24 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) {
if len(nodes) >= 1 {
node := nodes[0]
t.Logf("After reauth - Node ID: %d, Tags: %v, User: %s",
node.GetId(), node.GetTags(), node.GetUser().GetName())
node.GetID().Or(0), node.GetTags(), node.GetUser().Value.Name.Or(""))
// Assert: Node should have NO tags
assertNodeHasNoTagsWithCollect(c, node)
// Assert: Node should be owned by the user (not tagged-devices)
assert.Equal(c, tagTestUser, node.GetUser().GetName(),
assert.Equal(c, tagTestUser, node.GetUser().Value.Name.Or(""),
"Node ownership should return to user %s after untagging", tagTestUser)
// Verify the node ID is still the same (not a new registration)
assert.Equal(c, initialNodeID, node.GetId(),
assert.Equal(c, initialNodeID, node.GetID().Or(0),
"Node ID should remain the same after reauth")
if len(node.GetTags()) == 0 && node.GetUser().GetName() == tagTestUser {
if len(node.GetTags()) == 0 && node.GetUser().Value.Name.Or("") == tagTestUser {
t.Logf("Test #2979 (%s) PASS: Node successfully untagged and ownership returned to user", tc.name)
} else {
t.Logf("Test #2979 (%s) FAIL: Expected no tags and user=%s, got tags=%v user=%s",
tc.name, tagTestUser, node.GetTags(), node.GetUser().GetName())
tc.name, tagTestUser, node.GetTags(), node.GetUser().Value.Name.Or(""))
}
}
}, integrationutil.HAConvergeTimeout, 1*time.Second, "verifying tags removed and ownership returned")
@@ -3005,7 +3005,7 @@ func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) {
require.NoError(t, err)
// Login with the tags-only auth key
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
// Wait for node to be registered and verify it has the key's tags
@@ -3076,7 +3076,7 @@ func TestTagsAuthKeyWithoutUserRejectsAdvertisedTags(t *testing.T) {
require.NoError(t, err)
// Login should fail because ANY advertise-tags is rejected for PreAuthKey registrations
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
if err != nil {
t.Logf("Test 5.2 PASS: Registration correctly rejected with error: %v", err)
assert.ErrorContains(t, err, "requested tags")
@@ -3142,7 +3142,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
)
require.NoError(t, err)
err = client.Login(headscale.GetEndpoint(), authKey.GetKey())
err = client.Login(headscale.GetEndpoint(), authKey.GetKey().Or(""))
require.NoError(t, err)
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
@@ -3156,7 +3156,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
if len(nodes) == 1 {
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
t.Logf("Initial state - Node ID: %d, Tags: %v", nodes[0].GetId(), nodes[0].GetTags())
t.Logf("Initial state - Node ID: %d, Tags: %v", nodes[0].GetID().Or(0), nodes[0].GetTags())
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "node should be tagged initially")
@@ -3196,10 +3196,10 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
if len(nodes) == 1 {
assertNodeHasNoTagsWithCollect(c, nodes[0])
assert.Equal(c, tagTestUser, nodes[0].GetUser().GetName(),
assert.Equal(c, tagTestUser, nodes[0].GetUser().Value.Name.Or(""),
"Node ownership should be returned to user after untagging")
t.Logf("After conversion - Node ID: %d, Tags: %v, User: %s",
nodes[0].GetId(), nodes[0].GetTags(), nodes[0].GetUser().GetName())
nodes[0].GetID().Or(0), nodes[0].GetTags(), nodes[0].GetUser().Value.Name.Or(""))
}
}, integrationutil.HAConvergeTimeout, 1*time.Second, "node should be user-owned after conversion via CLI register")
}
+5 -5
View File
@@ -73,8 +73,8 @@ func TestTailscaleRustAxum(t *testing.T) {
var userID uint64
for _, u := range users {
if u.GetName() == "user1" { //nolint:goconst
userID = u.GetId()
if u.GetName().Or("") == "user1" { //nolint:goconst
userID = u.GetID().Or(0)
break
}
@@ -101,7 +101,7 @@ func TestTailscaleRustAxum(t *testing.T) {
tsrsOpts := []tsric.Option{
tsric.WithNetwork(network),
tsric.WithHeadscaleURL(headscaleEndpoint),
tsric.WithAuthKey(pak.GetKey()),
tsric.WithAuthKey(pak.GetKey().Or("")),
tsric.WithExtraHosts([]string{headscaleHostname + ":" + headscaleIP}),
}
@@ -142,7 +142,7 @@ func TestTailscaleRustAxum(t *testing.T) {
// Find the tailscale-rs node by hostname prefix
for _, n := range nodes {
if strings.HasPrefix(n.GetGivenName(), "tsrs-") {
if strings.HasPrefix(n.GetGivenName().Or(""), "tsrs-") {
addrs := n.GetIpAddresses()
if len(addrs) > 0 {
rustNodeIPv4 = addrs[0]
@@ -152,7 +152,7 @@ func TestTailscaleRustAxum(t *testing.T) {
rustNodeIPv6 = addrs[1]
}
rustNodeName = n.GetGivenName()
rustNodeName = n.GetGivenName().Or("")
}
}