Ctrl K
ComunidadGuías…

tests/session_close_test.go

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

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"net"
	"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"
	"golang.org/x/crypto/ssh"
)

func dialClient(t *testing.T, ctx context.Context, addr string, config *ssh.ClientConfig) *ssh.Client {
	t.Helper()

	return dialClientWithin(t, ctx, addr, config, 30*time.Second)
}

func dialClientWithin(t *testing.T, ctx context.Context, addr string, config *ssh.ClientConfig, timeout time.Duration) *ssh.Client {
	t.Helper()

	var client *ssh.Client

	require.EventuallyWithT(t, func(tt *assert.CollectT) {
		var err error

		client, err = dialClientOnce(ctx, addr, config)
		assert.NoError(tt, err)
	}, timeout, 1*time.Second)

	return client
}

func dialClientOnce(ctx context.Context, addr string, config *ssh.ClientConfig) (*ssh.Client, error) {
	dialer := net.Dialer{} //nolint:exhaustruct // the zero dialer is what ssh.Dial uses

	conn, err := dialer.DialContext(ctx, "tcp", addr)
	if err != nil {
		return nil, err
	}

	sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
	if err != nil {
		_ = conn.Close()

		return nil, err
	}

	return ssh.NewClient(sshConn, chans, reqs), nil
}

func dialDevice(t *testing.T, ctx context.Context, compose *environment.DockerCompose, device *models.Device, signer ssh.Signer) *ssh.Client {
	t.Helper()

	return dialDeviceWithin(t, ctx, compose, device, signer, 30*time.Second)
}

func dialDeviceWithin(t *testing.T, ctx context.Context, compose *environment.DockerCompose, device *models.Device, signer ssh.Signer, timeout time.Duration) *ssh.Client {
	t.Helper()

	return dialClientWithin(t, ctx, compose.SSHAddress(), &ssh.ClientConfig{ //nolint:exhaustruct // the remaining fields keep their defaults
		User: deviceSSHID(device),
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(signer),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec // the test stack's host key is ephemeral
	}, timeout)
}

func openShellSession(t *testing.T, ctx context.Context, compose *environment.DockerCompose, device *models.Device) (*ssh.Client, *models.Session) {
	t.Helper()

	conn := dialDevice(t, ctx, compose, device, registerDeviceKey(t, ctx, compose))

	sess, err := conn.NewSession()
	require.NoError(t, err)

	require.NoError(t, sess.RequestPty("xterm", 100, 100, ssh.TerminalModes{ssh.ECHO: 1}))
	require.NoError(t, sess.Shell())

	session := &models.Session{} //nolint:exhaustruct // filled from the API response

	require.EventuallyWithT(t, func(tt *assert.CollectT) {
		sessions := []models.Session{}

		resp, err := compose.R(ctx).SetResult(&sessions).Get("/api/sessions")
		assert.NoError(tt, err)
		assert.Equal(tt, 200, resp.StatusCode())

		for _, s := range sessions {
			if s.Active {
				*session = s

				return
			}
		}

		assert.Fail(tt, "no active session yet")
	}, 30*time.Second, 1*time.Second)

	return conn, session
}

func requireSessionActive(t *testing.T, ctx context.Context, compose *environment.DockerCompose, uid string, active bool) {
	t.Helper()

	awaitSession(t, ctx, compose, uid, 30*time.Second, func(tt *assert.CollectT, session models.Session) {
		assert.Equal(tt, active, session.Active)
	})
}

func TestSessionCloseDelegatesToTheGatewayThatOwnsIt(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	compose := newSSHEnvironment(t, ctx, legacyNamespace)
	_, device := startAcceptedAgent(t, ctx, compose)

	conn, session := openShellSession(t, ctx, compose, device)
	defer conn.Close() //nolint:errcheck // close fails once the gateway has closed the connection, which is the expected outcome

	resp, err := compose.R(ctx).
		SetBody(map[string]string{"device": device.UID}).
		Post("/api/sessions/" + session.UID + "/close")
	require.NoError(t, err)
	assert.Equal(t, 202, resp.StatusCode(),
		"a session the gateway still owns is delivered to the device, not closed by the endpoint")

	requireSessionActive(t, ctx, compose, session.UID, false)
}

func TestSessionCloseRetiresASessionNoGatewayOwns(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	compose := newSSHEnvironment(t, ctx, legacyNamespace)
	_, device := startAcceptedAgent(t, ctx, compose)

	conn, session := openShellSession(t, ctx, compose, device)
	defer conn.Close() //nolint:errcheck // the connection is already dead once the server restarts

	server := compose.Service(environment.ServiceServer)
	require.NoError(t, server.Stop(ctx, nil))
	require.NoError(t, server.Start(ctx))

	requireSessionActive(t, ctx, compose, session.UID, true)

	var status int

	require.EventuallyWithT(t, func(tt *assert.CollectT) {
		resp, err := compose.R(ctx).
			SetBody(map[string]string{"device": device.UID}).
			Post("/api/sessions/" + session.UID + "/close")
		assert.NoError(tt, err)
		assert.NotEqual(tt, 0, resp.StatusCode())

		status = resp.StatusCode()
	}, 60*time.Second, 2*time.Second)

	assert.Equal(t, 200, status,
		"no gateway owns this session, so the endpoint closes it instead of reporting delivery")

	requireSessionActive(t, ctx, compose, session.UID, false)
}