Ctrl K
ComunidadGuías…

tests/cloud_billing_routes_test.go

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

Archivo original ↗Preguntar a KiBot
package main

import (
	"fmt"
	"net/http"
	"testing"

	"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 secondTestCard = "pm_card_mastercard"

type billingCard struct {
	ID      string `json:"id"`
	Default bool   `json:"default"`
}

func billingCards(t *testing.T, compose *environment.DockerCompose) []billingCard {
	t.Helper()

	customer := struct {
		PaymentMethods []billingCard `json:"payment_methods"`
	}{}

	resp, err := compose.R(t.Context()).SetResult(&customer).Get("/api/billing/customer")
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())

	return customer.PaymentMethods
}

func defaultBillingCard(t *testing.T, cards []billingCard) string {
	t.Helper()

	var defaults []string

	for _, card := range cards {
		if card.Default {
			defaults = append(defaults, card.ID)
		}
	}

	require.Len(t, defaults, 1, "cards %+v", cards)

	return defaults[0]
}

func postBillingCard(t *testing.T, compose *environment.DockerCompose, action, card string, status int) {
	t.Helper()

	resp, err := compose.R(t.Context()).SetBody(map[string]string{"id": card}).Post("/api/billing/paymentmethod/" + action)
	require.NoError(t, err)
	require.Equal(t, status, resp.StatusCode(), "%s %s: %s", action, card, resp.String())
}

func postBillingSubscription(t *testing.T, compose *environment.DockerCompose, status int) {
	t.Helper()

	resp, err := compose.R(t.Context()).Post("/api/billing/subscription")
	require.NoError(t, err)
	require.Equal(t, status, resp.StatusCode(), resp.String())
}

func testBillingSubscription(t *testing.T, compose *environment.DockerCompose) {
	t.Helper()

	t.Run("subscribing again while the subscription is active is refused", func(t *testing.T) {
		owner := newBillingOwner(t, compose, "billing-sub-twice")
		subscription := subscribe(t, compose, owner.tenant)

		postBillingSubscription(t, compose, http.StatusConflict)

		billing := billedNamespace(t, compose, owner.tenant).Billing
		require.NotNil(t, billing.Subscription)
		assert.Equal(t, subscription.id, billing.Subscription.ID)
		assert.Equal(t, models.BillingStatusActive, billing.Subscription.Status)
		assert.Equal(t, 1, activeStripeSubscriptions(t, compose, subscription.customer))
	})

	t.Run("a canceled namespace whose subscription has an open invoice subscribes again only once the invoice is voided", func(t *testing.T) {
		owner := newBillingOwner(t, compose, "billing-resub-due")
		subscription := subscribe(t, compose, owner.tenant)
		invoice := openStripeInvoice(t, compose, subscription)

		canceledAt := cancelStripeSubscription(t, compose, subscription.id)
		requireStripeEventAnswered(t, compose, stripeSubscriptionDeleted, subscription.canceled(canceledAt))

		postBillingSubscription(t, compose, http.StatusPaymentRequired)

		billing := billedNamespace(t, compose, owner.tenant).Billing
		require.NotNil(t, billing.Subscription)
		assert.Equal(t, subscription.id, billing.Subscription.ID)
		assert.Equal(t, models.BillingStatusCanceled, billing.Subscription.Status)
		assert.Zero(t, activeStripeSubscriptions(t, compose, subscription.customer))

		voidStripeInvoice(t, compose, invoice)

		postBillingSubscription(t, compose, http.StatusCreated)

		billing = billedNamespace(t, compose, owner.tenant).Billing
		require.NotNil(t, billing.Subscription)
		assert.NotEqual(t, subscription.id, billing.Subscription.ID)
		assert.Equal(t, models.BillingStatusActive, billing.Subscription.Status)
		assert.Equal(t, 1, activeStripeSubscriptions(t, compose, subscription.customer))
	})
}

func testBillingPaymentMethods(t *testing.T, compose *environment.DockerCompose) {
	t.Helper()

	t.Run("the default card is detached only once another card becomes the default", func(t *testing.T) {
		owner := newBillingOwner(t, compose, "billing-cards")
		becomeCustomer(t, compose, owner.tenant)

		attachCard(t, compose, acceptedTestCard)
		first := defaultBillingCard(t, billingCards(t, compose))

		attachCard(t, compose, secondTestCard)
		cards := billingCards(t, compose)
		require.Len(t, cards, 2)

		second := defaultBillingCard(t, cards)
		require.NotEqual(t, first, second)

		postBillingCard(t, compose, "detach", second, http.StatusFailedDependency)
		assert.ElementsMatch(t, cards, billingCards(t, compose))

		postBillingCard(t, compose, "default", first, http.StatusOK)
		assert.Equal(t, first, defaultBillingCard(t, billingCards(t, compose)))

		postBillingCard(t, compose, "detach", second, http.StatusOK)
		assert.Equal(t, []billingCard{{ID: first, Default: true}}, billingCards(t, compose))
	})

	t.Run("an active subscription refuses detaching a card and changing the default", func(t *testing.T) {
		owner := newBillingOwner(t, compose, "billing-cards-active")
		subscribe(t, compose, owner.tenant)

		cards := billingCards(t, compose)
		card := defaultBillingCard(t, cards)

		postBillingCard(t, compose, "default", card, http.StatusBadRequest)
		postBillingCard(t, compose, "detach", card, http.StatusBadRequest)
		assert.Equal(t, cards, billingCards(t, compose))
	})
}

