cli: migrate the CLI and integration tests to the v1 HTTP API

Replace the gRPC client with the generated HTTP client across every
command: locally over the unix socket without auth (matching the previous
local gRPC socket), remotely over TLS with a Bearer API key. Output
rendering and integration tests move to the HTTP client types; the
transport changes, the assertions do not.
This commit is contained in:
Kristoffer Dalby
2026-06-19 06:13:50 +00:00
parent ba90048cfb
commit 8efa5ad1fe
35 changed files with 1654 additions and 1371 deletions
+137 -137
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"
clientv1 "github.com/juanfont/headscale/gen/client/v1"
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
@@ -86,26 +86,26 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
listUsers, err := headscale.ListUsers()
require.NoError(t, err)
want := []*v1.User{
want := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Email: "", // Unverified
Provider: "oidc",
@@ -114,10 +114,10 @@ func TestOIDCAuthenticationPingAll(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
}
@@ -245,34 +245,34 @@ func TestOIDC024UserCreation(t *testing.T) {
emailVerified bool
cliUsers []string
oidcUsers []string
want func(iss string) []*v1.User
want func(iss string) []*clientv1.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) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -286,26 +286,26 @@ 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) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2",
Email: "user2@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Provider: "oidc",
ProviderId: iss + "/user2",
@@ -318,26 +318,26 @@ 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) []*clientv1.User {
return []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1.headscale.net",
Email: "user1.headscale.net@test.no",
},
{
Id: 2,
Id: "2",
Name: "user1",
Provider: "oidc",
ProviderId: iss + "/user1",
},
{
Id: 3,
Id: "3",
Name: "user2.headscale.net",
Email: "user2.headscale.net@test.no",
},
{
Id: 4,
Id: "4",
Name: "user2",
Provider: "oidc",
ProviderId: 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 mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(want, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt")); diff != "" {
t.Errorf("unexpected users: %s", diff)
}
})
@@ -509,9 +509,9 @@ 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 := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -520,17 +520,17 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.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 []*clientv1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
@@ -593,16 +593,16 @@ 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 := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 2,
Id: "2",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -611,15 +611,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.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 []*clientv1.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].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Machine key should be preserved from original node")
assert.Equal(ct, listNodesAfterNewUserLogin[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "Both nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterNewUserLogin[0].NodeKey, listNodesAfterNewUserLogin[1].NodeKey, "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.User.Id == "2" { // user2
activeUser2NodeID = types.NodeID(mustParseID(node.Id))
t.Logf("Active user2 node: %d (User: %s)", mustParseID(node.Id), node.User.Name)
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.NotEmpty(ct, node.User.Id, "Node %d should have a valid user before logout", i)
assert.NotEmpty(ct, node.MachineKey, "Node %d should have a valid machine key before logout", i)
t.Logf("Pre-logout node %d: User=%s, MachineKey=%s", i, node.User.Name, node.MachineKey[: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.NotEmpty(ct, node.User.Id, "Node %d should still have a valid user after user2 logout", i)
assert.NotEmpty(ct, node.MachineKey, "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.User.Name, node.MachineKey[:16]+"...")
}
}, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence and integrity after user2 logout")
@@ -761,16 +761,16 @@ 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 := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
ProviderId: scenario.mockOIDC.Issuer() + "/user1",
},
{
Id: 2,
Id: "2",
Name: "user2",
Email: "user2@headscale.net",
Provider: "oidc",
@@ -779,15 +779,15 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.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 []*clientv1.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].MachineKey, listNodesAfterNewUserLogin[0].MachineKey, "Original user1 machine key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].NodeKey, listNodesAfterNewUserLogin[0].NodeKey, "Original user1 node key should match user1 node after user switch")
assert.Equal(ct, listNodes[0].Id, listNodesAfterNewUserLogin[0].Id, "Original user1 node ID should match user1 node after user switch")
assert.Equal(ct, listNodes[0].MachineKey, listNodesAfterNewUserLogin[1].MachineKey, "User1 and user2 nodes should share the same machine key")
assert.NotEqual(ct, listNodes[0].Id, listNodesAfterNewUserLogin[1].Id, "User1 and user2 nodes should have different node IDs")
assert.NotEqual(ct, listNodes[0].User.Id, listNodesAfterNewUserLogin[1].User.Id, "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].MachineKey, listNodesAfterLoggingBackIn[0].MachineKey, "Machine key should remain consistent after user1 relogin")
assert.NotEqual(ct, listNodes[0].NodeKey, listNodesAfterLoggingBackIn[0].NodeKey, "Node key should be regenerated after user1 relogin")
assert.Equal(ct, listNodes[0].Id, listNodesAfterLoggingBackIn[0].Id, "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].MachineKey, listNodesAfterLoggingBackIn[1].MachineKey, "Both final nodes should share the same machine key")
assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].NodeKey, listNodesAfterLoggingBackIn[1].NodeKey, "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.User.Id == "1" { // user1
activeUser1NodeID = types.NodeID(mustParseID(node.Id))
t.Logf("Active user1 node after relogin: %d (User: %s)", mustParseID(node.Id), node.User.Name)
break
}
@@ -942,9 +942,9 @@ func TestOIDCFollowUpUrl(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -954,15 +954,15 @@ func TestOIDCFollowUpUrl(t *testing.T) {
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(clientv1.User{}),
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1051,9 +1051,9 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
require.NoError(t, err)
assert.Len(t, listUsers, 1)
wantUsers := []*v1.User{
wantUsers := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1063,15 +1063,15 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
sort.Slice(
listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
},
)
if diff := cmp.Diff(
wantUsers,
listUsers,
cmpopts.IgnoreUnexported(v1.User{}),
cmpopts.IgnoreFields(v1.User{}, "CreatedAt"),
cmpopts.IgnoreUnexported(clientv1.User{}),
cmpopts.IgnoreFields(clientv1.User{}, "CreatedAt"),
); diff != "" {
t.Fatalf("unexpected users: %s", diff)
}
@@ -1159,9 +1159,9 @@ 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 := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1170,17 +1170,17 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.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 []*clientv1.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].MachineKey
initialNodeKey := initialNodes[0].NodeKey
initialNodeID := initialNodes[0].Id
// Logout user1
err = ts.Logout()
@@ -1262,9 +1262,9 @@ 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 := []*clientv1.User{
{
Id: 1,
Id: "1",
Name: "user1",
Email: "user1@headscale.net",
Provider: "oidc",
@@ -1273,15 +1273,15 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}
sort.Slice(listUsers, func(i, j int) bool {
return listUsers[i].GetId() < listUsers[j].GetId()
return mustParseID(listUsers[i].Id) < mustParseID(listUsers[j].Id)
})
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(clientv1.User{}), cmpopts.IgnoreFields(clientv1.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 []*clientv1.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.MachineKey, "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.Id, "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.NodeKey, "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(mustParseID(finalNodes[0].Id))
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.NotNil(ct, node.Expiry, "Expiry should be set after OIDC login")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.Expiry != nil {
expiryTime := *node.Expiry
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.NotNil(ct, node.Expiry, "Expiry should NOT be nil after restart")
if node.GetExpiry() != nil {
expiryTime := node.GetExpiry().AsTime()
if node.Expiry != nil {
expiryTime := *node.Expiry
// This is the bug check - expiry should NOT be zero time
assert.False(ct, expiryTime.IsZero(),
@@ -1567,9 +1567,9 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.Len(ct, nodes, 1)
gatewayNode := nodes[0]
gatewayNodeID = gatewayNode.GetId()
assert.Len(ct, gatewayNode.GetAvailableRoutes(), 1)
assert.Contains(ct, gatewayNode.GetAvailableRoutes(), advertiseRoute)
gatewayNodeID = mustParseID(gatewayNode.Id)
assert.Len(ct, gatewayNode.AvailableRoutes, 1)
assert.Contains(ct, gatewayNode.AvailableRoutes, advertiseRoute)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route advertisement should propagate to headscale")
// Approve the advertised route
@@ -1586,8 +1586,8 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) {
assert.Len(ct, nodes, 1)
gatewayNode := nodes[0]
assert.Len(ct, gatewayNode.GetApprovedRoutes(), 1)
assert.Contains(ct, gatewayNode.GetApprovedRoutes(), advertiseRoute)
assert.Len(ct, gatewayNode.ApprovedRoutes, 1)
assert.Contains(ct, gatewayNode.ApprovedRoutes, advertiseRoute)
}, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route approval should propagate to headscale")
// NOW create the OIDC user by having them join
@@ -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 *clientv1.User
for _, user := range users {
if user.GetName() == "gateway" && user.GetProvider() == "" {
if user.Name == "gateway" && user.Provider == "" {
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.Name)
}
// Find OIDC user
var oidcUserFound *v1.User
var oidcUserFound *clientv1.User
for _, user := range users {
if user.GetName() == "oidcuser" && user.GetProvider() == "oidc" {
if user.Name == "oidcuser" && user.Provider == "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.Name)
assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.Email)
}
}, 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 *clientv1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err := headscale.ListNodes()
@@ -1844,21 +1844,21 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
initialNode = nodes[0]
// Check: 1 announced, 1 approved, 1 serving (subnet route)
assert.Lenf(c, initialNode.GetAvailableRoutes(), 1,
"Node should have 1 available route, got %v", initialNode.GetAvailableRoutes())
assert.Lenf(c, initialNode.GetApprovedRoutes(), 1,
"Node should have 1 approved route, got %v", initialNode.GetApprovedRoutes())
assert.Lenf(c, initialNode.GetSubnetRoutes(), 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.GetSubnetRoutes())
assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, initialNode.AvailableRoutes, 1,
"Node should have 1 available route, got %v", initialNode.AvailableRoutes)
assert.Lenf(c, initialNode.ApprovedRoutes, 1,
"Node should have 1 approved route, got %v", initialNode.ApprovedRoutes)
assert.Lenf(c, initialNode.SubnetRoutes, 1,
"Node should have 1 serving (subnet) route, got %v - THIS IS THE BUG if empty", initialNode.SubnetRoutes)
assert.Contains(c, initialNode.SubnetRoutes, advertiseRoute,
"Subnet routes should contain %s", advertiseRoute)
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving")
require.NotNil(t, initialNode, "Initial node should be found")
initialNodeID := initialNode.GetId()
t.Logf("Initial node ID: %d, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.GetAvailableRoutes(), initialNode.GetApprovedRoutes(), initialNode.GetSubnetRoutes())
initialNodeID := initialNode.Id
t.Logf("Initial node ID: %s, Available: %v, Approved: %v, Serving: %v",
initialNodeID, initialNode.AvailableRoutes, initialNode.ApprovedRoutes, initialNode.SubnetRoutes)
// Step 2: Logout
t.Logf("Step 2: Logging out at %s", time.Now().Format(TimestampFormat))
@@ -1911,23 +1911,23 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) {
if len(nodes) == 1 {
node := nodes[0]
t.Logf("After relogin - Available: %v, Approved: %v, Serving: %v",
node.GetAvailableRoutes(), node.GetApprovedRoutes(), node.GetSubnetRoutes())
node.AvailableRoutes, node.ApprovedRoutes, node.SubnetRoutes)
// This is where issue #2896 manifests:
// - Available shows the route (from [tailcfg.Hostinfo.RoutableIPs])
// - Approved shows the route (from [tailcfg.Node.ApprovedRoutes])
// - BUT Serving ([tailcfg.Node.SubnetRoutes]/[ipnstate.PeerStatus.PrimaryRoutes]) is EMPTY!
assert.Lenf(c, node.GetAvailableRoutes(), 1,
"Node should have 1 available route after relogin, got %v", node.GetAvailableRoutes())
assert.Lenf(c, node.GetApprovedRoutes(), 1,
"Node should have 1 approved route after relogin, got %v", node.GetApprovedRoutes())
assert.Lenf(c, node.GetSubnetRoutes(), 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.GetSubnetRoutes())
assert.Contains(c, node.GetSubnetRoutes(), advertiseRoute,
assert.Lenf(c, node.AvailableRoutes, 1,
"Node should have 1 available route after relogin, got %v", node.AvailableRoutes)
assert.Lenf(c, node.ApprovedRoutes, 1,
"Node should have 1 approved route after relogin, got %v", node.ApprovedRoutes)
assert.Lenf(c, node.SubnetRoutes, 1,
"BUG #2896: Node should have 1 SERVING route after relogin, got %v", node.SubnetRoutes)
assert.Contains(c, node.SubnetRoutes, advertiseRoute,
"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.Id,
"Node ID should be preserved after same-user relogin")
}
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll,