tests/namespace_test.go
Rama master · réplica de lectura del repositorio original.
package main
import (
"context"
"net/http"
"testing"
"time"
"github.com/shellhub-io/shellhub/pkg/api/authorizer"
"github.com/shellhub-io/shellhub/pkg/api/requests"
"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 (
leaverUsername = "leaver"
leaverEmail = "leaver@ossystems.com.br"
leaverPassword = "password"
leftBehindNamespaceName = "otherspace"
leftBehindNamespace = "00000000-0000-4000-0000-000000000001"
neverLeftNamespaceName = "neverleftspace"
neverLeftNamespace = "00000000-0000-4000-0000-000000000002"
)
// TestLeaveNamespace covers both shapes the route answers. Leaving the namespace the caller's
// token is scoped to mints a replacement token, because the one they hold names a namespace they
// no longer belong to. Leaving any other namespace leaves that token valid, so there is nothing
// to send back and the route says so with its own status rather than an empty 200.
func TestLeaveNamespace(t *testing.T) {
t.Parallel()
ctx := context.Background()
compose := environment.New(t, run).Up(ctx)
t.Cleanup(compose.Down)
compose.NewUser(t, ShellHubUsername, ShellHubEmail, ShellHubPassword)
compose.NewNamespace(t, ShellHubUsername, ShellHubNamespaceName, ShellHubNamespace)
compose.NewNamespace(t, ShellHubUsername, leftBehindNamespaceName, leftBehindNamespace)
compose.NewNamespace(t, ShellHubUsername, neverLeftNamespaceName, neverLeftNamespace)
compose.NewUser(t, leaverUsername, leaverEmail, leaverPassword)
compose.NewMember(t, leaverUsername, ShellHubNamespaceName, string(authorizer.RoleObserver))
compose.NewMember(t, leaverUsername, leftBehindNamespaceName, string(authorizer.RoleObserver))
compose.NewMember(t, leaverUsername, neverLeftNamespaceName, string(authorizer.RoleObserver))
signedInto := signInto(t, compose, leaverUsername, leaverPassword)
require.NotNil(t, signedInto.Tenant)
require.Equal(t, ShellHubNamespace, *signedInto.Tenant)
t.Run("leaving another namespace answers no content", func(t *testing.T) {
resp, err := compose.Anonymous(ctx).
SetAuthToken(signedInto.Token).
Delete("/api/namespaces/" + leftBehindNamespace + "/members")
require.NoError(t, err)
require.Equal(t, http.StatusNoContent, resp.StatusCode())
require.Empty(t, resp.Body())
})
t.Run("leaving the namespace the token names answers a replacement token", func(t *testing.T) {
replacement := models.UserAuthResponse{}
resp, err := compose.Anonymous(ctx).
SetAuthToken(signedInto.Token).
SetResult(&replacement).
Delete("/api/namespaces/" + ShellHubNamespace + "/members")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())
require.NotEmpty(t, replacement.Token)
require.NotEqual(t, signedInto.Token, replacement.Token)
require.NotNil(t, replacement.Tenant)
require.Equal(t, neverLeftNamespace, *replacement.Tenant)
})
}
// TestDeleteNamespaceClosesItsTunnels deletes a namespace whose device is online and reads the
// server's count of tunnels closed because their device was removed. The API deletes in the
// process that holds the tunnel, which counts the eviction before the delete answers. The admin CLI
// runs apart from that process, so its deletion reaches the tunnel on the device's next heartbeat.
func TestDeleteNamespaceClosesItsTunnels(t *testing.T) {
t.Parallel()
const evictedTunnels = "shellhub_ssh_dialer_connections_evicted_total"
cases := []struct {
description string
deleteNamespace func(t *testing.T, compose *environment.DockerCompose)
onHeartbeat bool
}{
{
description: "through the API",
deleteNamespace: func(t *testing.T, compose *environment.DockerCompose) {
t.Helper()
resp, err := compose.R(t.Context()).Delete("/api/namespaces/" + ShellHubNamespace)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
},
},
{
description: "through the admin CLI",
deleteNamespace: func(t *testing.T, compose *environment.DockerCompose) {
t.Helper()
compose.DeleteNamespace(t, ShellHubNamespaceName)
},
onHeartbeat: true,
},
}
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
compose := newFastTunnelPingEnvironment(t, t.Context(), legacyNamespace)
startAcceptedAgent(t, t.Context(), compose)
evicted, err := compose.ServerMetric(t, evictedTunnels)
require.NoError(t, err)
require.Zero(t, evicted)
tc.deleteNamespace(t, compose)
if tc.onHeartbeat {
require.EventuallyWithT(t, func(tt *assert.CollectT) {
evicted, err := compose.ServerMetric(t, evictedTunnels)
if !assert.NoError(tt, err) {
return
}
assert.InDelta(tt, 1, evicted, 0)
}, tunnelBeatTimeout, time.Second)
} else {
evicted, err := compose.ServerMetric(t, evictedTunnels)
require.NoError(t, err)
assert.InDelta(t, 1, evicted, 0)
}
})
}
}
// TestDeleteNamespaceRefusesItsAPIKeys uses a key once, so the server caches it, then deletes the
// key's namespace. Deleting the namespace evicts nothing from that cache.
func TestDeleteNamespaceRefusesItsAPIKeys(t *testing.T) {
t.Parallel()
compose := environment.New(t, run).Up(t.Context())
t.Cleanup(compose.Down)
compose.NewUser(t, ShellHubUsername, ShellHubEmail, ShellHubPassword)
compose.NewNamespace(t, ShellHubUsername, ShellHubNamespaceName, ShellHubNamespace)
compose.JWT(compose.AuthUser(t, ShellHubUsername, ShellHubPassword).Token)
key := compose.CreateAPIKey(t, &requests.CreateAPIKey{
Name: "automation",
ExpiresAt: -1,
OptRole: authorizer.RoleAdministrator,
})
resp, err := compose.Anonymous(t.Context()).SetHeader("X-API-Key", key.Key).Get("/api/devices")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
resp, err = compose.R(t.Context()).Delete("/api/namespaces/" + ShellHubNamespace)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
resp, err = compose.Anonymous(t.Context()).SetHeader("X-API-Key", key.Key).Get("/api/devices")
require.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode(), resp.String())
}