Ctrl K
ComunidadGuías…

tests/agent_tunnel_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"strings"
	"testing"
	"time"

	"github.com/shellhub-io/shellhub/pkg/models"
	"github.com/shellhub-io/shellhub/tests/environment"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	agentTunnelPath        = "/agent/connection"
	redialLogSlack         = 5 * time.Second
	acceptedTunnelDeadline = 2*time.Minute + redialLogSlack
)

var redialWindows = []time.Duration{10 * time.Second, 20 * time.Second, 40 * time.Second, 80 * time.Second}

const redialWindowCap = 2 * time.Minute

// TestAgentTunnelRedial covers how an agent dials its tunnel again once the server refuses it.
// The server refuses the tunnel of a device that is not accepted when it requires accepted tunnels,
// while still authorizing the device, so the agent sees its tunnel dial fail and nothing else. The
// cases read the dials from the server's request log. Each redial waits a random delay inside a
// window that starts at 10 seconds and doubles up to 2 minutes, so each gap between refused dials
// must stay under its window plus redialLogSlack, and the tunnel must be up within the largest
// window, plus the same slack, of the accept.
func TestAgentTunnelRedial(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	compose := newConfiguredSSHEnvironment(t, ctx,
		environment.New(t, run).WithEnv("SHELLHUB_REQUIRE_ACCEPTED_TUNNEL", "true"),
		legacyNamespace)

	mark := compose.ServerLogMark(t)

	startAgent(t, ctx, compose)
	device := compose.AwaitDeviceWithStatus(t, models.DeviceStatusPending)

	t.Run("a refused tunnel is dialled again inside a doubling window", func(t *testing.T) {
		var refused []time.Time

		require.EventuallyWithT(t, func(tt *assert.CollectT) {
			refused = tunnelDials(tt, ctx, compose, mark, "403")
			assert.GreaterOrEqual(tt, len(refused), 4, "the agent stopped dialling its refused tunnel")
		}, 90*time.Second, time.Second)

		for i := 1; i < len(refused); i++ {
			window := redialWindowCap
			if i <= len(redialWindows) {
				window = redialWindows[i-1]
			}

			gap := refused[i].Sub(refused[i-1])

			assert.LessOrEqual(t, gap, window+redialLogSlack, "dial %d came %s after the one before it", i, gap)
		}
	})

	t.Run("the tunnel is up within the largest window of the device being accepted", func(t *testing.T) {
		accepting := compose.ServerLogMark(t)

		compose.UpdateDeviceStatus(t, device.UID, environment.DeviceActionAccept)

		accepted := logTime(t, compose.AwaitServerLogLine(t, accepting, "uri=/api/devices/"+device.UID+"/accept "))

		var established []time.Time

		require.EventuallyWithT(t, func(tt *assert.CollectT) {
			established = tunnelDials(tt, ctx, compose, accepting, "200")
			assert.NotEmpty(tt, established, "the agent never dialled its tunnel again once accepted")
		}, acceptedTunnelDeadline+30*time.Second, time.Second)

		assert.LessOrEqual(t, established[0].Sub(accepted), acceptedTunnelDeadline)
	})
}

func tunnelDials(tt *assert.CollectT, ctx context.Context, compose *environment.DockerCompose, mark int, status string) []time.Time {
	logs, err := compose.ReadServerLogSince(ctx, mark)
	require.NoError(tt, err)

	var dials []time.Time

	for line := range strings.Lines(logs) {
		fields := logFields(line)
		if fields["uri"] != agentTunnelPath || fields["status"] != status {
			continue
		}

		at, err := time.Parse(time.RFC3339, fields["time"])
		if assert.NoError(tt, err, "the server logged a dial without a readable time: %s", line) {
			dials = append(dials, at)
		}
	}

	return dials
}

func logTime(t *testing.T, line string) time.Time {
	t.Helper()

	at, err := time.Parse(time.RFC3339, logFields(line)["time"])
	require.NoError(t, err, "the server logged a line without a readable time: %s", line)

	return at
}