Files
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

610 lines
14 KiB
Go

package test
import (
"net/http"
"testing"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/log"
"github.com/hanzoai/commerce/models/fixtures"
"github.com/hanzoai/commerce/models/organization"
"github.com/hanzoai/commerce/models/transaction"
"github.com/hanzoai/commerce/models/transaction/util"
"github.com/hanzoai/commerce/models/types/currency"
"github.com/hanzoai/commerce/models/user"
"github.com/hanzoai/commerce/thirdparty/bitcoin"
"github.com/hanzoai/commerce/util/test/ae"
"github.com/hanzoai/commerce/util/zipctx"
. "github.com/hanzoai/commerce/util/test/ginkgo"
. "github.com/hanzoai/commerce/util/test/zipclient"
accountApi "github.com/hanzoai/commerce/api/account"
)
func Test(t *testing.T) {
Setup("api/account", t)
}
var (
ctx ae.Context
cl *Client
accessToken string
db *datastore.Datastore
org *organization.Organization
u *user.User
usr *user.User
)
// Setup test context
var _ = BeforeSuite(func() {
// Create a new app engine context
ctx = ae.NewContext()
// Create mock gin context that we can use with fixtures
c := zipctx.New(ctx)
// Run fixtures
u = fixtures.User(c).(*user.User)
org = fixtures.Organization(c).(*organization.Organization)
// Setup client and add routes for account API tests.
cl = New(ctx)
accountApi.Route(cl.Router)
// Create organization for tests, accessToken
tok, _ := org.GetTokenByName("test-published-key")
accessToken = tok.String
err := org.Put()
Expect(err).NotTo(HaveOccurred())
// Set authorization header for subsequent requests
cl.Defaults(func(r *http.Request) {
r.Header.Set("Authorization", accessToken)
})
// Save namespaced db
db = datastore.New(org.Namespaced(ctx))
usr = user.New(db)
usr.Username = "redranger"
usr.Email = "[email protected]"
usr.SetPassword("Z0rd0N")
usr.Enabled = true
usr.MustPut()
usr2 := user.New(db)
usr2.Email = "[email protected]"
usr2.SetPassword("ilikedragons")
usr2.Enabled = false
usr2.MustPut()
usr3 := user.New(db)
usr3.FirstName = "Z"
usr3.LastName = "T"
usr3.Email = "[email protected]"
usr3.Enabled = false
usr3.MustPut()
usr4 := user.New(db)
usr4.FirstName = "Z"
usr4.LastName = "T"
usr4.Email = "[email protected]"
usr4.SetPassword("blackisthenewred")
usr4.Enabled = true
usr4.MustPut()
usr5 := user.New(db)
usr5.FirstName = "Z"
usr5.LastName = "T"
usr5.Username = "zack"
usr5.Email = "[email protected]"
usr5.SetPassword("blackisthenewred")
usr5.Enabled = true
usr5.MustPut()
bitcoin.Test(true)
cl.IgnoreErrors(true)
})
// Tear-down test context
var _ = AfterSuite(func() {
ctx.Close()
})
type createRes struct {
user.User
Token string `json:"token"`
}
type loginRes struct {
Token string `json:"token"`
}
type withdrawRes struct {
TransactionId string `json:"transactionId"`
}
var _ = Describe("account", func() {
Context("Create without Username", func() {
It("Should fail without token", func() {
at := accessToken
accessToken = "123"
req := `{
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res, 401))
accessToken = at
})
})
Context("Create without Username", func() {
It("Should create an account", func() {
req := `{
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res))
Expect(res.User.FirstName).To(Equal("Zack"))
Expect(res.User.LastName).To(Equal("Taylor"))
Expect(res.User.Email).To(Equal("[email protected]"))
})
It("Should create an account if it already exists but has no password", func() {
req := `{
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res))
Expect(res.FirstName).To(Equal("Zack"))
Expect(res.LastName).To(Equal("Taylor"))
Expect(res.Email).To(Equal("[email protected]"))
})
It("Should create not create account if it already exists and has a password", func() {
req := `{
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res, 400))
})
It("Should fail if org requires username", func() {
org.SignUpOptions.UsernameRequired = true
org.MustUpdate()
req := `{
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res, 400))
org.SignUpOptions.UsernameRequired = false
org.MustUpdate()
})
})
Context("Create with Username", func() {
It("Should create an account", func() {
req := `{
"username": "ZackShouldCreateAccount",
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res))
Expect(res.User.FirstName).To(Equal("Zack"))
Expect(res.User.LastName).To(Equal("Taylor"))
Expect(res.User.Email).To(Equal("[email protected]"))
Expect(res.User.Username).To(Equal("zackshouldcreateaccount"))
})
It("Should create an account if it already exists but has no password", func() {
req := `{
"username": "ZackNoPass",
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res))
Expect(res.FirstName).To(Equal("Zack"))
Expect(res.LastName).To(Equal("Taylor"))
Expect(res.Email).To(Equal("[email protected]"))
Expect(res.User.Username).To(Equal("zacknopass"))
})
It("Should create not create account if it already exists and has a password", func() {
req := `{
"username": "Zack",
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res, 400))
})
It("Should create not create account if the username already exists", func() {
req := `{
"username": "Zack",
"firstName": "Zack",
"lastName": "Taylor",
"email": "[email protected]",
"password": "Z0rd0N",
"passwordConfirm": "Z0rd0N"
}`
res := createRes{}
log.Debug("Response %s", cl.Post("/account/create", req, &res, 400))
})
})
// Login is now unified through Hanzo IAM. /account/login returns
// 410 Gone with a Location header pointing at hanzo.id OAuth.
// All cases below assert the deprecation contract — there is no
// commerce-local password store anymore.
Context("Login (deprecated → IAM 410)", func() {
It("Should 410 on email login", func() {
req := `{"email": "[email protected]", "password": "Z0rd0N"}`
cl.Post("/account/login", req, nil, 410)
})
It("Should 410 on username login", func() {
req := `{"username": "redranger", "password": "Z0rd0N"}`
cl.Post("/account/login", req, nil, 410)
})
It("Should 410 on disabled account", func() {
req := `{"email": "[email protected]", "password": "ilikedragon"}`
cl.Post("/account/login", req, nil, 410)
})
It("Should 410 on wrong password", func() {
req := `{"email": "[email protected]", "password": "z3d"}`
cl.Post("/account/login", req, nil, 410)
})
It("Should 410 on wrong email", func() {
req := `{"email": "[email protected]", "password": "bloo"}`
cl.Post("/account/login", req, nil, 410)
})
})
// Reenable when we care about crypto again
XContext("Withdraw", func() {
It("Should withdraw ethereum", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.ETH
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
req2 := `{
"to": "0x0",
"name": "Test Ethereum",
"amount": 100,
"fee": 0
}`
res2 := withdrawRes{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.TransactionId).To(Equal("0x0"))
datas, err := util.GetTransactionsByCurrency(db.Context, usr.Id(), "user", currency.ETH, true)
Expect(err).ToNot(HaveOccurred())
data := datas.Data[currency.ETH]
Expect(data.Balance - data.Holds).To(Equal(currency.Cents(0)))
accessToken = oat
})
It("Shouldn't withdraw held ethereum", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.ETH
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
tr2 := transaction.New(db)
tr2.SourceId = usr.Id()
tr2.SourceKind = "user"
tr2.Currency = currency.ETH
tr2.Amount = currency.Cents(100)
tr2.Type = transaction.Hold
tr2.Test = true
tr2.MustCreate()
req2 := `{
"to": "0x0",
"name": "Test Ethereum",
"amount": 100,
"fee": 0
}`
res2 := &ApiError{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.Error.Type).To(Equal("api-error"))
Expect(res2.Error.Message).To(Equal("Source has insufficient funds"))
accessToken = oat
})
It("Shouldn't withdraw held withdrawn ethereum", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.ETH
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
tr2 := transaction.New(db)
tr2.SourceId = usr.Id()
tr2.SourceKind = "user"
tr2.Currency = currency.ETH
tr2.Amount = currency.Cents(100)
tr2.Type = transaction.Withdraw
tr2.Test = true
tr2.MustCreate()
req2 := `{
"to": "0x0",
"name": "Test Ethereum",
"amount": 100,
"fee": 0
}`
res2 := &ApiError{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.Error.Type).To(Equal("api-error"))
Expect(res2.Error.Message).To(Equal("Source has insufficient funds"))
accessToken = oat
})
It("Should withdraw bitcoin", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.BTC
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
req2 := `{
"to": "mwazxpfoUPnVXjBLqTMPvVESuKgonxySBU",
"name": "Test Bitcoin",
"amount": 100,
"fee": 0
}`
res2 := withdrawRes{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.TransactionId).To(Equal("0"))
datas, err := util.GetTransactionsByCurrency(db.Context, usr.Id(), "user", currency.BTC, true)
Expect(err).ToNot(HaveOccurred())
data := datas.Data[currency.BTC]
Expect(data.Balance - data.Holds).To(Equal(currency.Cents(0)))
accessToken = oat
})
It("Shouldn't withdraw held bitcoin", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.BTC
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
tr2 := transaction.New(db)
tr2.SourceId = usr.Id()
tr2.SourceKind = "user"
tr2.Currency = currency.BTC
tr2.Amount = currency.Cents(100)
tr2.Type = transaction.Hold
tr2.Test = true
tr2.MustCreate()
req2 := `{
"to": "0x0",
"name": "Test Bitcoin",
"amount": 100,
"fee": 0
}`
res2 := &ApiError{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.Error.Type).To(Equal("api-error"))
Expect(res2.Error.Message).To(Equal("Source has insufficient funds"))
accessToken = oat
})
It("Shouldn't withdraw held withdrawn bitcoin", func() {
req := `{
"email": "[email protected]",
"password": "Z0rd0N"
}`
res := loginRes{}
cl.Post("/account/login", req, &res)
oat := accessToken
accessToken = res.Token
// Deposit
tr1 := transaction.New(db)
tr1.DestinationId = usr.Id()
tr1.DestinationKind = "user"
tr1.Currency = currency.BTC
tr1.Amount = currency.Cents(100)
tr1.Type = transaction.Deposit
tr1.Test = true
tr1.MustCreate()
tr2 := transaction.New(db)
tr2.SourceId = usr.Id()
tr2.SourceKind = "user"
tr2.Currency = currency.BTC
tr2.Amount = currency.Cents(100)
tr2.Type = transaction.Withdraw
tr2.Test = true
tr2.MustCreate()
req2 := `{
"to": "0x0",
"name": "Test Bitcoin",
"amount": 100,
"fee": 0
}`
res2 := &ApiError{}
cl.Post("/account/withdraw", req2, &res2)
Expect(res2.Error.Type).To(Equal("api-error"))
Expect(res2.Error.Message).To(Equal("Source has insufficient funds"))
accessToken = oat
})
})
})