satellite/payments: promotional coupons generation functional added

Change-Id: Ie0df256503114ca377d81bf7c8b26cc90a1f5b26
This commit is contained in:
crawter 2020-01-18 04:34:06 +02:00 committed by Yehor Butko
parent a4026f97b8
commit c4cbc6ff2f
9 changed files with 570 additions and 7 deletions

View File

@ -129,6 +129,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
router.HandleFunc("/api/v0/graphql", server.grapqlHandler)
router.HandleFunc("/registrationToken/", server.createRegistrationTokenHandler)
router.HandleFunc("/populate-promotional-coupons", server.populatePromotionalCoupons).Methods(http.MethodPost)
router.HandleFunc("/robots.txt", server.seoHandler)
router.Handle(
@ -327,7 +328,7 @@ func (server *Server) bucketUsageReportHandler(w http.ResponseWriter, r *http.Re
}
}
// accountActivationHandler is web app http handler function
// createRegistrationTokenHandler is web app http handler function.
func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
@ -372,6 +373,40 @@ func (server *Server) createRegistrationTokenHandler(w http.ResponseWriter, r *h
response.Secret = token.Secret.String()
}
// populatePromotionalCoupons is web app http handler function for populating promotional coupons.
func (server *Server) populatePromotionalCoupons(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
w.Header().Set(contentType, applicationJSON)
var response struct {
Error string `json:"error,omitempty"`
}
defer func() {
err := json.NewEncoder(w).Encode(&response)
if err != nil {
server.log.Error("failed to write json error response", zap.Error(Error.Wrap(err)))
}
}()
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(server.config.AuthToken),
)
if equality != 1 {
w.WriteHeader(401)
response.Error = "unauthorized"
return
}
err := server.service.Payments().PopulatePromotionalCoupons(ctx)
if err != nil {
response.Error = err.Error()
return
}
}
// accountActivationHandler is web app http handler function
func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

View File

