Skip to content

Commit

Permalink
feat: provision a new tenant through CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
CaioTeixeira95 committed Nov 1, 2023
1 parent dd235f0 commit 7b13b6c
Show file tree
Hide file tree
Showing 10 changed files with 686 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ $$ language 'plpgsql';

CREATE TYPE public.email_sender_type AS ENUM ('AWS_EMAIL', 'DRY_RUN');
CREATE TYPE public.sms_sender_type AS ENUM ('TWILIO_SMS', 'AWS_SMS', 'DRY_RUN');
CREATE TYPE public.tenant_status AS ENUM ('TENANT_CREATED', 'TENANT_PROVISIONED', 'TENANT_ACTIVATED', 'TENANT_DEACTIVATED');

CREATE TABLE public.tenants
(
Expand All @@ -29,6 +30,7 @@ CREATE TABLE public.tenants
cors_allowed_origins text[] NULL,
base_url text NULL,
sdp_ui_base_url text NULL,
status tenant_status DEFAULT 'TENANT_CREATED',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
Expand All @@ -47,4 +49,6 @@ DROP TYPE public.email_sender_type;

DROP TYPE public.sms_sender_type;

DROP TYPE public.tenant_status;

DROP FUNCTION update_at_refresh;
12 changes: 12 additions & 0 deletions internal/utils/network_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package utils

import (
"fmt"
"strings"

"github.com/stellar/go/network"
)
Expand All @@ -23,3 +24,14 @@ func GetNetworkTypeFromNetworkPassphrase(networkPassphrase string) (NetworkType,
return "", fmt.Errorf("invalid network passphrase provided")
}
}

func GetNetworkTypeFromString(networkType string) (NetworkType, error) {
switch NetworkType(strings.ToLower(networkType)) {
case PubnetNetworkType:
return PubnetNetworkType, nil
case TestnetNetworkType:
return TestnetNetworkType, nil
default:
return "", fmt.Errorf("invalid network type provided")
}
}
42 changes: 35 additions & 7 deletions stellar-multitenant/pkg/cli/add_tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,62 @@ package cli
import (
"context"
"fmt"
"go/types"
"regexp"

"github.com/spf13/cobra"
"github.com/stellar/go/support/config"
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/db"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/cli/utils"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-multitenant/pkg/tenant"
)

var validTenantName *regexp.Regexp = regexp.MustCompile(`^[a-z-]+$`)

func AddTenantsCmd() *cobra.Command {
var networkType string
configOptions := config.ConfigOptions{
{
Name: "network-type",
Usage: "",
OptType: types.String,
CustomSetValue: utils.SetConfigOptionNetworkType,
ConfigKey: &networkType,
FlagDefault: "testnet",
Required: false,
},
}

cmd := cobra.Command{
Use: "add-tenants",
Short: "Add a new tenant.",
Example: "add-tenants [name]",
Example: "add-tenants [tenant name] [user first name] [user last name] [user email]",
Long: "Add a new tenant. The tenant name should only contain lower case characters and dash (-)",
Args: cobra.MatchAll(
cobra.ExactArgs(1),
cobra.ExactArgs(4),
validateTenantNameArg,
),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
cmd.Parent().PersistentPreRun(cmd.Parent(), args)
configOptions.Require()
err := configOptions.SetValues()
if err != nil {
log.Fatalf("Error setting values of config options: %s", err.Error())
}
},
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
if err := executeAddTenant(ctx, globalOptions.multitenantDbURL, args[0]); err != nil {
if err := executeAddTenant(ctx, globalOptions.multitenantDbURL, args[0], args[1], args[2], args[3], networkType); err != nil {
log.Fatal(err)
}
},
}

if err := configOptions.Init(&cmd); err != nil {
log.Fatalf("initializing config options: %v", err)
}

return &cmd
}

Expand All @@ -41,20 +69,20 @@ func validateTenantNameArg(cmd *cobra.Command, args []string) error {
return nil
}

