From 78fd6efb38166d33c5f8cd82d321df56c68931d7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 13 May 2026 13:18:52 +0000 Subject: [PATCH] integration: replace ad-hoc test timeouts with named constants Categorised timeouts in integrationutil/timeouts.go remove the drift opportunity between same-purpose budgets repeated across the suite. The auth, cli, dns, derp, ssh, and tags tests are swept; acl, route, and general tests follow in later commits alongside their other ergonomic fixes. --- integration/auth_key_test.go | 32 ++--- integration/auth_oidc_test.go | 82 ++++++------- integration/auth_web_flow_test.go | 12 +- integration/cli_test.go | 68 +++++------ integration/dns_test.go | 2 +- integration/embedded_derp_test.go | 6 +- integration/integrationutil/timeouts.go | 40 ++++++ integration/ssh_test.go | 6 +- integration/tags_test.go | 154 ++++++++++++------------ 9 files changed, 221 insertions(+), 181 deletions(-) create mode 100644 integration/integrationutil/timeouts.go diff --git a/integration/auth_key_test.go b/integration/auth_key_test.go index 02cf203b..3c2d2fce 100644 --- a/integration/auth_key_test.go +++ b/integration/auth_key_test.go @@ -88,7 +88,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { for _, node := range listNodes { assertLastSeenSetWithCollect(c, node) } - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for expected node list before logout") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected node list before logout") nodeCountBeforeLogout = len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -115,7 +115,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes after logout") assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should match before logout count - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating node persistence after logout (nodes should remain in database)") + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating node persistence after logout (nodes should remain in database)") for _, node := range listNodes { assertLastSeenSet(t, node) @@ -153,7 +153,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes after relogin") assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after relogin - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes)) - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating node count stability after same-user auth key relogin") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node count stability after same-user auth key relogin") for _, node := range listNodes { assertLastSeenSet(t, node) @@ -210,7 +210,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) { for _, node := range listNodes { assertLastSeenSetWithCollect(c, node) } - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for node list after relogin") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node list after relogin") }) } } @@ -266,7 +266,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(c, err) assert.Len(c, listNodes, len(allClients)) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for expected node list before logout") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected node list before logout") nodeCountBeforeLogout = len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -313,7 +313,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { user1Nodes, err = headscale.ListNodes("user1") assert.NoError(ct, err, "Failed to list nodes for user1 after relogin") assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after relogin, got %d nodes", len(allClients), len(user1Nodes)) - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating user1 has all client nodes after auth key relogin") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating user1 has all client nodes after auth key relogin") // Collect expected node IDs for user1 after relogin expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes)) @@ -337,7 +337,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { user2Nodes, err = headscale.ListNodes("user2") assert.NoError(ct, err, "Failed to list nodes for user2 after user1 relogin") assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d clients after user1 relogin, got %d nodes", len(allClients)/2, len(user2Nodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating user2 nodes persist after user1 relogin (should not be affected)") + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating user2 nodes persist after user1 relogin (should not be affected)") t.Logf("Validating client login states after user switch at %s", time.Now().Format(TimestampFormat)) @@ -346,7 +346,7 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) { status, err := client.Status() assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname()) assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating %s is logged in as user1 after auth key user switch", client.Hostname()) + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating %s is logged in as user1 after auth key user switch", client.Hostname()) } } @@ -412,7 +412,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(c, err) assert.Len(c, listNodes, len(allClients)) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for expected node list before logout") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected node list before logout") nodeCountBeforeLogout = len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -527,7 +527,7 @@ func TestAuthKeyDeleteKey(t *testing.T) { user1Nodes, err = headscale.ListNodes("user1") assert.NoError(c, err) assert.Len(c, user1Nodes, 1) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for node to be registered") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for node to be registered") nodeID := user1Nodes[0].GetId() nodeName := user1Nodes[0].GetName() @@ -554,7 +554,7 @@ func TestAuthKeyDeleteKey(t *testing.T) { status, err := client.Status() assert.NoError(c, err) assert.Equal(c, "Stopped", status.BackendState) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "client should be stopped") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "client should be stopped") err = client.Up() require.NoError(t, err) @@ -659,7 +659,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) { assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute, "Subnet routes should contain %s", advertiseRoute) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "initial route should be serving") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving") require.NotNil(t, initialNode, "Initial node should be found") initialNodeID := initialNode.GetId() @@ -677,7 +677,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) { status, err := client.Status() assert.NoError(ct, err) assert.Equal(ct, "NeedsLogin", status.BackendState, "Expected NeedsLogin state after logout") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for logout to complete") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for logout to complete") t.Logf("Logout completed, node should still exist in database") @@ -686,7 +686,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) { nodes, err := headscale.ListNodes() assert.NoError(c, err) assert.Len(c, nodes, 1, "Node should persist in database after logout") - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "node should persist after logout") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "node should persist after logout") // Step 3: Re-authenticate with the SAME user (using auth key) t.Logf("Step 3: Re-authenticating with same user at %s", time.Now().Format(TimestampFormat)) @@ -707,7 +707,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) { status, err := client.Status() assert.NoError(ct, err) assert.Equal(ct, "Running", status.BackendState, "Expected Running state after relogin") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for relogin to complete") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for relogin to complete") t.Logf("Re-authentication completed at %s", time.Now().Format(TimestampFormat)) @@ -741,7 +741,7 @@ func TestAuthKeyLogoutAndReloginRoutesPreserved(t *testing.T) { assert.Equal(c, initialNodeID, node.GetId(), "Node ID should be preserved after same-user relogin") } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "BUG #2896: routes should remain SERVING after logout/relogin with same user") t.Logf("Test completed - verifying issue #2896 fix") diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 88b8a371..2f0c0ea9 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -526,7 +526,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { ct.Errorf("User validation failed after first login - unexpected users: %s", diff) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating user1 creation after initial OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login") t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat)) @@ -538,7 +538,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes during initial validation") assert.Len(ct, listNodes, 1, "Expected exactly 1 node after first login, got %d", len(listNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating initial node creation for user1 after OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating initial node creation for user1 after OIDC login") // Collect expected node IDs for validation after user1 initial login expectedNodes := make([]types.NodeID, 0, 1) @@ -553,7 +553,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64) assert.NoError(ct, err, "Failed to parse node ID from status") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for node ID to be populated in status after initial login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for node ID to be populated in status after initial login") expectedNodes = append(expectedNodes, types.NodeID(nodeID)) @@ -579,7 +579,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err, "Failed to get client status during logout validation") assert.Equal(ct, "NeedsLogin", status.BackendState, "Expected NeedsLogin state after logout, got %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user1 logout to complete before user2 login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user1 logout to complete before user2 login") u, err = ts.LoginWithURL(headscale.GetEndpoint()) require.NoError(t, err) @@ -617,7 +617,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { ct.Errorf("User validation failed after user2 login - expected both user1 and user2: %s", diff) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating both user1 and user2 exist after second OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating both user1 and user2 exist after second OIDC login") var listNodesAfterNewUserLogin []*v1.Node // First, wait for the new node to be created @@ -627,7 +627,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { assert.NoError(ct, err, "Failed to list nodes after user2 login") // We might temporarily have more than 2 nodes during cleanup, so check for at least 2 assert.GreaterOrEqual(ct, len(listNodesAfterNewUserLogin), 2, "Should have at least 2 nodes after user2 login, got %d (may include temporary nodes during cleanup)", len(listNodesAfterNewUserLogin)) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user2 node creation (allowing temporary extra nodes during cleanup)") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user2 node creation (allowing temporary extra nodes during cleanup)") // Then wait for cleanup to stabilize at exactly 2 nodes t.Logf("Waiting for node cleanup stabilization at %s", time.Now().Format(TimestampFormat)) @@ -644,7 +644,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { 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") } - }, integrationutil.ScaledTimeout(90*time.Second), 2*time.Second, "waiting for node count stabilization at exactly 2 nodes after user2 login") + }, integrationutil.PolicyPropagationTimeout, 2*time.Second, "waiting for node count stabilization at exactly 2 nodes after user2 login") // Security validation: Only user2's node should be active after user switch var activeUser2NodeID types.NodeID @@ -674,7 +674,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { } else { assert.Fail(c, "User2 node not found in nodestore") } - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating only user2 node is online after user switch") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating only user2 node is online after user switch") // Before logging out user2, validate we have exactly 2 nodes and both are stable t.Logf("Pre-logout validation: checking node stability at %s", time.Now().Format(TimestampFormat)) @@ -689,7 +689,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { 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]+"...") } - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating stable node count and integrity before user2 logout") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating stable node count and integrity before user2 logout") // Log out user2, and log into user1, no new node should be created, // the node should now "become" node1 again @@ -715,7 +715,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err, "Failed to get client status during user2 logout validation") assert.Equal(ct, "NeedsLogin", status.BackendState, "Expected NeedsLogin state after user2 logout, got %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user2 logout to complete before user1 relogin") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user2 logout to complete before user1 relogin") // Before logging back in, ensure we still have exactly 2 nodes // Note: We skip validateLogoutComplete here since it expects all nodes to be offline, @@ -734,7 +734,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { 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]+"...") } - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating node persistence and integrity after user2 logout") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence and integrity after user2 logout") // We do not actually "change" the user here, it is done by logging in again // as the OIDC mock server is kind of like a stack, and the next user is @@ -750,7 +750,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err, "Failed to get client status during user1 relogin validation") assert.Equal(ct, "Running", status.BackendState, "Expected Running state after user1 relogin, got %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user1 relogin to complete (final login)") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user1 relogin to complete (final login)") t.Logf("Logged back in") t.Log("timestamp: " + time.Now().Format(TimestampFormat) + "\n") @@ -785,7 +785,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { ct.Errorf("Final user validation failed - both users should persist after relogin cycle: %s", diff) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user persistence after complete relogin cycle (user1->user2->user1)") var listNodesAfterLoggingBackIn []*v1.Node // Wait for login to complete and nodes to stabilize @@ -826,7 +826,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { assert.NotEqual(ct, listNodesAfterLoggingBackIn[0].GetNodeKey(), listNodesAfterLoggingBackIn[1].GetNodeKey(), "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.ScaledTimeout(60*time.Second), 2*time.Second, "validating final node state after complete user1->user2->user1 relogin cycle with detailed key validation") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating final node state after complete user1->user2->user1 relogin cycle with detailed key validation") // Security validation: Only user1's node should be active after relogin var activeUser1NodeID types.NodeID @@ -856,7 +856,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) { } else { assert.Fail(c, "User1 node not found in nodestore after relogin") } - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating only user1 node is online after final relogin") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating only user1 node is online after final relogin") } // TestOIDCFollowUpUrl validates the follow-up login flow @@ -933,7 +933,7 @@ func TestOIDCFollowUpUrl(t *testing.T) { assert.NoError(c, err) assert.NotEqual(c, u.String(), st.AuthURL, "AuthURL should change") - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for registration cache to expire and status to reflect NeedsLogin") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for registration cache to expire and status to reflect NeedsLogin") _, err = doLoginURL(ts.Hostname(), newUrl) require.NoError(t, err) @@ -971,7 +971,7 @@ func TestOIDCFollowUpUrl(t *testing.T) { listNodes, err := headscale.ListNodes() assert.NoError(c, err) assert.Len(c, listNodes, 1) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for expected node list after OIDC login") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected node list after OIDC login") } // TestOIDCMultipleOpenedLoginUrls tests the scenario: @@ -1081,7 +1081,7 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) { listNodes, err := headscale.ListNodes() assert.NoError(c, err) assert.Len(c, listNodes, 1) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for expected node list after OIDC login", + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for expected node list after OIDC login", ) } @@ -1176,7 +1176,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { ct.Errorf("User validation failed after first login - unexpected users: %s", diff) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating user1 creation after initial OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 creation after initial OIDC login") t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat)) @@ -1188,7 +1188,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { initialNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes during initial validation") assert.Len(ct, initialNodes, 1, "Expected exactly 1 node after first login, got %d", len(initialNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating initial node creation for user1 after OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating initial node creation for user1 after OIDC login") // Collect expected node IDs for validation after user1 initial login expectedNodes := make([]types.NodeID, 0, 1) @@ -1203,7 +1203,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64) assert.NoError(ct, err, "Failed to parse node ID from status") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for node ID to be populated in status after initial login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for node ID to be populated in status after initial login") expectedNodes = append(expectedNodes, types.NodeID(nodeID)) @@ -1232,7 +1232,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err, "Failed to get client status during logout validation") assert.Equal(ct, "NeedsLogin", status.BackendState, "Expected NeedsLogin state after logout, got %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user1 logout to complete before same-user relogin") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user1 logout to complete before same-user relogin") // Validate node persistence during logout (node should remain in DB) t.Logf("Validating node persistence during logout at %s", time.Now().Format(TimestampFormat)) @@ -1240,7 +1240,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { listNodes, err := headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes during logout validation") assert.Len(ct, listNodes, 1, "Should still have exactly 1 node during logout (node should persist in DB), got %d", len(listNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating node persistence in database during same-user logout") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating node persistence in database during same-user logout") // Login again as the same user (user1) u, err = ts.LoginWithURL(headscale.GetEndpoint()) @@ -1254,7 +1254,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err, "Failed to get client status during relogin validation") assert.Equal(ct, "Running", status.BackendState, "Expected Running state after user1 relogin, got %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for user1 relogin to complete (same user)") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for user1 relogin to complete (same user)") t.Logf("Final validation: checking user persistence after same-user relogin at %s", time.Now().Format(TimestampFormat)) assert.EventuallyWithT(t, func(ct *assert.CollectT) { @@ -1279,7 +1279,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { if diff := cmp.Diff(wantUsers, listUsers, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" { ct.Errorf("Final user validation failed - user1 should persist after same-user relogin: %s", diff) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle") var finalNodes []*v1.Node @@ -1302,7 +1302,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { assert.NotEqual(ct, initialNodeKey, finalNode.GetNodeKey(), "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.ScaledTimeout(60*time.Second), 2*time.Second, "validating final node state after same-user OIDC relogin cycle with key preservation validation") + }, 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()) @@ -1322,7 +1322,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) { } else { assert.Fail(c, "User1 node not found in nodestore after same-user relogin") } - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating user1 node is online after same-user OIDC relogin") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating user1 node is online after same-user OIDC relogin") } // TestOIDCExpiryAfterRestart validates that node expiry is preserved @@ -1398,7 +1398,7 @@ func TestOIDCExpiryAfterRestart(t *testing.T) { initialExpiry = expiryTime t.Logf("Initial expiry set to: %v (expires in %v)", expiryTime, time.Until(expiryTime)) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating initial expiry after OIDC login") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating initial expiry after OIDC login") // Now restart the tailscaled container t.Logf("Restarting tailscaled container at %s", time.Now().Format(TimestampFormat)) @@ -1420,7 +1420,7 @@ func TestOIDCExpiryAfterRestart(t *testing.T) { } assert.Equal(ct, "Running", status.BackendState) - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "waiting for tailscale to reconnect after restart") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "waiting for tailscale to reconnect after restart") // THE CRITICAL TEST: Verify expiry is still set correctly after restart t.Logf("Validating expiry preservation after restart at %s", time.Now().Format(TimestampFormat)) @@ -1448,7 +1448,7 @@ func TestOIDCExpiryAfterRestart(t *testing.T) { t.Logf("SUCCESS: Expiry preserved after restart: %v (expires in %v)", expiryTime, time.Until(expiryTime)) } - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "validating expiry preservation after restart") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "validating expiry preservation after restart") } // TestOIDCACLPolicyOnJoin validates that ACL policies are correctly applied @@ -1570,7 +1570,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) { gatewayNodeID = gatewayNode.GetId() assert.Len(ct, gatewayNode.GetAvailableRoutes(), 1) assert.Contains(ct, gatewayNode.GetAvailableRoutes(), advertiseRoute) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "route advertisement should propagate to headscale") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route advertisement should propagate to headscale") // Approve the advertised route _, err = headscale.ApproveRoutes( @@ -1588,7 +1588,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) { gatewayNode := nodes[0] assert.Len(ct, gatewayNode.GetApprovedRoutes(), 1) assert.Contains(ct, gatewayNode.GetApprovedRoutes(), advertiseRoute) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "route approval should propagate to headscale") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "route approval should propagate to headscale") // NOW create the OIDC user by having them join // This is where issue #2888 manifests - the new OIDC node should immediately @@ -1658,7 +1658,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) { t.Logf("Gateway peer AllowedIPs: %v", allowedIPs) } } - }, integrationutil.ScaledTimeout(15*time.Second), 500*time.Millisecond, + }, integrationutil.ScaledTimeout(15*time.Second), integrationutil.SlowPoll, "OIDC user should immediately see gateway's advertised route without client restart (issue #2888)") // Verify that the Gateway node sees the OIDC node's advertised route (AutoApproveRoutes check) @@ -1690,7 +1690,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) { "Gateway user should immediately see OIDC's advertised route %s in PrimaryRoutes", oidcAdvertiseRoute) } } - }, integrationutil.ScaledTimeout(15*time.Second), 500*time.Millisecond, + }, integrationutil.ScaledTimeout(15*time.Second), integrationutil.SlowPoll, "Gateway user should immediately see OIDC's advertised route (AutoApproveRoutes check)") // Additional validation: Verify nodes in headscale match expectations @@ -1738,7 +1738,7 @@ func TestOIDCACLPolicyOnJoin(t *testing.T) { assert.Equal(ct, "oidcuser", oidcUserFound.GetName()) assert.Equal(ct, "oidcuser@headscale.net", oidcUserFound.GetEmail()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "headscale should have correct users and nodes") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "headscale should have correct users and nodes") t.Logf("Test completed successfully - issue #2888 fix validated") } @@ -1829,7 +1829,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err) assert.Equal(ct, "Running", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for initial login to complete") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for initial login to complete") // 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)) @@ -1853,7 +1853,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { assert.Contains(c, initialNode.GetSubnetRoutes(), advertiseRoute, "Subnet routes should contain %s", advertiseRoute) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "initial route should be serving") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "initial route should be serving") require.NotNil(t, initialNode, "Initial node should be found") initialNodeID := initialNode.GetId() @@ -1871,7 +1871,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err) assert.Equal(ct, "NeedsLogin", status.BackendState, "Expected NeedsLogin state after logout") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for logout to complete") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for logout to complete") t.Logf("Logout completed, node should still exist in database") @@ -1880,7 +1880,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { nodes, err := headscale.ListNodes() assert.NoError(c, err) assert.Len(c, nodes, 1, "Node should persist in database after logout") - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "node should persist after logout") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "node should persist after logout") // Step 3: Re-authenticate via OIDC as the same user t.Logf("Step 3: Re-authenticating with same user via OIDC at %s", time.Now().Format(TimestampFormat)) @@ -1896,7 +1896,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { status, err := ts.Status() assert.NoError(ct, err) assert.Equal(ct, "Running", status.BackendState, "Expected Running state after relogin") - }, integrationutil.ScaledTimeout(30*time.Second), 1*time.Second, "waiting for relogin to complete") + }, integrationutil.StatusReadyTimeout, 1*time.Second, "waiting for relogin to complete") t.Logf("Re-authentication completed at %s", time.Now().Format(TimestampFormat)) @@ -1930,7 +1930,7 @@ func TestOIDCReloginSameUserRoutesPreserved(t *testing.T) { assert.Equal(c, initialNodeID, node.GetId(), "Node ID should be preserved after same-user relogin") } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "BUG #2896: routes should remain SERVING after OIDC logout/relogin with same user") t.Logf("Test completed - verifying issue #2896 fix for OIDC") diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index f8f66df0..752d78a3 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -107,7 +107,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes after web authentication") assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating node count matches client count after web authentication") + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating node count matches client count after web authentication") nodeCountBeforeLogout := len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -154,7 +154,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes after web flow logout") assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after logout - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes)) - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating node persistence in database after web flow logout") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating node persistence in database after web flow logout") t.Logf("node count first login: %d, after relogin: %d", nodeCountBeforeLogout, len(listNodes)) // Validate connection state after relogin @@ -265,7 +265,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) { listNodes, err = headscale.ListNodes() assert.NoError(ct, err, "Failed to list nodes after initial web authentication") assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating node count matches client count after initial web authentication") + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating node count matches client count after initial web authentication") nodeCountBeforeLogout := len(listNodes) t.Logf("node count before logout: %d", nodeCountBeforeLogout) @@ -325,7 +325,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) { user1Nodes, err = headscale.ListNodes("user1") assert.NoError(ct, err, "Failed to list nodes for user1 after web flow relogin") assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after web flow relogin, got %d nodes", len(allClients), len(user1Nodes)) - }, integrationutil.ScaledTimeout(60*time.Second), 2*time.Second, "validating user1 has all client nodes after web flow user switch relogin") + }, integrationutil.HAConvergeTimeout, 2*time.Second, "validating user1 has all client nodes after web flow user switch relogin") // Collect expected node IDs for user1 after relogin expectedUser1Nodes := make([]types.NodeID, 0, len(user1Nodes)) @@ -347,7 +347,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) { user2Nodes, err = headscale.ListNodes("user2") assert.NoError(ct, err, "Failed to list nodes for user2 after CLI registration to user1") assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d old nodes (likely expired) after CLI registration to user1, got %d nodes", len(allClients)/2, len(user2Nodes)) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating user2 old nodes remain in database after CLI registration to user1") + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating user2 old nodes remain in database after CLI registration to user1") t.Logf("Validating client login states after web flow user switch at %s", time.Now().Format(TimestampFormat)) @@ -356,7 +356,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) { status, err := client.Status() assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname()) assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after web flow user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second, "validating %s is logged in as user1 after web flow user switch", client.Hostname()) + }, integrationutil.StatusReadyTimeout, 2*time.Second, "validating %s is logged in as user1 after web flow user switch", client.Hostname()) } // Test connectivity after user switch diff --git a/integration/cli_test.go b/integration/cli_test.go index 7ee00271..fc40bc08 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -147,7 +147,7 @@ func TestUserCommand(t *testing.T) { &listByUsername, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for user list by username") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by username") slices.SortFunc(listByUsername, sortWithID) @@ -178,7 +178,7 @@ func TestUserCommand(t *testing.T) { &listByID, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for user list by ID") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list by ID") slices.SortFunc(listByID, sortWithID) @@ -264,7 +264,7 @@ func TestUserCommand(t *testing.T) { ) assert.NoError(c, err) assert.Empty(c, listAfterNameDelete) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for user list after name delete") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user list after name delete") } func TestPreAuthKeyCommand(t *testing.T) { @@ -315,7 +315,7 @@ func TestPreAuthKeyCommand(t *testing.T) { &preAuthKey, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth key creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation") keys[index] = &preAuthKey } @@ -337,7 +337,7 @@ func TestPreAuthKeyCommand(t *testing.T) { &listedPreAuthKeys, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth keys list") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list") // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 4) @@ -413,7 +413,7 @@ func TestPreAuthKeyCommand(t *testing.T) { &listedPreAuthKeysAfterExpire, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth keys list after expire") + }, 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())) @@ -457,7 +457,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { &preAuthKey, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth key creation without expiry") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth key creation without expiry") var listedPreAuthKeys []v1.PreAuthKey @@ -474,7 +474,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { &listedPreAuthKeys, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth keys list") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list") // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 2) @@ -523,7 +523,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { &preAuthReusableKey, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for reusable preauth key creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for reusable preauth key creation") var preAuthEphemeralKey v1.PreAuthKey @@ -543,7 +543,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { &preAuthEphemeralKey, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for ephemeral preauth key creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for ephemeral preauth key creation") assert.True(t, preAuthEphemeralKey.GetEphemeral()) assert.False(t, preAuthEphemeralKey.GetReusable()) @@ -563,7 +563,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { &listedPreAuthKeys, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for preauth keys list after reusable/ephemeral creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for preauth keys list after reusable/ephemeral creation") // There is one key created by "scenario.CreateHeadscaleEnv" assert.Len(t, listedPreAuthKeys, 3) @@ -621,7 +621,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { &user2Key, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for user2 preauth key creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user2 preauth key creation") var listNodes []*v1.Node @@ -653,7 +653,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { assert.NoError(ct, err) assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState, "Expected node to be logged out, backend state: %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) err = client.Login(headscale.GetEndpoint(), user2Key.GetKey()) require.NoError(t, err) @@ -665,7 +665,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { // With tags-as-identity model, tagged nodes show as TaggedDevices user (2147455555) // The PreAuthKey was created with tags, so the node is tagged assert.Equal(ct, "userid:2147455555", status.Self.UserID.String(), "Expected node to be logged in as tagged-devices user") - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) assert.EventuallyWithT(t, func(ct *assert.CollectT) { var err error @@ -730,7 +730,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { &user2Key, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for user2 tagged preauth key creation") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for user2 tagged preauth key creation") allClients, err := scenario.ListTailscaleClients() requireNoErrListClients(t, err) @@ -751,7 +751,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { assert.NoError(ct, err) assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState, "Expected node to be logged out, backend state: %s", status.BackendState) - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) // Log in with the tagged PreAuthKey (from user2, with tags) err = client.Login(headscale.GetEndpoint(), user2Key.GetKey()) @@ -763,7 +763,7 @@ func TestTaggedNodesCLIOutput(t *testing.T) { assert.Equal(ct, "Running", status.BackendState, "Expected node to be logged in, backend state: %s", status.BackendState) // With tags-as-identity model, tagged nodes show as TaggedDevices user (2147455555) assert.Equal(ct, "userid:2147455555", status.Self.UserID.String(), "Expected node to be logged in as tagged-devices user") - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) // Wait for the second node to appear var listNodes []*v1.Node @@ -850,7 +850,7 @@ func TestApiKeyCommand(t *testing.T) { &listedAPIKeys, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for API keys list") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list") assert.Len(t, listedAPIKeys, 5) @@ -925,7 +925,7 @@ func TestApiKeyCommand(t *testing.T) { &listedAfterExpireAPIKeys, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for API keys list after expire") + }, 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 { @@ -967,7 +967,7 @@ func TestApiKeyCommand(t *testing.T) { &listedAPIKeysAfterDelete, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for API keys list after delete") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after delete") assert.Len(t, listedAPIKeysAfterDelete, 4) @@ -996,7 +996,7 @@ func TestApiKeyCommand(t *testing.T) { &listedAPIKeysAfterExpireByID, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for API keys list after expire by ID") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after expire by ID") // Verify the key was expired for idx := range listedAPIKeysAfterExpireByID { @@ -1032,7 +1032,7 @@ func TestApiKeyCommand(t *testing.T) { &listedAPIKeysAfterDeleteByID, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for API keys list after delete by ID") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for API keys list after delete by ID") assert.Len(t, listedAPIKeysAfterDeleteByID, 3) @@ -1109,7 +1109,7 @@ func TestNodeCommand(t *testing.T) { &node, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for node registration") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node registration") nodes[index] = &node } @@ -1194,7 +1194,7 @@ func TestNodeCommand(t *testing.T) { &node, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for other-user node registration") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for other-user node registration") otherUserMachines[index] = &node } @@ -1219,7 +1219,7 @@ func TestNodeCommand(t *testing.T) { &listAllWithotherUser, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list after adding other-user nodes") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list after adding other-user nodes") // All nodes, nodes + otherUser assert.Len(t, listAllWithotherUser, 7) @@ -1248,7 +1248,7 @@ func TestNodeCommand(t *testing.T) { &listOnlyotherUserMachineUser, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list filtered by other-user") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list filtered by other-user") assert.Len(t, listOnlyotherUserMachineUser, 2) @@ -1368,7 +1368,7 @@ func TestNodeExpireCommand(t *testing.T) { &node, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for node-expire-user node registration") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node-expire-user node registration") nodes[index] = &node } @@ -1390,7 +1390,7 @@ func TestNodeExpireCommand(t *testing.T) { &listAll, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list in expire test") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list in expire test") assert.Len(t, listAll, 5) @@ -1429,7 +1429,7 @@ func TestNodeExpireCommand(t *testing.T) { &listAllAfterExpiry, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list after expiry") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list after expiry") assert.Len(t, listAllAfterExpiry, 5) @@ -1506,7 +1506,7 @@ func TestNodeRenameCommand(t *testing.T) { &node, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for node-rename-command node registration") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for node-rename-command node registration") nodes[index] = &node } @@ -1528,7 +1528,7 @@ func TestNodeRenameCommand(t *testing.T) { &listAll, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list in rename test") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list in rename test") assert.Len(t, listAll, 5) @@ -1569,7 +1569,7 @@ func TestNodeRenameCommand(t *testing.T) { &listAllAfterRename, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list after rename") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list after rename") assert.Len(t, listAllAfterRename, 5) @@ -1607,7 +1607,7 @@ func TestNodeRenameCommand(t *testing.T) { &listAllAfterRenameAttempt, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for nodes list after failed rename attempt") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for nodes list after failed rename attempt") assert.Len(t, listAllAfterRenameAttempt, 5) @@ -1696,7 +1696,7 @@ func TestPolicyCommand(t *testing.T) { &output, ) assert.NoError(c, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond, "Waiting for policy get command") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll, "Waiting for policy get command") assert.Len(t, output.TagOwners, 1) assert.Len(t, output.ACLs, 1) diff --git a/integration/dns_test.go b/integration/dns_test.go index 3841c7a1..5dc2386a 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -67,7 +67,7 @@ func TestResolveMagicDNS(t *testing.T) { for _, ip := range ips { assert.Contains(ct, result, ip.String(), "IP %s should be found in DNS resolution result from %s to %s", ip.String(), client.Hostname(), peer.Hostname()) } - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) } } } diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index f55de2cc..64132290 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -153,7 +153,7 @@ func derpServerScenario( assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP", client.Hostname()) } - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) } success := pingDerpAllHelper(t, allClients, allHostnames) @@ -174,7 +174,7 @@ func derpServerScenario( assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP after first run", client.Hostname()) } - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) } t.Logf("Run 1: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) @@ -200,7 +200,7 @@ func derpServerScenario( assert.NotContains(ct, health, "could not connect to the 'Headscale Embedded DERP' relay server.", "Client %s should be connected to Headscale Embedded DERP after second run", client.Hostname()) } - }, integrationutil.ScaledTimeout(30*time.Second), 2*time.Second) + }, integrationutil.StatusReadyTimeout, 2*time.Second) } t.Logf("Run2: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) diff --git a/integration/integrationutil/timeouts.go b/integration/integrationutil/timeouts.go new file mode 100644 index 00000000..593c7938 --- /dev/null +++ b/integration/integrationutil/timeouts.go @@ -0,0 +1,40 @@ +package integrationutil + +import "time" + +// CI-scaled convergence budgets. ScaledTimeout doubles each on CI. +var ( + // HAConvergeTimeout: routes / ACL / policy propagation to reach + // every node and show up in status, traceroute, or curl. + HAConvergeTimeout = ScaledTimeout(60 * time.Second) + + // HASlowConvergeTimeout: multi-step failover sequences that need + // at least one HA prober cycle plus a data-plane settle. + HASlowConvergeTimeout = ScaledTimeout(120 * time.Second) + + // PolicyPropagationTimeout: post-SetPolicy filter rules and peer + // reachability to reflect the change. Sized for wgengine's + // rule-reload lag on contended CI runners (~2 min observed). + PolicyPropagationTimeout = ScaledTimeout(180 * time.Second) + + // AuthFlowTimeout: OIDC / web-auth / preauth-key flows to reach + // Running. + AuthFlowTimeout = ScaledTimeout(30 * time.Second) + + // StatusReadyTimeout: post-event read to reflect the event + // (created node visible in list, set tags visible on node). + StatusReadyTimeout = ScaledTimeout(30 * time.Second) +) + +// Polling intervals for EventuallyWithT. +const ( + // FastPoll: in-process reads (HA state, route table snapshots). + FastPoll = 200 * time.Millisecond + + // SlowPoll: cross-container reads (tailscale status, curl, + // headscale API) where each tick pays a docker exec round-trip. + SlowPoll = 500 * time.Millisecond + + // PingPoll: gap between full ping-matrix sweeps. + PingPoll = 2 * time.Second +) diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 544ca03a..0b5f661a 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -471,7 +471,7 @@ func doSSHWithRetryAsUser( // For all other errors, assert no error to trigger retry assert.NoError(ct, err) - }, integrationutil.ScaledTimeout(10*time.Second), 200*time.Millisecond) + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.FastPoll) } else { // For failure cases, just execute once result, stderr, err = client.Execute(command) @@ -701,7 +701,7 @@ func findSSHCheckAuthID(t *testing.T, headscale ControlServer) string { } assert.NotEmpty(c, authID, "auth-id not found in headscale logs") - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "waiting for SSH check auth-id in headscale logs") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "waiting for SSH check auth-id in headscale logs") return authID } @@ -810,7 +810,7 @@ func findNewSSHCheckAuthID( } assert.NotEmpty(c, authID, "new auth-id not found in headscale logs") - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "waiting for new SSH check auth-id") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "waiting for new SSH check auth-id") return authID } diff --git a/integration/tags_test.go b/integration/tags_test.go index 1d66c593..dc0320b5 100644 --- a/integration/tags_test.go +++ b/integration/tags_test.go @@ -182,7 +182,7 @@ func TestTagsAuthKeyWithTagRequestDifferentTag(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -252,7 +252,7 @@ func TestTagsAuthKeyWithTagNoAdvertiseFlag(t *testing.T) { t.Logf("Node registered with tags: %v", node.GetTags()) assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying node inherited tags from auth key") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node inherited tags from auth key") t.Logf("Test 2.2 completed - node inherited tags from auth key") } @@ -320,7 +320,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") t.Logf("Node registered with tag:valid-owned, now attempting to add tag:second via CLI") @@ -353,7 +353,7 @@ func TestTagsAuthKeyWithTagCannotAddViaCLI(t *testing.T) { assert.Fail(c, "Tags should not have changed") } } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying tags unchanged") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tags unchanged") } } @@ -416,7 +416,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) { nodes, err := headscale.ListNodes() assert.NoError(c, err) assert.Len(c, nodes, 1) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") t.Logf("Node registered, now attempting to change to different tag via CLI") @@ -448,7 +448,7 @@ func TestTagsAuthKeyWithTagCannotChangeViaCLI(t *testing.T) { assert.Fail(c, "Tags should not have changed") } } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying tags unchanged") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tags unchanged") } } @@ -519,7 +519,7 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) { nodeID = nodes[0].GetId() assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") t.Logf("Step 1 complete: Node %d registered with tag:valid-owned", nodeID) @@ -536,12 +536,12 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) { t.Logf("After admin assignment, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin tag assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin tag assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin tag assignment propagated to node self") t.Logf("Step 2 complete: Admin assigned tag:second (verified on both server and node self)") @@ -569,12 +569,12 @@ func TestTagsAuthKeyWithTagAdminOverrideReauthPreserves(t *testing.T) { // Expected: admin-assigned tags are preserved through reauth assertNodeHasTagsWithCollect(c, node, []string{"tag:second"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after reauth on server") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after reauth on server") // Verify admin tags are preserved in node's self view after reauth (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after reauth in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after reauth in node self") t.Logf("Test 2.5 PASS: Admin tags preserved through reauth (admin decisions are authoritative)") } @@ -645,7 +645,7 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) { if len(nodes) == 1 { nodeID = nodes[0].GetId() } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns multiple tags via headscale CLI err = headscale.SetNodeTags(nodeID, []string{"tag:valid-owned", "tag:second"}) @@ -659,12 +659,12 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin tag assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin tag assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin tag assignment propagated to node self") t.Logf("Admin assigned both tags, now attempting to reduce via CLI") @@ -691,12 +691,12 @@ func TestTagsAuthKeyWithTagCLICannotModifyAdminTags(t *testing.T) { // Expected: tags should remain unchanged (admin wins) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved after CLI attempt on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after CLI attempt on server") // Verify admin tags are preserved in node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after CLI attempt in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after CLI attempt in node self") t.Logf("Test 2.6 PASS: Admin tags preserved - CLI cannot modify admin-assigned tags") } @@ -770,7 +770,7 @@ func TestTagsAuthKeyWithoutTagCannotRequestTags(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -837,7 +837,7 @@ func TestTagsAuthKeyWithoutTagRegisterNoTags(t *testing.T) { t.Logf("Node registered with tags: %v", nodes[0].GetTags()) assertNodeHasNoTagsWithCollect(c, nodes[0]) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying node has no tags") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node has no tags") t.Logf("Test 3.2 completed - node registered without tags") } @@ -905,7 +905,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) { if len(nodes) == 1 { assertNodeHasNoTagsWithCollect(c, nodes[0]) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") t.Logf("Node registered without tags, attempting to add via CLI") @@ -936,7 +936,7 @@ func TestTagsAuthKeyWithoutTagCannotAddViaCLI(t *testing.T) { assert.Fail(c, "Tags should not have changed") } } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying tags unchanged") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying tags unchanged") } } @@ -1007,7 +1007,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) { nodeID = nodes[0].GetId() assertNodeHasNoTagsWithCollect(c, nodes[0]) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns tags err = headscale.SetNodeTags(nodeID, []string{"tag:valid-owned"}) @@ -1021,12 +1021,12 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin tag assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin tag assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin tag assignment propagated to node self") t.Logf("Admin assigned tag, now running CLI with --reset") @@ -1050,12 +1050,12 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithReset(t *testing.T) { t.Logf("After --reset, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved after --reset on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after --reset on server") // Verify admin tags are preserved in node's self view after --reset (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after --reset in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after --reset in node self") t.Logf("Test 3.4 PASS: Admin tags preserved after --reset") } @@ -1126,7 +1126,7 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T) if len(nodes) == 1 { nodeID = nodes[0].GetId() } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns tags err = headscale.SetNodeTags(nodeID, []string{"tag:valid-owned"}) @@ -1140,12 +1140,12 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T) if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin tag assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin tag assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin tag assignment propagated to node self") t.Logf("Admin assigned tag, now running CLI with empty --advertise-tags") @@ -1169,12 +1169,12 @@ func TestTagsAuthKeyWithoutTagCLINoOpAfterAdminWithEmptyAdvertise(t *testing.T) t.Logf("After empty --advertise-tags, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved after empty --advertise-tags on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after empty --advertise-tags on server") // Verify admin tags are preserved in node's self view after empty --advertise-tags (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after empty --advertise-tags in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after empty --advertise-tags in node self") t.Logf("Test 3.5 PASS: Admin tags preserved after empty --advertise-tags") } @@ -1245,7 +1245,7 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) { if len(nodes) == 1 { nodeID = nodes[0].GetId() } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns multiple tags err = headscale.SetNodeTags(nodeID, []string{"tag:valid-owned", "tag:second"}) @@ -1259,12 +1259,12 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin tag assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin tag assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin tag assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin tag assignment propagated to node self") t.Logf("Admin assigned both tags, now attempting to reduce via CLI") @@ -1288,12 +1288,12 @@ func TestTagsAuthKeyWithoutTagCLICannotReduceAdminMultiTag(t *testing.T) { t.Logf("After CLI reduce attempt, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved after CLI reduce attempt on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved after CLI reduce attempt on server") // Verify admin tags are preserved in node's self view after CLI reduce attempt (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved after CLI reduce attempt in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved after CLI reduce attempt in node self") t.Logf("Test 3.6 PASS: Admin tags preserved - CLI cannot reduce admin-assigned multi-tag set") } @@ -1369,7 +1369,7 @@ func TestTagsUserLoginOwnedTagAtRegistration(t *testing.T) { t.Logf("Node registered with tags: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying node has advertised tag") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node has advertised tag") t.Logf("Test 1.1 completed - web auth with owned tag succeeded") } @@ -1442,7 +1442,7 @@ func TestTagsUserLoginNonExistentTagAtRegistration(t *testing.T) { "Non-existent tag should not be applied to node") t.Logf("Test 1.2: Node registered with tags: %v (non-existent tag correctly rejected)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node registration result") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node registration result") } } @@ -1510,7 +1510,7 @@ func TestTagsUserLoginUnownedTagAtRegistration(t *testing.T) { "Unowned tag should not be applied to node (tag:valid-unowned is owned by other-user)") t.Logf("Test 1.3: Node registered with tags: %v (unowned tag correctly rejected)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node registration result") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node registration result") } // TestTagsUserLoginAddTagViaCLIReauth tests that a user can add tags via CLI reauthentication. @@ -1574,7 +1574,7 @@ func TestTagsUserLoginAddTagViaCLIReauth(t *testing.T) { if len(nodes) == 1 { t.Logf("Initial tags: %v", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "checking initial tags") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking initial tags") // Step 2: Try to add second tag via CLI t.Logf("Attempting to add second tag via CLI reauth") @@ -1601,7 +1601,7 @@ func TestTagsUserLoginAddTagViaCLIReauth(t *testing.T) { t.Logf("Test 1.4: Tags are %v (may require manual reauth completion)", nodes[0].GetTags()) } } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "checking tags after CLI") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking tags after CLI") } // TestTagsUserLoginRemoveTagViaCLIReauth tests that a user can remove tags via CLI reauthentication. @@ -1665,7 +1665,7 @@ func TestTagsUserLoginRemoveTagViaCLIReauth(t *testing.T) { if len(nodes) == 1 { t.Logf("Initial tags: %v", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "checking initial tags") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking initial tags") // Step 2: Try to remove second tag via CLI t.Logf("Attempting to remove tag via CLI reauth") @@ -1690,7 +1690,7 @@ func TestTagsUserLoginRemoveTagViaCLIReauth(t *testing.T) { t.Logf("Test 1.5 PASS: Only one tag after removal") } } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "checking tags after CLI") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking tags after CLI") } // TestTagsUserLoginCLINoOpAfterAdminAssignment tests that CLI advertise-tags becomes @@ -1760,7 +1760,7 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) { nodeID = nodes[0].GetId() t.Logf("Step 1: Node %d registered with tags: %v", nodeID, nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns different tag err = headscale.SetNodeTags(nodeID, []string{"tag:second"}) @@ -1775,12 +1775,12 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) { t.Logf("Step 2: After admin assignment, server tags: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin assignment propagated to node self") // Step 3: Try to change tags via CLI command := []string{ @@ -1801,12 +1801,12 @@ func TestTagsUserLoginCLINoOpAfterAdminAssignment(t *testing.T) { t.Logf("Step 3: After CLI, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved - CLI advertise-tags should be no-op on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved - CLI advertise-tags should be no-op on server") // Verify admin tags are preserved in node's self view after CLI attempt (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved - CLI advertise-tags should be no-op in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved - CLI advertise-tags should be no-op in node self") t.Logf("Test 1.6 PASS: Admin tags preserved (CLI was no-op)") } @@ -1876,7 +1876,7 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) { if len(nodes) == 1 { nodeID = nodes[0].GetId() } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Step 2: Admin assigns both tags err = headscale.SetNodeTags(nodeID, []string{"tag:valid-owned", "tag:second"}) @@ -1891,12 +1891,12 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) { t.Logf("After admin assignment, server tags: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying admin assignment on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying admin assignment on server") // Verify admin assignment propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying admin assignment propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying admin assignment propagated to node self") // Step 3: Try to reduce tags via CLI command := []string{ @@ -1917,12 +1917,12 @@ func TestTagsUserLoginCLICannotRemoveAdminTags(t *testing.T) { t.Logf("Test 1.7: After CLI, server tags are: %v", nodes[0].GetTags()) assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "admin tags should be preserved - CLI cannot remove them on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "admin tags should be preserved - CLI cannot remove them on server") // Verify admin tags are preserved in node's self view after CLI attempt (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned", "tag:second"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "admin tags should be preserved - CLI cannot remove them in node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "admin tags should be preserved - CLI cannot remove them in node self") t.Logf("Test 1.7 PASS: Admin tags preserved (CLI cannot remove)") } @@ -1995,7 +1995,7 @@ func TestTagsAuthKeyWithTagRequestNonExistentTag(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -2065,7 +2065,7 @@ func TestTagsAuthKeyWithTagRequestUnownedTag(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -2139,7 +2139,7 @@ func TestTagsAuthKeyWithoutTagRequestNonExistentTag(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -2209,7 +2209,7 @@ func TestTagsAuthKeyWithoutTagRequestUnownedTag(t *testing.T) { if len(nodes) == 1 { t.Logf("Node registered with tags: %v (expected rejection)", nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "checking node state") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "checking node state") t.Fail() } @@ -2281,7 +2281,7 @@ func TestTagsAdminAPICannotSetNonExistentTag(t *testing.T) { nodeID = nodes[0].GetId() t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration") // Try to set a non-existent tag via admin API - should fail err = headscale.SetNodeTags(nodeID, []string{"tag:nonexistent"}) @@ -2353,7 +2353,7 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) { nodeID = nodes[0].GetId() t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration") // Admin sets an "unowned" tag - should SUCCEED because admin has full authority // (tag:valid-unowned is owned by other-user, but admin can assign it) @@ -2369,12 +2369,12 @@ func TestTagsAdminAPICanSetUnownedTag(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-unowned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying unowned tag was applied on server") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying unowned tag was applied on server") // Verify the tag was propagated to node's self view (issue #2978) assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-unowned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying unowned tag propagated to node self") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying unowned tag propagated to node self") t.Logf("Test 4.2 PASS: Admin API correctly allowed setting unowned tag") } @@ -2441,7 +2441,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) { nodeID = nodes[0].GetId() t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration") // Try to remove all tags - should fail err = headscale.SetNodeTags(nodeID, []string{}) @@ -2458,7 +2458,7 @@ func TestTagsAdminAPICannotRemoveAllTags(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying original tags preserved") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying original tags preserved") } // assertNetmapSelfHasTagsWithCollect asserts that the client's netmap self node has expected tags. @@ -2563,12 +2563,12 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) { nodeID = nodes[0].GetId() assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for initial registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for initial registration") // Verify client initially sees tag:valid-owned assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-owned"}) - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "client should see initial tag") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "client should see initial tag") t.Logf("Step 1: Node %d registered via web auth with --advertise-tags=tag:valid-owned, client sees it", nodeID) @@ -2588,7 +2588,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:second"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "server should show tag:second after first call") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "server should show tag:second after first call") t.Log("Step 2a: Server shows tag:second after first call") @@ -2638,11 +2638,11 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) { t.Log("Step 3a: Verifying client self view updates after SECOND call") assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "client status.Self should update to tag:second after SECOND call") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "client status.Self should update to tag:second after SECOND call") assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNetmapSelfHasTagsWithCollect(c, client, []string{"tag:second"}) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "client netmap.SelfNode should update to tag:second after SECOND call") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "client netmap.SelfNode should update to tag:second after SECOND call") t.Log("Step 3b: Client self view updated to tag:second after SECOND call") @@ -2660,7 +2660,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-unowned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "server should show tag:valid-unowned") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "server should show tag:valid-unowned") t.Log("Step 4a: Server shows tag:valid-unowned after first call") @@ -2692,11 +2692,11 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) { t.Log("Step 5a: Verifying client self view updates after SECOND call") assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNodeSelfHasTagsWithCollect(c, client, []string{"tag:valid-unowned"}) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "client status.Self should update to tag:valid-unowned after SECOND call") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "client status.Self should update to tag:valid-unowned after SECOND call") assert.EventuallyWithT(t, func(c *assert.CollectT) { assertNetmapSelfHasTagsWithCollect(c, client, []string{"tag:valid-unowned"}) - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "client netmap.SelfNode should update to tag:valid-unowned after SECOND call") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "client netmap.SelfNode should update to tag:valid-unowned after SECOND call") t.Log("Test complete - see logs for bug reproduction details") } @@ -2763,7 +2763,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) { nodeID = nodes[0].GetId() t.Logf("Node %d registered with tags: %v", nodeID, nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "waiting for registration") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "waiting for registration") // Try to set a tag without the "tag:" prefix - should fail err = headscale.SetNodeTags(nodeID, []string{"invalid-no-prefix"}) @@ -2780,7 +2780,7 @@ func TestTagsAdminAPICannotSetInvalidFormat(t *testing.T) { if len(nodes) == 1 { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(10*time.Second), 500*time.Millisecond, "verifying original tags preserved") + }, integrationutil.ScaledTimeout(10*time.Second), integrationutil.SlowPoll, "verifying original tags preserved") } // ============================================================================= @@ -2871,7 +2871,7 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) { // Verify node has the expected tags assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned", "tag:second"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "checking initial tags") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "checking initial tags") // Step 2: Reauth with empty tags to remove all tags t.Logf("Step 2: Reauthenticating with empty tag list to untag device (%s)", tc.name) @@ -2947,7 +2947,7 @@ func TestTagsUserLoginReauthWithEmptyTagsRemovesAllTags(t *testing.T) { tc.name, tagTestUser, node.GetTags(), node.GetUser().GetName()) } } - }, integrationutil.ScaledTimeout(60*time.Second), 1*time.Second, "verifying tags removed and ownership returned") + }, integrationutil.HAConvergeTimeout, 1*time.Second, "verifying tags removed and ownership returned") }) } @@ -3020,7 +3020,7 @@ func TestTagsAuthKeyWithoutUserInheritsTags(t *testing.T) { t.Logf("Node registered with tags: %v", node.GetTags()) assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"}) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "verifying node inherited tags from auth key") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "verifying node inherited tags from auth key") t.Logf("Test 5.1 PASS: Node inherited tags from tags-only auth key") } @@ -3158,7 +3158,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) { assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"}) t.Logf("Initial state - Node ID: %d, Tags: %v", nodes[0].GetId(), nodes[0].GetTags()) } - }, integrationutil.ScaledTimeout(30*time.Second), 500*time.Millisecond, "node should be tagged initially") + }, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "node should be tagged initially") // Step 2: Force reauth with empty tags (triggers web auth flow) command := []string{ @@ -3201,5 +3201,5 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) { t.Logf("After conversion - Node ID: %d, Tags: %v, User: %s", nodes[0].GetId(), nodes[0].GetTags(), nodes[0].GetUser().GetName()) } - }, integrationutil.ScaledTimeout(60*time.Second), 1*time.Second, "node should be user-owned after conversion via CLI register") + }, integrationutil.HAConvergeTimeout, 1*time.Second, "node should be user-owned after conversion via CLI register") }