@ -17,6 +17,7 @@ import (
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/common/macaroon"
"storj.io/common/memory"
"storj.io/storj/pkg/auth"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console/consoleauth"
@ -203,7 +204,7 @@ func (payments PaymentsService) RemoveCreditCard(ctx context.Context, cardID str
return payments.service.accounts.CreditCards().Remove(ctx, auth.User.ID, cardID)
}
// BillingHistory returns a list of invoices, transactions and all others billing history items for payment account.
// BillingHistory returns a list of billing history items for payment account.
func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
defer mon.Task()(&ctx)(&err)
@ -217,7 +218,6 @@ func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHist
return nil, Error.Wrap(err)
}
// TODO: add transactions, etc in future
for _, invoice := range invoices {
billingHistory = append(billingHistory, &BillingHistoryItem{
ID: invoice.ID,
@ -275,9 +275,8 @@ func (payments PaymentsService) BillingHistory(ctx context.Context) (billingHist
for _, coupon := range coupons {
billingHistory = append(billingHistory,
&BillingHistoryItem{
ID: coupon.ID.String(),
// TODO: update description in future, when there will be more coupon types.
Description: fmt.Sprintf("Promotional credits (limited time - %d billing periods)", coupon.Duration),
ID: coupon.ID.String(),
Description: coupon.Description,
Amount: coupon.Amount,
Status: "Added to balance",
Link: "",
@ -309,6 +308,15 @@ func (payments PaymentsService) TokenDeposit(ctx context.Context, amount int64)
return tx, errs.Wrap(err)
}
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
// a project, payment method and do not have a promotional coupon yet.
// And updates project limits to selected size.
func (payments PaymentsService) PopulatePromotionalCoupons(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
return Error.Wrap(payments.service.accounts.PopulatePromotionalCoupons(ctx, 2, 5500, memory.TB))
}
// CreateUser gets password hash value and creates new inactive User
func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret RegistrationSecret, refUserID string) (u *User, err error) {
defer mon.Task()(&ctx)(&err)
@ -381,6 +389,7 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
Status: Inactive,
}
if user.PartnerID != "" {
partnerID, err := uuid.Parse(user.PartnerID)

View File

@ -8,6 +8,8 @@ import (
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/common/memory"
)
// ErrAccountNotSetup is an error type which indicates that payment account is not created.
@ -31,6 +33,11 @@ type Accounts interface {
// Coupons return list of all coupons of specified payment account.
Coupons(ctx context.Context, userID uuid.UUID) ([]Coupon, error)
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
// a project, payment method and do not have a promotional coupon yet.
// And updates project limits to selected size.
PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) error
// CreditCards exposes all needed functionality to manage account credit cards.
CreditCards() CreditCards

View File

@ -8,8 +8,9 @@ import (
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
monkit "gopkg.in/spacemonkeygo/monkit.v2"
"gopkg.in/spacemonkeygo/monkit.v2"
"storj.io/common/memory"
"storj.io/storj/satellite/payments"
)
@ -95,6 +96,15 @@ func (accounts *accounts) Coupons(ctx context.Context, userID uuid.UUID) (coupon
return coupons, nil
}
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
// a project, payment method and do not have a promotional coupon yet.
// And updates project limits to selected size.
func (accounts *accounts) PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) (err error) {
defer mon.Task()(&ctx, duration, amount, projectLimit)(&err)
return nil
}
// List returns a list of credit cards for a given payment account.
func (creditCards *creditCards) List(ctx context.Context, userID uuid.UUID) (_ []payments.CreditCard, err error) {
defer mon.Task()(&ctx, userID)(&err)

View File

@ -195,6 +195,77 @@ func (accounts *accounts) Coupons(ctx context.Context, userID uuid.UUID) (coupon
return coupons, Error.Wrap(err)
}
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have
// a project, payment method and do not have a promotional coupon yet.
// And updates project limits to selected size.
func (accounts *accounts) PopulatePromotionalCoupons(ctx context.Context, duration int, amount int64, projectLimit memory.Size) (err error) {
defer mon.Task()(&ctx, duration, amount, projectLimit)(&err)
const limit = 50
before := time.Now()
cusPage, err := accounts.service.db.Customers().List(ctx, 0, limit, before)
if err != nil {
return Error.Wrap(err)
}
// taking only users that attached a payment method.
var usersIDs []uuid.UUID
for _, cus := range cusPage.Customers {
params := &stripe.PaymentMethodListParams{
Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
Customer: stripe.String(cus.ID),
}
paymentMethodsIterator := accounts.service.stripeClient.PaymentMethods.List(params)
for paymentMethodsIterator.Next() {
// if user has at least 1 payment method - break a loop.
usersIDs = append(usersIDs, cus.UserID)
break
}
if err = paymentMethodsIterator.Err(); err != nil {
return Error.Wrap(err)
}
}
err = accounts.service.db.Coupons().PopulatePromotionalCoupons(ctx, usersIDs, duration, amount, projectLimit)
if err != nil {
return Error.Wrap(err)
}
for cusPage.Next {
// we have to wait before each iteration because
// Stripe has rate limits - 100 read and 100 write operations per second per secret key.
time.Sleep(time.Second)
var usersIDs []uuid.UUID
for _, cus := range cusPage.Customers {
params := &stripe.PaymentMethodListParams{
Type: stripe.String(string(stripe.PaymentMethodTypeCard)),
Customer: stripe.String(cus.ID),
}
paymentMethodsIterator := accounts.service.stripeClient.PaymentMethods.List(params)
for paymentMethodsIterator.Next() {
usersIDs = append(usersIDs, cus.UserID)
break
}
if err = paymentMethodsIterator.Err(); err != nil {
return Error.Wrap(err)
}
}
err = accounts.service.db.Coupons().PopulatePromotionalCoupons(ctx, usersIDs, duration, amount, projectLimit)
if err != nil {
return Error.Wrap(err)
}
}
return nil
}
// StorjTokens exposes all storj token related functionality.
func (accounts *accounts) StorjTokens() payments.StorjTokens {
return &storjTokens{service: accounts.service}

View File

@ -9,6 +9,7 @@ import (
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/common/memory"
"storj.io/storj/satellite/payments"
)
@ -43,6 +44,10 @@ type CouponsDB interface {
ListUnapplied(ctx context.Context, offset int64, limit int, before time.Time) (CouponUsagePage, error)
// ApplyUsage applies coupon usage and updates its status.
ApplyUsage(ctx context.Context, couponID uuid.UUID, period time.Time) error
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have a project
// and do not have a promotional coupon yet. And updates project limits to selected size.
PopulatePromotionalCoupons(ctx context.Context, users []uuid.UUID, duration int, amount int64, projectLimit memory.Size) error
}
// CouponUsage stores amount of money that should be charged from coupon for billing period.

View File

@ -7,11 +7,15 @@ import (
"testing"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/common/memory"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/satellite"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
@ -86,3 +90,282 @@ func TestCouponRepository(t *testing.T) {
})
})
}
// TestPopulatePromotionalCoupons is a test for PopulatePromotionalCoupons function
// that creates coupons with predefined values for each of user (from arguments) that have a project
// and that don't have a promotional coupon yet. Also it updates limits of selected projects to 1TB.
// Because the coupon should be added to a project, we select the first project of the user.
// In this test i have next test cases:
// 1. Activated user, 2 projects, without coupon. For this case we should add new coupon to his first project.
// 2. Activated user, 1 project, without coupon.
// 3. Activated user without project. Coupon should not be added.
// 4. User with inactive account. Coupon should not be added.
// 5. Activated user with project and coupon. Coupon should not be added.
// 6. Next step - is populating coupons for all 5 users. Only 2 coupons should be added.
// 7. Creating new user with project.
// 8. Populating coupons again. For 6 users above. Only 1 new coupon should be added.
// Three new coupons total should be added by 2 runs of PopulatePromotionalCoupons method.
func TestPopulatePromotionalCoupons(t *testing.T) {
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
usersRepo := db.Console().Users()
projectsRepo := db.Console().Projects()
couponsRepo := db.StripeCoinPayments().Coupons()
usageRepo := db.ProjectAccounting()
// creating test users with different status.
// activated user with 2 project. New coupon should be added to the first project.
user1, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user1",
ShortName: "",
Email: "test1@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
user1.Status = console.Active
err = usersRepo.Update(ctx, user1)
require.NoError(t, err)
// activated user with proj. New coupon should be added.
user2, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user2",
ShortName: "",
Email: "test2@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
user2.Status = console.Active
err = usersRepo.Update(ctx, user2)
require.NoError(t, err)
// activated user without proj. New coupon should not be added.
user3, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user3",
ShortName: "",
Email: "test3@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
user3.Status = console.Active
err = usersRepo.Update(ctx, user3)
require.NoError(t, err)
// inactive user. New coupon should not be added.
user4, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user4",
ShortName: "",
Email: "test4@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
// activated user with proj and coupon. New coupon should not be added.
user5, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user5",
ShortName: "",
Email: "test5@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
user5.Status = console.Active
err = usersRepo.Update(ctx, user5)
require.NoError(t, err)
// creating projects for users above.
proj1, err := projectsRepo.Insert(ctx, &console.Project{
ID: testrand.UUID(),
Name: "proj 1 of user 1",
Description: "descr 1",
OwnerID: user1.ID,
})
if err != nil {
require.NoError(t, err)
}
// should not be processed as we takes only first project of the user.
proj2, err := projectsRepo.Insert(ctx, &console.Project{
ID: testrand.UUID(),
Name: "proj 2 of user 1",
Description: "descr 2",
OwnerID: user1.ID,
})
if err != nil {
require.NoError(t, err)
}
proj3, err := projectsRepo.Insert(ctx, &console.Project{
ID: testrand.UUID(),
Name: "proj 1 of user 2",
Description: "descr 3",
OwnerID: user2.ID,
})
if err != nil {
require.NoError(t, err)
}
proj4, err := projectsRepo.Insert(ctx, &console.Project{
ID: testrand.UUID(),
Name: "proj 1 of user 5",
Description: "descr 4",
OwnerID: user5.ID,
})
if err != nil {
require.NoError(t, err)
}
couponID := testrand.UUID()
err = couponsRepo.Insert(ctx, payments.Coupon{
ID: couponID,
UserID: user5.ID,
ProjectID: proj4.ID,
Amount: 5500,
Duration: 2,
Description: "qw",
Type: payments.CouponTypePromotional,
Status: payments.CouponActive,
})
if err != nil {
require.NoError(t, err)
}
// creating new users and projects to test that multiple execution of populate method wont generate extra coupons.
user6, err := usersRepo.Insert(ctx, &console.User{
ID: testrand.UUID(),
FullName: "user6",
ShortName: "",
Email: "test6@example.com",
PasswordHash: []byte("123qwe"),
})
require.NoError(t, err)
user6.Status = console.Active
err = usersRepo.Update(ctx, user6)
require.NoError(t, err)
proj5, err := projectsRepo.Insert(ctx, &console.Project{
ID: testrand.UUID(),
Name: "proj 1 of user 6",
Description: "descr 6",
OwnerID: user6.ID,
})
if err != nil {
require.NoError(t, err)
}
t.Run("first population", func(t *testing.T) {
var usersIds = []uuid.UUID{
user1.ID,
user2.ID,
user3.ID,
user4.ID,
user5.ID,
}
err := couponsRepo.PopulatePromotionalCoupons(ctx, usersIds, 2, 5500, memory.TB)
assert.NoError(t, err)
user1Coupons, err := couponsRepo.ListByUserID(ctx, user1.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user1Coupons))
assert.Equal(t, proj1.ID, user1Coupons[0].ProjectID)
proj1Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj1.ID)
assert.NoError(t, err)
assert.Equal(t, memory.TB, proj1Usage)
proj2Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj2.ID)
assert.NoError(t, err)
assert.Equal(t, 0, int(proj2Usage))
user2Coupons, err := couponsRepo.ListByUserID(ctx, user2.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user2Coupons))
proj3Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj3.ID)
assert.NoError(t, err)
assert.Equal(t, memory.TB, proj3Usage)
user3Coupons, err := couponsRepo.ListByUserID(ctx, user3.ID)
assert.NoError(t, err)
assert.Equal(t, 0, len(user3Coupons))
user4Coupons, err := couponsRepo.ListByUserID(ctx, user4.ID)
assert.NoError(t, err)
assert.Equal(t, 0, len(user4Coupons))
user5Coupons, err := couponsRepo.ListByUserID(ctx, user5.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user5Coupons))
assert.Equal(t, "qw", user5Coupons[0].Description)
})
t.Run("second population", func(t *testing.T) {
var usersIds = []uuid.UUID{
user1.ID,
user2.ID,
user3.ID,
user4.ID,
user5.ID,
user6.ID,
}
err := couponsRepo.PopulatePromotionalCoupons(ctx, usersIds, 2, 5500, memory.TB)
assert.NoError(t, err)
user1Coupons, err := couponsRepo.ListByUserID(ctx, user1.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user1Coupons))
assert.Equal(t, proj1.ID, user1Coupons[0].ProjectID)
proj1Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj1.ID)
assert.NoError(t, err)
assert.Equal(t, memory.TB, proj1Usage)
proj2Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj2.ID)
assert.NoError(t, err)
assert.Equal(t, 0, int(proj2Usage))
user2Coupons, err := couponsRepo.ListByUserID(ctx, user2.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user2Coupons))
proj3Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj3.ID)
assert.NoError(t, err)
assert.Equal(t, memory.TB, proj3Usage)
user3Coupons, err := couponsRepo.ListByUserID(ctx, user3.ID)
assert.NoError(t, err)
assert.Equal(t, 0, len(user3Coupons))
user4Coupons, err := couponsRepo.ListByUserID(ctx, user4.ID)
assert.NoError(t, err)
assert.Equal(t, 0, len(user4Coupons))
user5Coupons, err := couponsRepo.ListByUserID(ctx, user5.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user5Coupons))
assert.Equal(t, "qw", user5Coupons[0].Description)
user6Coupons, err := couponsRepo.ListByUserID(ctx, user6.ID)
assert.NoError(t, err)
assert.Equal(t, 1, len(user6Coupons))
proj5Usage, err := usageRepo.GetProjectStorageLimit(ctx, proj5.ID)
assert.NoError(t, err)
assert.Equal(t, memory.TB, proj5Usage)
})
})
}