func executeAddTenant(ctx context.Context, dbURL, name string) error {
func executeAddTenant(ctx context.Context, dbURL, tenantName, userFirstName, userLastName, userEmail, networkType string) error {
dbConnectionPool, err := db.OpenDBConnectionPool(dbURL)
if err != nil {
return fmt.Errorf("opening database connection pool: %w", err)
}
defer dbConnectionPool.Close()

m := tenant.NewManager(tenant.WithDatabase(dbConnectionPool))
t, err := m.AddTenant(ctx, name)
t, err := m.ProvisionNewTenant(ctx, tenantName, userFirstName, userLastName, userEmail, networkType)
if err != nil {
return fmt.Errorf("adding tenant with name %s: %w", name, err)
return fmt.Errorf("adding tenant with name %s: %w", tenantName, err)
}

log.Ctx(ctx).Infof("tenant %s added successfully", name)
log.Ctx(ctx).Infof("tenant %s added successfully", tenantName)
log.Ctx(ctx).Infof("tenant ID: %s", t.ID)

return nil
Expand Down
169 changes: 147 additions & 22 deletions stellar-multitenant/pkg/cli/add_tenants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"testing"

"github.com/lib/pq"
"github.com/spf13/cobra"
"github.com/stellar/go/support/log"
"github.com/stellar/stellar-disbursement-platform-backend/db"
Expand All @@ -17,9 +19,20 @@ import (
)

func DeleteAllTenantsFixture(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool) {
const q = "DELETE FROM tenants"
q := "DELETE FROM tenants"
_, err := dbConnectionPool.ExecContext(ctx, q)
require.NoError(t, err)

var schemasToDrop []string
q = "SELECT schema_name FROM information_schema.schemata WHERE schema_name ILIKE 'sdp_%'"
err = dbConnectionPool.SelectContext(ctx, &schemasToDrop, q)
require.NoError(t, err)

for _, schema := range schemasToDrop {
q = fmt.Sprintf("DROP SCHEMA %s CASCADE", pq.QuoteIdentifier(schema))
_, err = dbConnectionPool.ExecContext(ctx, q)
require.NoError(t, err)
}
}

func Test_validateTenantNameArg(t *testing.T) {
Expand Down Expand Up @@ -81,7 +94,7 @@ func Test_executeAddTenant(t *testing.T) {

getEntries := log.DefaultLogger.StartTest(log.InfoLevel)

err := executeAddTenant(ctx, dbt.DSN, "myorg")
err := executeAddTenant(ctx, dbt.DSN, "myorg", "first", "last", "[email protected]", "testnet")
assert.Nil(t, err)

const q = "SELECT id FROM tenants WHERE name = $1"
Expand All @@ -90,20 +103,20 @@ func Test_executeAddTenant(t *testing.T) {
require.NoError(t, err)

entries := getEntries()
require.Len(t, entries, 2)
assert.Equal(t, "tenant myorg added successfully", entries[0].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[1].Message)
require.Len(t, entries, 15)
assert.Equal(t, "tenant myorg added successfully", entries[13].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[14].Message)
})

t.Run("duplicated tenant name", func(t *testing.T) {
DeleteAllTenantsFixture(t, ctx, dbConnectionPool)

getEntries := log.DefaultLogger.StartTest(log.DebugLevel)

err := executeAddTenant(ctx, dbt.DSN, "myorg")
err := executeAddTenant(ctx, dbt.DSN, "myorg", "first", "last", "[email protected]", "testnet")
assert.Nil(t, err)

err = executeAddTenant(ctx, dbt.DSN, "MyOrg")
err = executeAddTenant(ctx, dbt.DSN, "myorg", "first", "last", "[email protected]", "testnet")
assert.ErrorIs(t, err, tenant.ErrDuplicatedTenantName)

const q = "SELECT id FROM tenants WHERE name = $1"
Expand All @@ -112,9 +125,9 @@ func Test_executeAddTenant(t *testing.T) {
require.NoError(t, err)

entries := getEntries()
require.Len(t, entries, 2)
assert.Equal(t, "tenant myorg added successfully", entries[0].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[1].Message)
require.Len(t, entries, 16)
assert.Equal(t, "tenant myorg added successfully", entries[13].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[14].Message)
})
}

Expand All @@ -135,17 +148,18 @@ func Test_AddTenantsCmd(t *testing.T) {
mockCmd.SetErr(out)
mockCmd.SetArgs([]string{"add-tenants"})
err := mockCmd.ExecuteContext(ctx)
assert.EqualError(t, err, "accepts 1 arg(s), received 0")
assert.EqualError(t, err, "accepts 4 arg(s), received 0")

expectUsageMessage := `Error: accepts 1 arg(s), received 0
expectUsageMessage := `Error: accepts 4 arg(s), received 0
Usage:
add-tenants [flags]
Examples:
add-tenants [name]
add-tenants [tenant name] [user first name] [user last name] [user email]
Flags:
-h, --help help for add-tenants
-h, --help help for add-tenants
--network-type string (NETWORK_TYPE) (default "testnet")
`
assert.Equal(t, expectUsageMessage, out.String())
Expand All @@ -161,21 +175,27 @@ Usage:
add-tenants [flags]
Examples:
add-tenants [name]
add-tenants [tenant name] [user first name] [user last name] [user email]
Flags:
-h, --help help for add-tenants
-h, --help help for add-tenants
--network-type string (NETWORK_TYPE) (default "testnet")
`
assert.Equal(t, expectUsageMessage, out.String())
})

t.Run("adds new tenant successfully", func(t *testing.T) {
t.Run("adds new tenant successfully testnet", func(t *testing.T) {
tenantName := "unhcr"
userFirstName := "First"
userLastName := "Last"
userEmail := "[email protected]"

out := new(strings.Builder)
rootCmd := rootCmd()
rootCmd.AddCommand(AddTenantsCmd())
rootCmd.SetOut(out)
rootCmd.SetErr(out)
rootCmd.SetArgs([]string{"add-tenants", "unhcr", "--multitenant-db-url", dbt.DSN})
rootCmd.SetArgs([]string{"add-tenants", tenantName, userFirstName, userLastName, userEmail, "--network-type", "testnet", "--multitenant-db-url", dbt.DSN})
getEntries := log.DefaultLogger.StartTest(log.InfoLevel)

err := rootCmd.ExecuteContext(ctx)
Expand All @@ -184,12 +204,117 @@ Flags:

const q = "SELECT id FROM tenants WHERE name = $1"
var tenantID string
err = dbConnectionPool.GetContext(ctx, &tenantID, q, "unhcr")
err = dbConnectionPool.GetContext(ctx, &tenantID, q, tenantName)
require.NoError(t, err)

entries := getEntries()
require.Len(t, entries, 4)
assert.Equal(t, "tenant unhcr added successfully", entries[2].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[3].Message)
require.Len(t, entries, 17)
assert.Equal(t, "tenant unhcr added successfully", entries[15].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[16].Message)

// Connecting to the new schema
schemaName := fmt.Sprintf("sdp_%s", tenantName)
dataSourceName := dbConnectionPool.DSN()
u, err := url.Parse(dataSourceName)
require.NoError(t, err)
uq := u.Query()
uq.Set("search_path", schemaName)
u.RawQuery = uq.Encode()

tenantSchemaConnectionPool, err := db.OpenDBConnectionPool(u.String())
require.NoError(t, err)
defer tenantSchemaConnectionPool.Close()

expectedTablesAfterMigrationsApplied := []string{
"assets",
"auth_migrations",
"auth_user_mfa_codes",
"auth_user_password_reset",
"auth_users",
"channel_accounts",
"countries",
"disbursements",
"gorp_migrations",
"messages",
"organizations",
"payments",
"receiver_verifications",
"receiver_wallets",
"receivers",
"submitter_transactions",
"wallets",
"wallets_assets",
}
tenant.TenantSchemaHasTablesFixture(t, ctx, tenantSchemaConnectionPool, schemaName, expectedTablesAfterMigrationsApplied)
tenant.AssertRegisteredAssets(t, ctx, tenantSchemaConnectionPool, []string{"USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", "XLM:"})
tenant.AssertRegisteredWallets(t, ctx, tenantSchemaConnectionPool, []string{"Demo Wallet", "Vibrant Assist"})
tenant.AssertRegisteredUser(t, ctx, tenantSchemaConnectionPool, userFirstName, userLastName, userEmail)
})

t.Run("adds new tenant successfully pubnet", func(t *testing.T) {
tenantName := "irc"
userFirstName := "First"
userLastName := "Last"
userEmail := "[email protected]"

out := new(strings.Builder)
rootCmd := rootCmd()
rootCmd.AddCommand(AddTenantsCmd())
rootCmd.SetOut(out)
rootCmd.SetErr(out)
rootCmd.SetArgs([]string{"add-tenants", tenantName, userFirstName, userLastName, userEmail, "--network-type", "pubnet", "--multitenant-db-url", dbt.DSN})
getEntries := log.DefaultLogger.StartTest(log.InfoLevel)

err := rootCmd.ExecuteContext(ctx)
require.NoError(t, err)
assert.Empty(t, out.String())

const q = "SELECT id FROM tenants WHERE name = $1"
var tenantID string
err = dbConnectionPool.GetContext(ctx, &tenantID, q, tenantName)
require.NoError(t, err)

entries := getEntries()
require.Len(t, entries, 17)
assert.Equal(t, "tenant irc added successfully", entries[15].Message)
assert.Contains(t, fmt.Sprintf("tenant ID: %s", tenantID), entries[16].Message)

// Connecting to the new schema
schemaName := fmt.Sprintf("sdp_%s", tenantName)
dataSourceName := dbConnectionPool.DSN()
u, err := url.Parse(dataSourceName)
require.NoError(t, err)
uq := u.Query()
uq.Set("search_path", schemaName)
u.RawQuery = uq.Encode()

tenantSchemaConnectionPool, err := db.OpenDBConnectionPool(u.String())
require.NoError(t, err)
defer tenantSchemaConnectionPool.Close()

expectedTablesAfterMigrationsApplied := []string{
"assets",
"auth_migrations",
"auth_user_mfa_codes",
"auth_user_password_reset",
"auth_users",
"channel_accounts",
"countries",
"disbursements",
"gorp_migrations",
"messages",
"organizations",
"payments",
"receiver_verifications",
"receiver_wallets",
"receivers",
"submitter_transactions",
"wallets",
"wallets_assets",
}
tenant.TenantSchemaHasTablesFixture(t, ctx, tenantSchemaConnectionPool, schemaName, expectedTablesAfterMigrationsApplied)
tenant.AssertRegisteredAssets(t, ctx, tenantSchemaConnectionPool, []string{"USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", "XLM:"})
tenant.AssertRegisteredWallets(t, ctx, tenantSchemaConnectionPool, []string{"Vibrant Assist RC", "Vibrant Assist"})
tenant.AssertRegisteredUser(t, ctx, tenantSchemaConnectionPool, userFirstName, userLastName, userEmail)
})
}
Loading

0 comments on commit 7b13b6c

Please sign in to comment.