tests/invitation_link_test.go
Rama master · réplica de lectura del repositorio original.
package main
import (
"encoding/json"
"net/http"
"net/url"
"testing"
"time"
"github.com/go-resty/resty/v2"
"github.com/shellhub-io/shellhub/pkg/models"
"github.com/shellhub-io/shellhub/pkg/uuid"
"github.com/shellhub-io/shellhub/tests/environment"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const invitationLifetime = 7 * 24 * time.Hour
// TestInvitationLink covers the link an invitation to a namespace travels by: it resolves for seven
// days, is refused once it expires, and generating it again then issues a fresh link while the old
// one stays refused; a link still valid is not generated again; and accepting the invitation takes
// the code the link carries. Each case invites its own namespace, so the namespace holds one
// invitation.
func TestInvitationLink(t *testing.T) {
t.Parallel()
compose := newSSHEnvironment(t, t.Context(), legacyNamespace)
t.Run("a link resolves for seven days", func(t *testing.T) {
namespace := newInvitingNamespace(t, compose, "invitation-lifetime")
link := generateInvitationLink(t, compose, namespace, "lifetime@ossystems.com.br")
require.Equal(t, http.StatusOK, resolveInvitation(t, compose, link).StatusCode())
invitations := []models.MembershipInvitation{}
resp, err := asBearer(t, compose, namespace.token).SetResult(&invitations).Get("/api/namespaces/" + namespace.tenant + "/invitations")
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
require.Len(t, invitations, 1)
require.NotNil(t, invitations[0].ExpiresAt)
assert.WithinDuration(t, wallClock.Now().Add(invitationLifetime), *invitations[0].ExpiresAt, 5*time.Minute)
})
t.Run("an expired link is refused, and generating it again issues a fresh one", func(t *testing.T) {
namespace := newInvitingNamespace(t, compose, "invitation-expired")
invitee := "expired@ossystems.com.br"
expired := generateInvitationLink(t, compose, namespace, invitee)
require.Equal(t, http.StatusOK, resolveInvitation(t, compose, expired).StatusCode())
compose.ExpireMembershipInvitation(t, namespace.tenant)
assert.Equal(t, http.StatusForbidden, resolveInvitation(t, compose, expired).StatusCode())
fresh := generateInvitationLink(t, compose, namespace, invitee)
assert.NotEqual(t, expired, fresh)
assert.Equal(t, http.StatusOK, resolveInvitation(t, compose, fresh).StatusCode())
assert.Equal(t, http.StatusForbidden, resolveInvitation(t, compose, expired).StatusCode())
})
t.Run("a link still valid is not generated again", func(t *testing.T) {
namespace := newInvitingNamespace(t, compose, "invitation-valid")
invitee := "valid@ossystems.com.br"
link := generateInvitationLink(t, compose, namespace, invitee)
resp, err := postInvitationLink(t, compose, namespace, invitee)
require.NoError(t, err)
assert.Equal(t, http.StatusConflict, resp.StatusCode(), resp.String())
assert.Equal(t, http.StatusOK, resolveInvitation(t, compose, link).StatusCode())
})
t.Run("accepting an invitation takes the code its link carries", func(t *testing.T) {
namespace := newInvitingNamespace(t, compose, "invitation-code")
username := "accepting"
email := username + "@ossystems.com.br"
compose.NewUser(t, username, email, ShellHubPassword)
link := generateInvitationLink(t, compose, namespace, email)
token := compose.AuthUser(t, username, ShellHubPassword).Token
accept := func(body map[string]string) *resty.Response {
resp, err := asBearer(t, compose, token).
SetBody(body).
Patch("/api/namespaces/" + namespace.tenant + "/invitations/accept")
require.NoError(t, err)
return resp
}
resp := accept(map[string]string{})
assert.Equal(t, http.StatusBadRequest, resp.StatusCode(), resp.String())
resp = accept(map[string]string{"code": "WXYZ2K7QABCD"})
assert.Equal(t, http.StatusNotFound, resp.StatusCode(), resp.String())
resp = accept(map[string]string{"code": inviteCode(t, link)})
assert.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
})
}
type invitingNamespace struct {
tenant string
token string
}
func newInvitingNamespace(t *testing.T, compose *environment.DockerCompose, name string) invitingNamespace {
t.Helper()
tenant := uuid.Generate()
compose.NewNamespace(t, ShellHubUsername, name, tenant)
return invitingNamespace{tenant: tenant, token: signIntoTenant(t, compose, ShellHubUsername, ShellHubPassword, tenant).Token}
}
func postInvitationLink(t *testing.T, compose *environment.DockerCompose, namespace invitingNamespace, email string) (*resty.Response, error) {
t.Helper()
return asBearer(t, compose, namespace.token).
SetBody(map[string]string{"email": email, "role": "observer"}).
Post("/api/namespaces/" + namespace.tenant + "/invitations/links")
}
func generateInvitationLink(t *testing.T, compose *environment.DockerCompose, namespace invitingNamespace, email string) string {
t.Helper()
resp, err := postInvitationLink(t, compose, namespace, email)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
generated := struct {
Link string `json:"link"`
}{}
require.NoError(t, json.Unmarshal(resp.Body(), &generated))
require.NotEmpty(t, generated.Link)
return generated.Link
}
func inviteCode(t *testing.T, link string) string {
t.Helper()
parsed, err := url.Parse(link)
require.NoError(t, err)
code := parsed.Query().Get("invite")
require.NotEmpty(t, code, "the link %s carries no invite code", link)
return code
}
func resolveInvitation(t *testing.T, compose *environment.DockerCompose, link string) *resty.Response {
t.Helper()
resp, err := compose.Anonymous(t.Context()).SetQueryParam("invite", inviteCode(t, link)).Get("/api/invitations/resolve")
require.NoError(t, err)
return resp
}