Ctrl K
ComunidadGuías…

tests/invitation_link_test.go

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

Archivo original ↗Preguntar a KiBot
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
}