From add794d19047fae71148d32b35f71abc85b5013a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 18 Jun 2026 13:45:46 +0000 Subject: [PATCH] integration: port the new CLI command tests to the HTTP client #3326 added per-resource CLI coverage decoding into the proto types; convert the transport to the apiv1/ogen types, keeping every command and assertion. Drop the now-duplicate health and node-routes tests #3326 covers more fully. --- integration/cli_apikeys_test.go | 80 ++++++------ integration/cli_auth_test.go | 2 +- integration/cli_nodes_test.go | 188 ++++++++++++++-------------- integration/cli_preauthkeys_test.go | 72 +++++------ integration/cli_server_test.go | 6 +- integration/cli_test.go | 131 ++----------------- integration/cli_users_test.go | 70 +++++------ 7 files changed, 217 insertions(+), 332 deletions(-) diff --git a/integration/cli_apikeys_test.go b/integration/cli_apikeys_test.go index f42de600..e34ff604 100644 --- a/integration/cli_apikeys_test.go +++ b/integration/cli_apikeys_test.go @@ -5,7 +5,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/integration/hsic" "github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/tsic" @@ -55,7 +55,7 @@ func TestApiKeyCommand(t *testing.T) { assert.Len(t, keys, 5) - var listedAPIKeys []v1.ApiKey + var listedAPIKeys []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -73,43 +73,43 @@ func TestApiKeyCommand(t *testing.T) { assert.Len(t, listedAPIKeys, 5) - assert.Equal(t, uint64(1), listedAPIKeys[0].GetId()) - assert.Equal(t, uint64(2), listedAPIKeys[1].GetId()) - assert.Equal(t, uint64(3), listedAPIKeys[2].GetId()) - assert.Equal(t, uint64(4), listedAPIKeys[3].GetId()) - assert.Equal(t, uint64(5), listedAPIKeys[4].GetId()) + assert.Equal(t, uint64(1), listedAPIKeys[0].GetID().Or(0)) + assert.Equal(t, uint64(2), listedAPIKeys[1].GetID().Or(0)) + assert.Equal(t, uint64(3), listedAPIKeys[2].GetID().Or(0)) + assert.Equal(t, uint64(4), listedAPIKeys[3].GetID().Or(0)) + assert.Equal(t, uint64(5), listedAPIKeys[4].GetID().Or(0)) - assert.NotEmpty(t, listedAPIKeys[0].GetPrefix()) - assert.NotEmpty(t, listedAPIKeys[1].GetPrefix()) - assert.NotEmpty(t, listedAPIKeys[2].GetPrefix()) - assert.NotEmpty(t, listedAPIKeys[3].GetPrefix()) - assert.NotEmpty(t, listedAPIKeys[4].GetPrefix()) + assert.NotEmpty(t, listedAPIKeys[0].GetPrefix().Or("")) + assert.NotEmpty(t, listedAPIKeys[1].GetPrefix().Or("")) + assert.NotEmpty(t, listedAPIKeys[2].GetPrefix().Or("")) + assert.NotEmpty(t, listedAPIKeys[3].GetPrefix().Or("")) + assert.NotEmpty(t, listedAPIKeys[4].GetPrefix().Or("")) - assert.True(t, listedAPIKeys[0].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedAPIKeys[1].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedAPIKeys[2].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedAPIKeys[3].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedAPIKeys[4].GetExpiration().AsTime().After(time.Now())) + assert.True(t, listedAPIKeys[0].GetExpiration().Value.After(time.Now())) + assert.True(t, listedAPIKeys[1].GetExpiration().Value.After(time.Now())) + assert.True(t, listedAPIKeys[2].GetExpiration().Value.After(time.Now())) + assert.True(t, listedAPIKeys[3].GetExpiration().Value.After(time.Now())) + assert.True(t, listedAPIKeys[4].GetExpiration().Value.After(time.Now())) assert.True( t, - listedAPIKeys[0].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedAPIKeys[0].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedAPIKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedAPIKeys[1].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedAPIKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedAPIKeys[2].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedAPIKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedAPIKeys[3].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedAPIKeys[4].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedAPIKeys[4].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) expiredPrefixes := make(map[string]bool) @@ -122,15 +122,15 @@ func TestApiKeyCommand(t *testing.T) { "apikeys", "expire", "--prefix", - listedAPIKeys[idx].GetPrefix(), + listedAPIKeys[idx].GetPrefix().Or(""), }, ) require.NoError(t, err) - expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true + expiredPrefixes[listedAPIKeys[idx].GetPrefix().Or("")] = true } - var listedAfterExpireAPIKeys []v1.ApiKey + var listedAfterExpireAPIKeys []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -147,17 +147,17 @@ func TestApiKeyCommand(t *testing.T) { }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after expire") for index := range listedAfterExpireAPIKeys { - if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok { + if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix().Or("")]; ok { // Expired assert.True( t, - listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()), + listedAfterExpireAPIKeys[index].GetExpiration().Value.Before(time.Now()), ) } else { // Not expired assert.False( t, - listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()), + listedAfterExpireAPIKeys[index].GetExpiration().Value.Before(time.Now()), ) } } @@ -168,11 +168,11 @@ func TestApiKeyCommand(t *testing.T) { "apikeys", "delete", "--prefix", - listedAPIKeys[0].GetPrefix(), + listedAPIKeys[0].GetPrefix().Or(""), }) require.NoError(t, err) - var listedAPIKeysAfterDelete []v1.ApiKey + var listedAPIKeysAfterDelete []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -197,11 +197,11 @@ func TestApiKeyCommand(t *testing.T) { "apikeys", "expire", "--id", - strconv.FormatUint(listedAPIKeysAfterDelete[0].GetId(), 10), + strconv.FormatUint(listedAPIKeysAfterDelete[0].GetID().Or(0), 10), }) require.NoError(t, err) - var listedAPIKeysAfterExpireByID []v1.ApiKey + var listedAPIKeysAfterExpireByID []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -219,14 +219,14 @@ func TestApiKeyCommand(t *testing.T) { // Verify the key was expired for idx := range listedAPIKeysAfterExpireByID { - if listedAPIKeysAfterExpireByID[idx].GetId() == listedAPIKeysAfterDelete[0].GetId() { - assert.True(t, listedAPIKeysAfterExpireByID[idx].GetExpiration().AsTime().Before(time.Now()), + if listedAPIKeysAfterExpireByID[idx].GetID().Or(0) == listedAPIKeysAfterDelete[0].GetID().Or(0) { + assert.True(t, listedAPIKeysAfterExpireByID[idx].GetExpiration().Value.Before(time.Now()), "Key expired by ID should have expiration in the past") } } // Test delete by ID (using key at index 1) - deletedKeyID := listedAPIKeysAfterExpireByID[1].GetId() + deletedKeyID := listedAPIKeysAfterExpireByID[1].GetID().Or(0) _, err = headscale.Execute( []string{ "headscale", @@ -237,7 +237,7 @@ func TestApiKeyCommand(t *testing.T) { }) require.NoError(t, err) - var listedAPIKeysAfterDeleteByID []v1.ApiKey + var listedAPIKeysAfterDeleteByID []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -257,7 +257,7 @@ func TestApiKeyCommand(t *testing.T) { // Verify the specific key was deleted for idx := range listedAPIKeysAfterDeleteByID { - assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].GetId(), + assert.NotEqual(t, deletedKeyID, listedAPIKeysAfterDeleteByID[idx].GetID().Or(0), "Deleted key should not be present in the list") } } @@ -277,7 +277,7 @@ func TestApiKeyCommandValidation(t *testing.T) { _, err := headscale.Execute([]string{"headscale", "apikeys", "create", "--output", "json"}) require.NoError(t, err) - var listed []v1.ApiKey + var listed []apiv1.ApiKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -288,8 +288,8 @@ func TestApiKeyCommandValidation(t *testing.T) { assert.Len(c, listed, 1) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API key list") - prefix := listed[0].GetPrefix() - id := strconv.FormatUint(listed[0].GetId(), 10) + prefix := listed[0].GetPrefix().Or("") + id := strconv.FormatUint(listed[0].GetID().Or(0), 10) tests := []struct { name string diff --git a/integration/cli_auth_test.go b/integration/cli_auth_test.go index e3ea96d7..de535670 100644 --- a/integration/cli_auth_test.go +++ b/integration/cli_auth_test.go @@ -8,7 +8,7 @@ import ( ) // TestAuthCommandValidation exercises the validation permutations of the auth -// subcommands over the gRPC transport: `register` against a non-existent user +// subcommands over the HTTP transport: `register` against a non-existent user // and a malformed auth-id, and `approve`/`reject` against malformed and unknown // auth-ids. // diff --git a/integration/cli_nodes_test.go b/integration/cli_nodes_test.go index 4ea21e4e..6f0ecc02 100644 --- a/integration/cli_nodes_test.go +++ b/integration/cli_nodes_test.go @@ -7,7 +7,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" @@ -43,7 +43,7 @@ func TestNodeCommand(t *testing.T) { types.MustAuthID().String(), types.MustAuthID().String(), } - nodes := make([]*v1.Node, len(regIDs)) + nodes := make([]*apiv1.Node, len(regIDs)) require.NoError(t, err) @@ -65,7 +65,7 @@ func TestNodeCommand(t *testing.T) { ) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -94,7 +94,7 @@ func TestNodeCommand(t *testing.T) { }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second) // Test list all nodes after added seconds - var listAll []v1.Node + var listAll []apiv1.Node assert.EventuallyWithT(t, func(ct *assert.CollectT) { err := executeAndUnmarshal( @@ -112,23 +112,23 @@ func TestNodeCommand(t *testing.T) { assert.Len(ct, listAll, len(regIDs), "Should list all nodes after CLI operations") }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) - assert.Equal(t, uint64(1), listAll[0].GetId()) - assert.Equal(t, uint64(2), listAll[1].GetId()) - assert.Equal(t, uint64(3), listAll[2].GetId()) - assert.Equal(t, uint64(4), listAll[3].GetId()) - assert.Equal(t, uint64(5), listAll[4].GetId()) + assert.Equal(t, uint64(1), listAll[0].GetID().Or(0)) + assert.Equal(t, uint64(2), listAll[1].GetID().Or(0)) + assert.Equal(t, uint64(3), listAll[2].GetID().Or(0)) + assert.Equal(t, uint64(4), listAll[3].GetID().Or(0)) + assert.Equal(t, uint64(5), listAll[4].GetID().Or(0)) - assert.Equal(t, "node-1", listAll[0].GetName()) - assert.Equal(t, "node-2", listAll[1].GetName()) - assert.Equal(t, "node-3", listAll[2].GetName()) - assert.Equal(t, "node-4", listAll[3].GetName()) - assert.Equal(t, "node-5", listAll[4].GetName()) + assert.Equal(t, "node-1", listAll[0].GetName().Or("")) + assert.Equal(t, "node-2", listAll[1].GetName().Or("")) + assert.Equal(t, "node-3", listAll[2].GetName().Or("")) + assert.Equal(t, "node-4", listAll[3].GetName().Or("")) + assert.Equal(t, "node-5", listAll[4].GetName().Or("")) otherUserRegIDs := []string{ types.MustAuthID().String(), types.MustAuthID().String(), } - otherUserMachines := make([]*v1.Node, len(otherUserRegIDs)) + otherUserMachines := make([]*apiv1.Node, len(otherUserRegIDs)) require.NoError(t, err) @@ -150,7 +150,7 @@ func TestNodeCommand(t *testing.T) { ) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -179,7 +179,7 @@ func TestNodeCommand(t *testing.T) { }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second) // Test list all nodes after added otherUser - var listAllWithotherUser []v1.Node + var listAllWithotherUser []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -199,14 +199,14 @@ func TestNodeCommand(t *testing.T) { // All nodes, nodes + otherUser assert.Len(t, listAllWithotherUser, 7) - assert.Equal(t, uint64(6), listAllWithotherUser[5].GetId()) - assert.Equal(t, uint64(7), listAllWithotherUser[6].GetId()) + assert.Equal(t, uint64(6), listAllWithotherUser[5].GetID().Or(0)) + assert.Equal(t, uint64(7), listAllWithotherUser[6].GetID().Or(0)) - assert.Equal(t, "otheruser-node-1", listAllWithotherUser[5].GetName()) - assert.Equal(t, "otheruser-node-2", listAllWithotherUser[6].GetName()) + assert.Equal(t, "otheruser-node-1", listAllWithotherUser[5].GetName().Or("")) + assert.Equal(t, "otheruser-node-2", listAllWithotherUser[6].GetName().Or("")) // Test list all nodes after added otherUser - var listOnlyotherUserMachineUser []v1.Node + var listOnlyotherUserMachineUser []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -227,18 +227,18 @@ func TestNodeCommand(t *testing.T) { assert.Len(t, listOnlyotherUserMachineUser, 2) - assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].GetId()) - assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].GetId()) + assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].GetID().Or(0)) + assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].GetID().Or(0)) assert.Equal( t, "otheruser-node-1", - listOnlyotherUserMachineUser[0].GetName(), + listOnlyotherUserMachineUser[0].GetName().Or(""), ) assert.Equal( t, "otheruser-node-2", - listOnlyotherUserMachineUser[1].GetName(), + listOnlyotherUserMachineUser[1].GetName().Or(""), ) // Delete a nodes @@ -258,7 +258,7 @@ func TestNodeCommand(t *testing.T) { require.NoError(t, err) // Test: list main user after node is deleted - var listOnlyMachineUserAfterDelete []v1.Node + var listOnlyMachineUserAfterDelete []apiv1.Node assert.EventuallyWithT(t, func(ct *assert.CollectT) { err := executeAndUnmarshal( @@ -304,7 +304,7 @@ func TestNodeExpireCommand(t *testing.T) { types.MustAuthID().String(), types.MustAuthID().String(), } - nodes := make([]*v1.Node, len(regIDs)) + nodes := make([]*apiv1.Node, len(regIDs)) for index, regID := range regIDs { _, err := headscale.Execute( @@ -324,7 +324,7 @@ func TestNodeExpireCommand(t *testing.T) { ) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -350,7 +350,7 @@ func TestNodeExpireCommand(t *testing.T) { assert.Len(t, nodes, len(regIDs)) - var listAll []v1.Node + var listAll []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -372,7 +372,7 @@ func TestNodeExpireCommand(t *testing.T) { // With node.expiry defaulting to 0, non-tagged nodes have zero expiry // (never expire unless explicitly expired). for i := range 5 { - assert.True(t, listAll[i].GetExpiry().AsTime().IsZero(), + assert.True(t, listAll[i].GetExpiry().Or(time.Time{}).IsZero(), "node %d should have zero expiry (no default node.expiry)", i) } @@ -383,13 +383,13 @@ func TestNodeExpireCommand(t *testing.T) { "nodes", "expire", "--identifier", - strconv.FormatUint(listAll[idx].GetId(), 10), + strconv.FormatUint(listAll[idx].GetID().Or(0), 10), }, ) require.NoError(t, err) } - var listAllAfterExpiry []v1.Node + var listAllAfterExpiry []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -408,11 +408,11 @@ func TestNodeExpireCommand(t *testing.T) { assert.Len(t, listAllAfterExpiry, 5) - assert.True(t, listAllAfterExpiry[0].GetExpiry().AsTime().Before(time.Now())) - assert.True(t, listAllAfterExpiry[1].GetExpiry().AsTime().Before(time.Now())) - assert.True(t, listAllAfterExpiry[2].GetExpiry().AsTime().Before(time.Now())) - assert.True(t, listAllAfterExpiry[3].GetExpiry().AsTime().IsZero()) - assert.True(t, listAllAfterExpiry[4].GetExpiry().AsTime().IsZero()) + assert.True(t, listAllAfterExpiry[0].GetExpiry().Or(time.Time{}).Before(time.Now())) + assert.True(t, listAllAfterExpiry[1].GetExpiry().Or(time.Time{}).Before(time.Now())) + assert.True(t, listAllAfterExpiry[2].GetExpiry().Or(time.Time{}).Before(time.Now())) + assert.True(t, listAllAfterExpiry[3].GetExpiry().Or(time.Time{}).IsZero()) + assert.True(t, listAllAfterExpiry[4].GetExpiry().Or(time.Time{}).IsZero()) } func TestNodeRenameCommand(t *testing.T) { @@ -440,7 +440,7 @@ func TestNodeRenameCommand(t *testing.T) { types.MustAuthID().String(), types.MustAuthID().String(), } - nodes := make([]*v1.Node, len(regIDs)) + nodes := make([]*apiv1.Node, len(regIDs)) require.NoError(t, err) @@ -462,7 +462,7 @@ func TestNodeRenameCommand(t *testing.T) { ) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -488,7 +488,7 @@ func TestNodeRenameCommand(t *testing.T) { assert.Len(t, nodes, len(regIDs)) - var listAll []v1.Node + var listAll []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -507,11 +507,11 @@ func TestNodeRenameCommand(t *testing.T) { assert.Len(t, listAll, 5) - assert.Contains(t, listAll[0].GetGivenName(), "node-1") - assert.Contains(t, listAll[1].GetGivenName(), "node-2") - assert.Contains(t, listAll[2].GetGivenName(), "node-3") - assert.Contains(t, listAll[3].GetGivenName(), "node-4") - assert.Contains(t, listAll[4].GetGivenName(), "node-5") + assert.Contains(t, listAll[0].GetGivenName().Or(""), "node-1") + assert.Contains(t, listAll[1].GetGivenName().Or(""), "node-2") + assert.Contains(t, listAll[2].GetGivenName().Or(""), "node-3") + assert.Contains(t, listAll[3].GetGivenName().Or(""), "node-4") + assert.Contains(t, listAll[4].GetGivenName().Or(""), "node-5") for idx := range 3 { res, err := headscale.Execute( @@ -520,7 +520,7 @@ func TestNodeRenameCommand(t *testing.T) { "nodes", "rename", "--identifier", - strconv.FormatUint(listAll[idx].GetId(), 10), + strconv.FormatUint(listAll[idx].GetID().Or(0), 10), fmt.Sprintf("newnode-%d", idx+1), }, ) @@ -529,7 +529,7 @@ func TestNodeRenameCommand(t *testing.T) { assert.Contains(t, res, "Node renamed") } - var listAllAfterRename []v1.Node + var listAllAfterRename []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -548,11 +548,11 @@ func TestNodeRenameCommand(t *testing.T) { assert.Len(t, listAllAfterRename, 5) - assert.Equal(t, "newnode-1", listAllAfterRename[0].GetGivenName()) - assert.Equal(t, "newnode-2", listAllAfterRename[1].GetGivenName()) - assert.Equal(t, "newnode-3", listAllAfterRename[2].GetGivenName()) - assert.Contains(t, listAllAfterRename[3].GetGivenName(), "node-4") - assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5") + assert.Equal(t, "newnode-1", listAllAfterRename[0].GetGivenName().Or("")) + assert.Equal(t, "newnode-2", listAllAfterRename[1].GetGivenName().Or("")) + assert.Equal(t, "newnode-3", listAllAfterRename[2].GetGivenName().Or("")) + assert.Contains(t, listAllAfterRename[3].GetGivenName().Or(""), "node-4") + assert.Contains(t, listAllAfterRename[4].GetGivenName().Or(""), "node-5") // Test failure for too long names _, err = headscale.Execute( @@ -561,13 +561,13 @@ func TestNodeRenameCommand(t *testing.T) { "nodes", "rename", "--identifier", - strconv.FormatUint(listAll[4].GetId(), 10), + strconv.FormatUint(listAll[4].GetID().Or(0), 10), strings.Repeat("t", 64), }, ) require.ErrorContains(t, err, "is too long, max length is 63 bytes") - var listAllAfterRenameAttempt []v1.Node + var listAllAfterRenameAttempt []apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -586,11 +586,11 @@ func TestNodeRenameCommand(t *testing.T) { assert.Len(t, listAllAfterRenameAttempt, 5) - assert.Equal(t, "newnode-1", listAllAfterRenameAttempt[0].GetGivenName()) - assert.Equal(t, "newnode-2", listAllAfterRenameAttempt[1].GetGivenName()) - assert.Equal(t, "newnode-3", listAllAfterRenameAttempt[2].GetGivenName()) - assert.Contains(t, listAllAfterRenameAttempt[3].GetGivenName(), "node-4") - assert.Contains(t, listAllAfterRenameAttempt[4].GetGivenName(), "node-5") + assert.Equal(t, "newnode-1", listAllAfterRenameAttempt[0].GetGivenName().Or("")) + assert.Equal(t, "newnode-2", listAllAfterRenameAttempt[1].GetGivenName().Or("")) + assert.Equal(t, "newnode-3", listAllAfterRenameAttempt[2].GetGivenName().Or("")) + assert.Contains(t, listAllAfterRenameAttempt[3].GetGivenName().Or(""), "node-4") + assert.Contains(t, listAllAfterRenameAttempt[4].GetGivenName().Or(""), "node-5") } func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { @@ -623,7 +623,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { u2, err := headscale.CreateUser(user2) require.NoError(t, err) - var user2Key v1.PreAuthKey + var user2Key apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -632,7 +632,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { "headscale", "preauthkeys", "--user", - strconv.FormatUint(u2.GetId(), 10), + strconv.FormatUint(u2.GetID().Or(0), 10), "create", "--reusable", "--expiration", @@ -647,7 +647,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user2 preauth key creation") - var listNodes []*v1.Node + var listNodes []*apiv1.Node assert.EventuallyWithT(t, func(ct *assert.CollectT) { var err error @@ -655,7 +655,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err) assert.Len(ct, listNodes, 1, "Should have exactly 1 node for user1") - assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "Node should belong to user1") + assert.Equal(ct, user1, listNodes[0].GetUser().Value.Name.Or(""), "Node should belong to user1") }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second) allClients, err := scenario.ListTailscaleClients() @@ -679,7 +679,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { "Expected node to be logged out, backend state: %s", status.BackendState) }, integrationutil.StatusReadyTimeout, 2*time.Second) - err = client.Login(headscale.GetEndpoint(), user2Key.GetKey()) + err = client.Login(headscale.GetEndpoint(), user2Key.GetKey().Or("")) require.NoError(t, err) assert.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -697,9 +697,9 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err) assert.Len(ct, listNodes, 2, "Should have 2 nodes after re-login") - assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "First node should belong to user1") + assert.Equal(ct, user1, listNodes[0].GetUser().Value.Name.Or(""), "First node should belong to user1") // Second node is tagged (created with tagged PreAuthKey), so it shows as "tagged-devices" - assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().GetName(), "Second node should be tagged-devices") + assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().Value.Name.Or(""), "Second node should be tagged-devices") }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) } @@ -731,7 +731,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { u2, err := headscale.CreateUser(user2) require.NoError(t, err) - var user2Key v1.PreAuthKey + var user2Key apiv1.PreAuthKey // Create a tagged PreAuthKey for user2 assert.EventuallyWithT(t, func(c *assert.CollectT) { @@ -741,7 +741,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { "headscale", "preauthkeys", "--user", - strconv.FormatUint(u2.GetId(), 10), + strconv.FormatUint(u2.GetID().Or(0), 10), "create", "--reusable", "--expiration", @@ -778,7 +778,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { }, integrationutil.StatusReadyTimeout, 2*time.Second) // Log in with the tagged PreAuthKey (from user2, with tags) - err = client.Login(headscale.GetEndpoint(), user2Key.GetKey()) + err = client.Login(headscale.GetEndpoint(), user2Key.GetKey().Or("")) require.NoError(t, err) assert.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -790,7 +790,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { }, integrationutil.StatusReadyTimeout, 2*time.Second) // Wait for the second node to appear - var listNodes []*v1.Node + var listNodes []*apiv1.Node assert.EventuallyWithT(t, func(ct *assert.CollectT) { var err error @@ -798,8 +798,8 @@ func TestTaggedNodesCLIOutput(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err) assert.Len(ct, listNodes, 2, "Should have 2 nodes after re-login with tagged key") - assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "First node should belong to user1") - assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().GetName(), "Second node should be tagged-devices") + assert.Equal(ct, user1, listNodes[0].GetUser().Value.Name.Or(""), "First node should belong to user1") + assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().Value.Name.Or(""), "Second node should be tagged-devices") }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) // Test: tailscale status output should show "tagged-devices" not "userid:2147455555" @@ -838,7 +838,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) { }) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -853,14 +853,14 @@ func TestNodeExpireFlagsCommand(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node registration") - nodeID := strconv.FormatUint(node.GetId(), 10) + nodeID := strconv.FormatUint(node.GetID().Or(0), 10) // listNodeByID returns the node with the given id from `nodes list`. The // expire mutations are verified by reading the node back (authoritative, // eventually-consistent) rather than trusting the mutation's immediate // response. - listNodeByID := func(ct *assert.CollectT) *v1.Node { - var nodes []v1.Node + listNodeByID := func(ct *assert.CollectT) *apiv1.Node { + var nodes []apiv1.Node err := executeAndUnmarshal(headscale, []string{"headscale", "nodes", "list", "--output", "json"}, @@ -869,7 +869,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) { require.NoError(ct, err) for i := range nodes { - if nodes[i].GetId() == node.GetId() { + if nodes[i].GetID().Or(0) == node.GetID().Or(0) { return &nodes[i] } } @@ -896,8 +896,8 @@ func TestNodeExpireFlagsCommand(t *testing.T) { return } - assert.False(ct, n.GetExpiry().AsTime().IsZero(), "expiry should be set") - assert.True(ct, n.GetExpiry().AsTime().After(time.Now()), "expiry should be in the future") + assert.False(ct, n.GetExpiry().Or(time.Time{}).IsZero(), "expiry should be set") + assert.True(ct, n.GetExpiry().Or(time.Time{}).After(time.Now()), "expiry should be in the future") }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second, "Waiting for future expiry to apply") // Disable expiry entirely; the node should then report no expiry. @@ -919,7 +919,7 @@ func TestNodeExpireFlagsCommand(t *testing.T) { // future — it never expires. A nil expiry deserialises to the Unix // epoch rather than the zero time, so assert "not in the future" // rather than IsZero. - assert.False(ct, n.GetExpiry().AsTime().After(time.Now()), + assert.False(ct, n.GetExpiry().Or(time.Time{}).After(time.Now()), "disabled node should not have a future expiry") }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second, "Waiting for --disable to clear expiry") } @@ -943,7 +943,7 @@ func TestNodeCommandValidation(t *testing.T) { }) require.NoError(t, err) - var node v1.Node + var node apiv1.Node assert.EventuallyWithT(t, func(c *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -953,7 +953,7 @@ func TestNodeCommandValidation(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node registration") - id := strconv.FormatUint(node.GetId(), 10) + id := strconv.FormatUint(node.GetID().Or(0), 10) // wantErr is matched with ErrorContains; an empty wantErr only requires // that the command fails (used where the exact message is not load-bearing). @@ -1049,8 +1049,8 @@ func TestNodeTagCommand(t *testing.T) { assert.Len(ct, nodes, 1) if len(nodes) == 1 { - nodeID = nodes[0].GetId() - assert.Equal(ct, "user1", nodes[0].GetUser().GetName(), "node should start user-owned") + nodeID = nodes[0].GetID().Or(0) + assert.Equal(ct, "user1", nodes[0].GetUser().Value.Name.Or(""), "node should start user-owned") } }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) @@ -1059,13 +1059,13 @@ func TestNodeTagCommand(t *testing.T) { // Set two tags. The command response is round-tripped (transport check); // the resulting tag state is asserted via the authoritative list read-back // below rather than the immediate mutation response. - tagged := assertJSONRoundtrip[*v1.Node](t, headscale, []string{ + tagged := assertJSONRoundtrip[*apiv1.Node](t, headscale, []string{ "headscale", "nodes", "tag", "--identifier", idStr, "--tags", "tag:test1,tag:test2", "--output", "json", }) - assert.Equal(t, nodeID, tagged.GetId(), "tag response should be for the same node") + assert.Equal(t, nodeID, tagged.GetID().Or(0), "tag response should be for the same node") // The node is now a tagged node, presented as the tagged-devices user. assert.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -1074,7 +1074,7 @@ func TestNodeTagCommand(t *testing.T) { assert.Len(ct, nodes, 1) if len(nodes) == 1 { - assert.Equal(ct, "tagged-devices", nodes[0].GetUser().GetName(), "tagged node shows as tagged-devices") + assert.Equal(ct, "tagged-devices", nodes[0].GetUser().Value.Name.Or(""), "tagged node shows as tagged-devices") assert.ElementsMatch(ct, []string{"tag:test1", "tag:test2"}, nodes[0].GetTags()) } }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) @@ -1138,7 +1138,7 @@ func TestNodeRouteCommands(t *testing.T) { var nodeID uint64 assert.EventuallyWithT(t, func(ct *assert.CollectT) { - var nodes []v1.Node + var nodes []apiv1.Node err := executeAndUnmarshal(headscale, []string{"headscale", "nodes", "list-routes", "--output", "json"}, @@ -1148,7 +1148,7 @@ func TestNodeRouteCommands(t *testing.T) { assert.Len(ct, nodes, 1, "list-routes should show the route-advertising node") if len(nodes) == 1 { - nodeID = nodes[0].GetId() + nodeID = nodes[0].GetID().Or(0) assert.Contains(ct, nodes[0].GetAvailableRoutes(), route) assert.Empty(ct, nodes[0].GetApprovedRoutes()) } @@ -1157,7 +1157,7 @@ func TestNodeRouteCommands(t *testing.T) { idStr := strconv.FormatUint(nodeID, 10) // Approve the route via the CLI. - approved := assertJSONRoundtrip[*v1.Node](t, headscale, []string{ + approved := assertJSONRoundtrip[*apiv1.Node](t, headscale, []string{ "headscale", "nodes", "approve-routes", "--identifier", idStr, "--routes=" + route, @@ -1168,7 +1168,7 @@ func TestNodeRouteCommands(t *testing.T) { // list-routes filtered by the identifier should report the approved route // as a primary subnet route. assert.EventuallyWithT(t, func(ct *assert.CollectT) { - var nodes []v1.Node + var nodes []apiv1.Node err := executeAndUnmarshal(headscale, []string{"headscale", "nodes", "list-routes", "--identifier", idStr, "--output", "json"}, @@ -1184,7 +1184,7 @@ func TestNodeRouteCommands(t *testing.T) { }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) // Remove all approved routes by passing an empty --routes value. - cleared := assertJSONRoundtrip[*v1.Node](t, headscale, []string{ + cleared := assertJSONRoundtrip[*apiv1.Node](t, headscale, []string{ "headscale", "nodes", "approve-routes", "--identifier", idStr, "--routes=", @@ -1205,7 +1205,7 @@ func TestNodeBackfillIPsCommand(t *testing.T) { require.NoError(t, scenario.WaitForTailscaleSync()) - var before []*v1.Node + var before []*apiv1.Node assert.EventuallyWithT(t, func(ct *assert.CollectT) { var err error diff --git a/integration/cli_preauthkeys_test.go b/integration/cli_preauthkeys_test.go index 63fb43cd..5c653619 100644 --- a/integration/cli_preauthkeys_test.go +++ b/integration/cli_preauthkeys_test.go @@ -5,7 +5,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/integration/hsic" "github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/tsic" @@ -34,12 +34,12 @@ func TestPreAuthKeyCommand(t *testing.T) { headscale, err := scenario.Headscale() require.NoError(t, err) - keys := make([]*v1.PreAuthKey, count) + keys := make([]*apiv1.PreAuthKey, count) require.NoError(t, err) for index := range count { - var preAuthKey v1.PreAuthKey + var preAuthKey apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err := executeAndUnmarshal( @@ -68,7 +68,7 @@ func TestPreAuthKeyCommand(t *testing.T) { assert.Len(t, keys, 3) - var listedPreAuthKeys []v1.PreAuthKey + var listedPreAuthKeys []apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -90,34 +90,34 @@ func TestPreAuthKeyCommand(t *testing.T) { assert.Equal( t, - []uint64{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()}, + []uint64{keys[0].GetID().Or(0), keys[1].GetID().Or(0), keys[2].GetID().Or(0)}, []uint64{ - listedPreAuthKeys[1].GetId(), - listedPreAuthKeys[2].GetId(), - listedPreAuthKeys[3].GetId(), + listedPreAuthKeys[1].GetID().Or(0), + listedPreAuthKeys[2].GetID().Or(0), + listedPreAuthKeys[3].GetID().Or(0), }, ) // New keys show prefix after listing, so check the created keys instead - assert.NotEmpty(t, keys[0].GetKey()) - assert.NotEmpty(t, keys[1].GetKey()) - assert.NotEmpty(t, keys[2].GetKey()) + assert.NotEmpty(t, keys[0].GetKey().Or("")) + assert.NotEmpty(t, keys[1].GetKey().Or("")) + assert.NotEmpty(t, keys[2].GetKey().Or("")) - assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedPreAuthKeys[2].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedPreAuthKeys[3].GetExpiration().AsTime().After(time.Now())) + assert.True(t, listedPreAuthKeys[1].GetExpiration().Value.After(time.Now())) + assert.True(t, listedPreAuthKeys[2].GetExpiration().Value.After(time.Now())) + assert.True(t, listedPreAuthKeys[3].GetExpiration().Value.After(time.Now())) assert.True( t, - listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedPreAuthKeys[1].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedPreAuthKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedPreAuthKeys[2].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) assert.True( t, - listedPreAuthKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)), + listedPreAuthKeys[3].GetExpiration().Value.Before(time.Now().Add(time.Hour*26)), ) for index := range listedPreAuthKeys { @@ -139,12 +139,12 @@ func TestPreAuthKeyCommand(t *testing.T) { "preauthkeys", "expire", "--id", - strconv.FormatUint(keys[0].GetId(), 10), + strconv.FormatUint(keys[0].GetID().Or(0), 10), }, ) require.NoError(t, err) - var listedPreAuthKeysAfterExpire []v1.PreAuthKey + var listedPreAuthKeysAfterExpire []apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -161,9 +161,9 @@ func TestPreAuthKeyCommand(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after expire") - assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now())) - assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now())) - assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().AsTime().After(time.Now())) + assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().Value.Before(time.Now())) + assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().Value.After(time.Now())) + assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().Value.After(time.Now())) } func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { @@ -185,7 +185,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { headscale, err := scenario.Headscale() require.NoError(t, err) - var preAuthKey v1.PreAuthKey + var preAuthKey apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -205,7 +205,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation without expiry") - var listedPreAuthKeys []v1.PreAuthKey + var listedPreAuthKeys []apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -225,10 +225,10 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { // There is one key created by [Scenario.CreateHeadscaleEnv] assert.Len(t, listedPreAuthKeys, 2) - assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now())) + assert.True(t, listedPreAuthKeys[1].GetExpiration().Value.After(time.Now())) assert.True( t, - listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Minute*70)), + listedPreAuthKeys[1].GetExpiration().Value.Before(time.Now().Add(time.Minute*70)), ) } @@ -251,7 +251,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { headscale, err := scenario.Headscale() require.NoError(t, err) - var preAuthReusableKey v1.PreAuthKey + var preAuthReusableKey apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -271,7 +271,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for reusable preauth key creation") - var preAuthEphemeralKey v1.PreAuthKey + var preAuthEphemeralKey apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -291,10 +291,10 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { assert.NoError(c, err) }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for ephemeral preauth key creation") - assert.True(t, preAuthEphemeralKey.GetEphemeral()) - assert.False(t, preAuthEphemeralKey.GetReusable()) + assert.True(t, preAuthEphemeralKey.GetEphemeral().Or(false)) + assert.False(t, preAuthEphemeralKey.GetReusable().Or(false)) - var listedPreAuthKeys []v1.PreAuthKey + var listedPreAuthKeys []apiv1.PreAuthKey assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal( @@ -325,7 +325,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) { defer scenario.ShutdownAssertNoPanics(t) // Create a key to delete. - created := assertJSONRoundtrip[*v1.PreAuthKey](t, headscale, []string{ + created := assertJSONRoundtrip[*apiv1.PreAuthKey](t, headscale, []string{ "headscale", "preauthkeys", "--user", "1", @@ -333,7 +333,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) { "--reusable", "--output", "json", }) - require.NotZero(t, created.GetId()) + require.NotZero(t, created.GetID().Or(0)) // delete with no --id must be rejected. _, err := headscale.Execute([]string{"headscale", "preauthkeys", "delete"}) @@ -342,13 +342,13 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) { // delete the created key by id. _, err = headscale.Execute([]string{ "headscale", "preauthkeys", "delete", - "--id", strconv.FormatUint(created.GetId(), 10), + "--id", strconv.FormatUint(created.GetID().Or(0), 10), }) require.NoError(t, err) // The deleted key must be gone from the list. assert.EventuallyWithT(t, func(c *assert.CollectT) { - var listed []v1.PreAuthKey + var listed []apiv1.PreAuthKey err := executeAndUnmarshal(headscale, []string{"headscale", "preauthkeys", "list", "--output", "json"}, @@ -357,7 +357,7 @@ func TestPreAuthKeyDeleteCommand(t *testing.T) { assert.NoError(c, err) for i := range listed { - assert.NotEqual(c, created.GetId(), listed[i].GetId(), "deleted key should not be listed") + assert.NotEqual(c, created.GetID().Or(0), listed[i].GetID().Or(0), "deleted key should not be listed") } }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key list after delete") } diff --git a/integration/cli_server_test.go b/integration/cli_server_test.go index 51842b15..a1f572b8 100644 --- a/integration/cli_server_test.go +++ b/integration/cli_server_test.go @@ -3,7 +3,7 @@ package integration import ( "testing" - 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/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,10 +20,10 @@ func TestServerInfoCommands(t *testing.T) { defer scenario.ShutdownAssertNoPanics(t) t.Run("health", func(t *testing.T) { - health := assertJSONRoundtrip[*v1.HealthResponse](t, headscale, []string{ + health := assertJSONRoundtrip[*apiv1.HealthOK](t, headscale, []string{ "headscale", "health", "--output", "json", }) - assert.True(t, health.GetDatabaseConnectivity(), "database should be reachable") + assert.True(t, health.GetDatabaseConnectivity().Or(false), "database should be reachable") }) t.Run("version", func(t *testing.T) { diff --git a/integration/cli_test.go b/integration/cli_test.go index 9a3decf7..20316c15 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -6,6 +6,7 @@ import ( "fmt" "testing" + apiv1 "github.com/juanfont/headscale/gen/api/v1" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/require" @@ -19,8 +20,8 @@ import ( // // The whole point of the CLI test suite is to guard the transport: every // command is invoked with `--output json` and the result is unmarshalled into -// the matching gen/go/headscale/v1 Go type, so a change to the gRPC handlers, -// proto definitions or output encoders that breaks a command is caught here. +// the matching gen/api/v1 Go type, so a change to the API handlers or output +// encoders that breaks a command is caught here. func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error { str, err := headscale.Execute(command) @@ -62,14 +63,13 @@ func assertJSONRoundtrip[T any](t require.TestingT, headscale ControlServer, com return second } -// Interface ensuring that we can sort structs from gRPC that -// have an ID field. -type GRPCSortable interface { - GetId() uint64 +// hasID is implemented by API structs with an ID field, so they can be sorted. +type hasID interface { + GetID() apiv1.OptUint64 } -func sortWithID[T GRPCSortable](a, b T) int { - return cmp.Compare(a.GetId(), b.GetId()) +func sortWithID[T hasID](a, b T) int { + return cmp.Compare(a.GetID().Or(0), b.GetID().Or(0)) } // setupCLIScenario boots a scenario with the given users and nodes-per-user, @@ -97,118 +97,3 @@ func setupCLIScenario(t *testing.T, testName string, users []string, nodesPerUse return scenario, headscale } - -// TestHealthCommand exercises the `headscale health` CLI command end-to-end. -// Until now only the raw /health HTTP endpoint was hit (WaitForRunning); the -// CLI's ogen client.Health() path had no coverage. -func TestHealthCommand(t *testing.T) { - IntegrationSkip(t) - - spec := ScenarioSpec{ - Users: []string{"health-user"}, - } - - scenario, err := NewScenario(spec) - - require.NoError(t, err) - defer scenario.ShutdownAssertNoPanics(t) - - err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cli-health")) - require.NoError(t, err) - - headscale, err := scenario.Headscale() - require.NoError(t, err) - - // JSON output decodes and reports the database as reachable. - var health apiv1.HealthOK - - err = executeAndUnmarshal( - headscale, - []string{"headscale", "health", "--output", "json"}, - &health, - ) - require.NoError(t, err) - assert.True(t, health.GetDatabaseConnectivity().Or(false), "database should be reachable") - - // Default (non-JSON) output path also succeeds. - _, err = headscale.Execute([]string{"headscale", "health"}) - require.NoError(t, err) -} - -// TestNodeRoutesCommand covers `nodes list-routes` and `nodes backfillips`, -// neither of which had any integration coverage. Both go through the ogen HTTP -// client; list-routes also renders via the table path on HTTP-decoded data. -func TestNodeRoutesCommand(t *testing.T) { - IntegrationSkip(t) - - spec := ScenarioSpec{ - Users: []string{"routes-user"}, - } - - scenario, err := NewScenario(spec) - - require.NoError(t, err) - defer scenario.ShutdownAssertNoPanics(t) - - err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("cli-routes")) - require.NoError(t, err) - - headscale, err := scenario.Headscale() - require.NoError(t, err) - - regIDs := []string{ - types.MustAuthID().String(), - types.MustAuthID().String(), - } - - for index, regID := range regIDs { - _, err := headscale.Execute( - []string{ - "headscale", "debug", "create-node", - "--name", fmt.Sprintf("route-node-%d", index+1), - "--user", "routes-user", - "--key", regID, - "--output", "json", - }, - ) - require.NoError(t, err) - - var node apiv1.Node - - assert.EventuallyWithT(t, func(c *assert.CollectT) { - err = executeAndUnmarshal( - headscale, - []string{ - "headscale", "auth", "register", - "--user", "routes-user", - "--auth-id", regID, - "--output", "json", - }, - &node, - ) - assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "registering node") - } - - // list-routes decodes over HTTP (json output). - var routeNodes []apiv1.Node - - assert.EventuallyWithT(t, func(c *assert.CollectT) { - err := executeAndUnmarshal( - headscale, - []string{"headscale", "nodes", "list-routes", "--output", "json"}, - &routeNodes, - ) - assert.NoError(c, err) - }, integrationutil.ScaledTimeout(15*time.Second), 1*time.Second) - - // list-routes also renders as a table (exercises nodeRoutesToPtables). - _, err = headscale.Execute([]string{"headscale", "nodes", "list-routes"}) - require.NoError(t, err) - - // backfillips runs non-interactively and decodes its change list. - _, err = headscale.Execute( - []string{"headscale", "nodes", "backfillips", "--force", "--output", "json"}, - ) - require.NoError(t, err) -} diff --git a/integration/cli_users_test.go b/integration/cli_users_test.go index c2f95bf7..877e30c1 100644 --- a/integration/cli_users_test.go +++ b/integration/cli_users_test.go @@ -9,7 +9,7 @@ import ( tcmp "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" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/integrationutil" "github.com/juanfont/headscale/integration/tsic" @@ -36,7 +36,7 @@ func TestUserCommand(t *testing.T) { require.NoError(t, err) var ( - listUsers []*v1.User + listUsers []*apiv1.User result []string ) @@ -54,7 +54,7 @@ func TestUserCommand(t *testing.T) { assert.NoError(ct, err) slices.SortFunc(listUsers, sortWithID) - result = []string{listUsers[0].GetName(), listUsers[1].GetName()} + result = []string{listUsers[0].GetName().Or(""), listUsers[1].GetName().Or("")} assert.Equal( ct, @@ -70,13 +70,13 @@ func TestUserCommand(t *testing.T) { "users", "rename", "--output=json", - fmt.Sprintf("--identifier=%d", listUsers[1].GetId()), + fmt.Sprintf("--identifier=%d", listUsers[1].GetID().Or(0)), "--new-name=newname", }, ) require.NoError(t, err) - var listAfterRenameUsers []*v1.User + var listAfterRenameUsers []*apiv1.User assert.EventuallyWithT(t, func(ct *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -92,7 +92,7 @@ func TestUserCommand(t *testing.T) { assert.NoError(ct, err) slices.SortFunc(listAfterRenameUsers, sortWithID) - result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()} + result = []string{listAfterRenameUsers[0].GetName().Or(""), listAfterRenameUsers[1].GetName().Or("")} assert.Equal( ct, @@ -102,7 +102,7 @@ func TestUserCommand(t *testing.T) { ) }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) - var listByUsername []*v1.User + var listByUsername []*apiv1.User assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -121,19 +121,19 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listByUsername, sortWithID) - 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"), }, } - if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { + if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" { t.Errorf("unexpected users (-want +got):\n%s", diff) } - var listByID []*v1.User + var listByID []*apiv1.User assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -152,15 +152,15 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listByID, sortWithID) - 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"), }, } - if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { + if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" { t.Errorf("unexpected users (-want +got):\n%s", diff) } @@ -177,7 +177,7 @@ func TestUserCommand(t *testing.T) { require.NoError(t, err) assert.Contains(t, deleteResult, "User destroyed") - var listAfterIDDelete []*v1.User + var listAfterIDDelete []*apiv1.User assert.EventuallyWithT(t, func(ct *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -194,15 +194,15 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listAfterIDDelete, sortWithID) - want := []*v1.User{ + want := []*apiv1.User{ { - Id: 2, - Name: "newname", - Email: "user2@test.no", + ID: apiv1.NewOptUint64(2), + Name: apiv1.NewOptString("newname"), + Email: apiv1.NewOptString("user2@test.no"), }, } - if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { + if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(apiv1.User{}), cmpopts.IgnoreFields(apiv1.User{}, "CreatedAt")); diff != "" { assert.Fail(ct, "unexpected users", "diff (-want +got):\n%s", diff) } }, integrationutil.ScaledTimeout(20*time.Second), 1*time.Second) @@ -219,7 +219,7 @@ func TestUserCommand(t *testing.T) { require.NoError(t, err) assert.Contains(t, deleteResult, "User destroyed") - var listAfterNameDelete []v1.User + var listAfterNameDelete []apiv1.User assert.EventuallyWithT(t, func(c *assert.CollectT) { err = executeAndUnmarshal(headscale, @@ -249,8 +249,8 @@ func TestUserCreateCommand(t *testing.T) { defer scenario.ShutdownAssertNoPanics(t) // Create a user populated with every optional field. The created user is - // returned on stdout and round-tripped through the v1.User type. - created := assertJSONRoundtrip[*v1.User](t, headscale, []string{ + // returned on stdout and round-tripped through the apiv1.User type. + created := assertJSONRoundtrip[*apiv1.User](t, headscale, []string{ "headscale", "users", "create", @@ -261,14 +261,14 @@ func TestUserCreateCommand(t *testing.T) { "--output", "json", }) - assert.Equal(t, "cli-created", created.GetName()) - assert.Equal(t, "CLI Created", created.GetDisplayName()) - assert.Equal(t, "cli-created@example.com", created.GetEmail()) - assert.Equal(t, "https://example.com/avatar.png", created.GetProfilePicUrl()) + assert.Equal(t, "cli-created", created.GetName().Or("")) + assert.Equal(t, "CLI Created", created.GetDisplayName().Or("")) + assert.Equal(t, "cli-created@example.com", created.GetEmail().Or("")) + assert.Equal(t, "https://example.com/avatar.png", created.GetProfilePicUrl().Or("")) // The created fields must survive a list query (read-after-write) and be // filterable by email. - var byEmail []*v1.User + var byEmail []*apiv1.User assert.EventuallyWithT(t, func(ct *assert.CollectT) { err := executeAndUnmarshal(headscale, @@ -286,8 +286,8 @@ func TestUserCreateCommand(t *testing.T) { }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by email") require.Len(t, byEmail, 1) - assert.Equal(t, "cli-created", byEmail[0].GetName()) - assert.Equal(t, "CLI Created", byEmail[0].GetDisplayName()) + assert.Equal(t, "cli-created", byEmail[0].GetName().Or("")) + assert.Equal(t, "CLI Created", byEmail[0].GetDisplayName().Or("")) } // TestUserCommandValidation exercises the validation and error permutations of @@ -325,7 +325,7 @@ func TestUserCommandValidation(t *testing.T) { case tt.wantEmptyList: require.NoError(t, err) - var users []v1.User + var users []apiv1.User require.NoError(t, json.Unmarshal([]byte(out), &users)) require.Empty(t, users)