View File

@ -6,12 +6,15 @@ package satellitedb
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/common/memory"
"storj.io/storj/private/dbutil"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/coinpayments"
"storj.io/storj/satellite/payments/stripecoinpayments"
@ -355,3 +358,115 @@ func couponUsageFromDbxSlice(couponUsageDbx *dbx.CouponUsage) (usage stripecoinp
return usage, err
}
// PopulatePromotionalCoupons is used to populate promotional coupons through all active users who already have a project
// and do not have a promotional coupon yet. And updates project limits to selected size.
func (coupons *coupons) PopulatePromotionalCoupons(ctx context.Context, users []uuid.UUID, duration int, amount int64, projectLimit memory.Size) (err error) {
defer mon.Task()(&ctx, users, duration, amount, projectLimit)(&err)
// converting to []interface{} instead of [][]byte
// to pass it to QueryContext as usersIDs...
var usersIDs []interface{}
for i := range users {
usersIDs = append(usersIDs, users[i][:])
}
query := coupons.db.Rebind(fmt.Sprintf(
`SELECT users_with_projects.id, users_with_projects.project_id FROM (
SELECT selected_users.id, first_proj.id as project_id
FROM (select id, status from users where id in `+generateArgumentsDBX(len(users))+`) as selected_users INNER JOIN
(select distinct on (owner_id) owner_id, id from projects order by owner_id, created_at asc) as first_proj
ON (selected_users.id = first_proj.owner_id)
WHERE selected_users.status = %d) AS users_with_projects
WHERE users_with_projects.id NOT IN (
SELECT user_id FROM coupons WHERE type = %d
);`, console.Active, payments.CouponTypePromotional),
)
usersIDsRows, err := coupons.db.QueryContext(ctx, query, usersIDs...)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, usersIDsRows.Close()) }()
type UserAndProjectIDs struct {
userID uuid.UUID
projectID uuid.UUID
}
var ids []UserAndProjectIDs
for usersIDsRows.Next() {
var userID uuid.UUID
var userIDBytes []byte
var projectID uuid.UUID
var projectIDBytes []byte
err = usersIDsRows.Scan(&userIDBytes, &projectIDBytes)
if err != nil {
return err
}
userID, err = dbutil.BytesToUUID(userIDBytes)
if err != nil {
return err
}
projectID, err = dbutil.BytesToUUID(projectIDBytes)
if err != nil {
return err
}
ids = append(ids, UserAndProjectIDs{userID: userID, projectID: projectID})
}
if err = usersIDsRows.Err(); err != nil {
return err
}
return coupons.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
for _, userAndProjectID := range ids {
err = coupons.Insert(ctx, payments.Coupon{
UserID: userAndProjectID.userID,
ProjectID: userAndProjectID.projectID,
Amount: amount,
Duration: duration,
Description: fmt.Sprintf("Promotional credits (limited time - %d billing periods)", duration),
Type: payments.CouponTypePromotional,
Status: payments.CouponActive,
})
if err != nil {
return err
}
_, err = coupons.db.Update_Project_By_Id(ctx,
dbx.Project_Id(userAndProjectID.projectID[:]),
dbx.Project_Update_Fields{
UsageLimit: dbx.Project_UsageLimit(projectLimit.Int64()),
},
)
if err != nil {
return err
}
}
return nil
})
}
// generates variadic number of '?' symbols in brackets depends on input parameter.
func generateArgumentsDBX(length int) string {
start := "("
if length >= 1 {
start += "?"
}
middle := ""
for i := 1; i < length; i++ {
middle += ", ?"
}
end := ")"
return start + middle + end
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package satellitedb
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGenerateArgumentsDBX(t *testing.T) {
testCases := [...]struct {
length int
expectedResult string
}{
0: {0, "()"},
1: {1, "(?)"},
2: {2, "(?, ?)"},
3: {3, "(?, ?, ?)"},
4: {-1, "()"},
5: {-2, "()"},
}
for _, tc := range testCases {
assert.Equal(t, tc.expectedResult, generateArgumentsDBX(tc.length))
}
}