func testBillingDeviceChooser(t *testing.T, compose *environment.DockerCompose) {
	t.Helper()

	t.Run("a canceled namespace keeps the devices chosen from the most used and accepts again up to the free tier", func(t *testing.T) {
		owner := newBillingOwner(t, compose, "billing-chooser")
		subscription := subscribe(t, compose, owner.tenant)

		sessions := []int{1, 4, 0, 3, 2}
		devices := make([]models.Device, len(sessions))

		for i, count := range sessions {
			devices[i] = enrollIn(t, compose, owner.key, fmt.Sprintf("chooser-%d", i), fmt.Sprintf("02:00:00:00:45:%02x", i))
			compose.UpdateDeviceStatus(t, devices[i].UID, environment.DeviceActionAccept)
			compose.RecordClosedSessions(t, owner.tenant, devices[i].UID, count)
		}

		canceledAt := cancelStripeSubscription(t, compose, subscription.id)
		requireStripeEventAnswered(t, compose, stripeSubscriptionDeleted, subscription.canceled(canceledAt))

		mostUsed := []models.Device{}

		resp, err := compose.R(t.Context()).SetResult(&mostUsed).Get("/api/billing/devices-most-used")
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())
		require.Equal(t, []string{devices[1].UID, devices[3].UID, devices[4].UID}, deviceUIDs(mostUsed))

		kept := []string{devices[1].UID, devices[3].UID}

		resp, err = compose.R(t.Context()).SetBody(map[string][]string{"choices": kept}).Post("/api/billing/device-choice")
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode(), resp.String())

		assert.ElementsMatch(t, kept, deviceUIDs(compose.ListDevices(t, models.DeviceStatusAccepted)))
		assert.ElementsMatch(t, []string{devices[0].UID, devices[2].UID, devices[4].UID},
			deviceUIDs(compose.ListDevices(t, models.DeviceStatusPending)))

		requireAcceptAnswers(t, compose, devices[0], http.StatusOK)
		requireAcceptRefused(t, compose, devices[2], deviceLimitMessage)
	})
}

func testBillingPermissions(t *testing.T, compose *environment.DockerCompose) {
	t.Helper()

	owner := newBillingOwner(t, compose, "billing-permissions")
	becomeCustomer(t, compose, owner.tenant)
	attachCard(t, compose, acceptedTestCard)

	roles := []authorizer.Role{authorizer.RoleObserver, authorizer.RoleOperator, authorizer.RoleAdministrator}

	routes := []struct {
		method string
		path   string
	}{
		{http.MethodPost, "/api/billing/customer"},
		{http.MethodGet, "/api/billing/customer"},
		{http.MethodPost, "/api/billing/subscription"},
		{http.MethodGet, "/api/billing/subscription"},
		{http.MethodPost, "/api/billing/portal"},
		{http.MethodPost, "/api/billing/paymentmethod/attach"},
		{http.MethodPost, "/api/billing/paymentmethod/detach"},
		{http.MethodPost, "/api/billing/paymentmethod/default"},
		{http.MethodGet, "/api/billing/devices-most-used"},
		{http.MethodPost, "/api/billing/device-choice"},
	}

	for _, role := range roles {
		t.Run(fmt.Sprintf("an %s is refused managing the namespace's billing", role), func(t *testing.T) {
			username := "billing-" + string(role)

			compose.NewUser(t, username, username+stripeTestEmailDomain, ShellHubPassword)
			compose.NewMember(t, username, owner.username, string(role))

			member := compose.AuthUser(t, username, ShellHubPassword)
			require.NotNil(t, member.Tenant)
			require.Equal(t, owner.tenant, *member.Tenant)

			for _, route := range routes {
				resp, err := compose.R(t.Context()).
					SetAuthToken(member.Token).
					SetBody(map[string]any{}).
					Execute(route.method, route.path)
				require.NoError(t, err)
				assert.Equal(t, http.StatusForbidden, resp.StatusCode(), "%s %s: %s", route.method, route.path, resp.String())
			}
		})
	}

	for _, role := range roles {
		t.Run(fmt.Sprintf("an API key with the %s role is refused reading the namespace's billing customer", role), func(t *testing.T) {
			key := compose.CreateAPIKey(t, &requests.CreateAPIKey{Name: "bill-" + string(role)[:3], ExpiresAt: -1, OptRole: role})
			require.Equal(t, role, key.Role)

			resp, err := withAPIKey(t, compose, key.Key).Get("/api/billing/customer")
			require.NoError(t, err)
			assert.Equal(t, http.StatusForbidden, resp.StatusCode(), resp.String())
		})
	}
}