Skip to content

Commit

Permalink
Invest endpoint loan retrieval fix (#1606)
Browse files Browse the repository at this point in the history
* investor: Retrieve active and created loans when authenticating

* CI: Enable build for branch

* loans: Update mocks and add more unit tests
  • Loading branch information
cdamian authored Apr 10, 2024
1 parent aa767bf commit bccfb87
Show file tree
Hide file tree
Showing 8 changed files with 793 additions and 39 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/ChainSafe/go-schnorrkel v1.0.0
github.com/Masterminds/semver v1.5.0
github.com/centrifuge/centrifuge-protobufs v1.0.0
github.com/centrifuge/chain-custom-types v1.0.8
github.com/centrifuge/chain-custom-types v1.0.9
github.com/centrifuge/go-substrate-rpc-client/v4 v4.1.0
github.com/centrifuge/gocelery/v2 v2.0.0-20221101190423-3b07af1b49a6
github.com/centrifuge/precise-proofs v1.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/centrifuge/centrifuge-protobufs v1.0.0 h1:ZPg0XpkTrGrjQu8scXjMGs7jjqsWPiXmOXdV/bz30ng=
github.com/centrifuge/centrifuge-protobufs v1.0.0/go.mod h1:VL6mcnK6vTRiFljHP39J0WBI3Uu5BHQjhdFkCxY9/9I=
github.com/centrifuge/chain-custom-types v1.0.8 h1:JcXQNzjzs1y/xEBK23XlRJeD1OxLZByfnwstQkORuIg=
github.com/centrifuge/chain-custom-types v1.0.8/go.mod h1:kSUJ3O83vaLutJIiaEfqwn3lfTaisn/G/baS8WrycTg=
github.com/centrifuge/chain-custom-types v1.0.9 h1:utkYu/8Tgze6xktHMZ9wgcDHXUsM2yWuwSHL2YqfZ+8=
github.com/centrifuge/chain-custom-types v1.0.9/go.mod h1:kSUJ3O83vaLutJIiaEfqwn3lfTaisn/G/baS8WrycTg=
github.com/centrifuge/go-merkle v0.0.0-20190727075423-0ac78bbbc01b h1:TPvvMcGAc3TVBVgQ4XYYEWTXxYls8YuylZ8JzrVxPzc=
github.com/centrifuge/go-merkle v0.0.0-20190727075423-0ac78bbbc01b/go.mod h1:0voJY6Qzxvr2S0LeDSFQiCnJzGq5gORg2SwCmn8602I=
github.com/centrifuge/go-substrate-rpc-client/v4 v4.1.0 h1:GEvub7kU5YFAcn5A2uOo4AZSM1/cWZCOvfu7E3gQmK8=
Expand Down
2 changes: 1 addition & 1 deletion http/auth/access/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const (
ErrInvestorAccessParamsRetrieval = errors.Error("investor access params retrieval")
ErrDocumentIDRetrieval = errors.Error("document ID retrieval")
ErrDocumentIDMismatch = errors.Error("document IDs do not match")
ErrCreatedLoanRetrieval = errors.Error("created loan retrieval")
ErrNoValidationServiceForPath = errors.Error("no validator service for request path")
ErrIdentityNotFound = errors.Error("identity not found")
ErrInvalidProxyType = errors.Error("invalid proxy type")
ErrInvalidAuthorizationHeader = errors.Error("invalid authorization header")
ErrLoanCollateralNotFound = errors.Error("loan collateral not found")
)
65 changes: 53 additions & 12 deletions http/auth/access/investor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package access

import (
"bytes"
"errors"
"fmt"
"net/http"
"strconv"

loanTypes "github.com/centrifuge/chain-custom-types/pkg/loans"
"github.com/centrifuge/go-substrate-rpc-client/v4/types"
authToken "github.com/centrifuge/pod/http/auth/token"
"github.com/centrifuge/pod/http/coreapi"
Expand All @@ -17,6 +19,17 @@ import (
logging "github.com/ipfs/go-log"
)

type InvestorAccessParams struct {
AssetID []byte
PoolID types.U64
LoanID types.U64
}

type LoanCollateral struct {
Asset loanTypes.Asset
Borrower types.AccountID
}

type investorAccessValidator struct {
log *logging.ZapEventLogger
loansAPI loans.API
Expand Down Expand Up @@ -65,12 +78,6 @@ func (i *investorAccessValidator) Validate(req *http.Request, token *authToken.J
return i.validateDocument(params)
}

type InvestorAccessParams struct {
AssetID []byte
PoolID types.U64
LoanID types.U64
}

func getInvestorAccessParams(req *http.Request) (*InvestorAccessParams, error) {
poolID, err := strconv.Atoi(req.URL.Query().Get(coreapi.PoolIDQueryParam))

Expand Down Expand Up @@ -120,17 +127,17 @@ func (i *investorAccessValidator) validatePoolPermissions(
}

func (i *investorAccessValidator) validateDocument(params *InvestorAccessParams) (*types.AccountID, error) {
loan, err := i.loansAPI.GetCreatedLoan(params.PoolID, params.LoanID)
collateral, err := i.getLoanCollateral(params)

if err != nil {
i.log.Errorf("Couldn't get loan: %s", err)
i.log.Errorf("Couldn't get collateral for loan: %s", err)

return nil, ErrCreatedLoanRetrieval
return nil, err
}

documentID, err := i.uniquesAPI.GetItemAttribute(
loan.Info.Collateral.CollectionID,
loan.Info.Collateral.ItemID,
collateral.Asset.CollectionID,
collateral.Asset.ItemID,
[]byte(nftv3.DocumentIDAttributeKey),
)

Expand All @@ -146,5 +153,39 @@ func (i *investorAccessValidator) validateDocument(params *InvestorAccessParams)
return nil, ErrDocumentIDMismatch
}

return &loan.Borrower, nil
return &collateral.Borrower, nil
}

func (i *investorAccessValidator) getLoanCollateral(params *InvestorAccessParams) (*LoanCollateral, error) {
activeLoan, err := i.loansAPI.GetActiveLoan(params.PoolID, params.LoanID)

if err != nil && !errors.Is(err, loans.ErrActiveLoanNotFound) {
i.log.Errorf("Couldn't get active loan: %s", err)

return nil, err
}

if activeLoan != nil {
return &LoanCollateral{
Asset: activeLoan.Collateral,
Borrower: activeLoan.Borrower,
}, nil
}

createdLoan, err := i.loansAPI.GetCreatedLoan(params.PoolID, params.LoanID)

if err != nil && !errors.Is(err, loans.ErrCreatedLoanNotFound) {
i.log.Errorf("Couldn't get created loan: %s", err)

return nil, err
}

if createdLoan != nil {
return &LoanCollateral{
Asset: createdLoan.Info.Collateral,
Borrower: createdLoan.Borrower,
}, nil
}

return nil, ErrLoanCollateralNotFound
}
165 changes: 146 additions & 19 deletions http/auth/access/investor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"github.com/vedhavyas/go-subkey"
)

func TestInvestorAccessValidator_Validate(t *testing.T) {
func TestInvestorAccessValidator_Validate_WithActiveLoan(t *testing.T) {
loansAPIMock := loans.NewAPIMock(t)
permissionsAPIMock := permissions.NewAPIMock(t)
uniquesAPIMock := uniques.NewAPIMock(t)
Expand Down Expand Up @@ -74,6 +74,83 @@ func TestInvestorAccessValidator_Validate(t *testing.T) {
collectionID := types.U64(rand.Uint32())
itemID := types.NewU128(*big.NewInt(rand.Int63()))

activeLoan := &loanTypes.ActiveLoan{
Collateral: loanTypes.Asset{
CollectionID: collectionID,
ItemID: itemID,
},
Borrower: *borrowerAccountID,
}

loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(activeLoan, nil).
Once()

uniquesAPIMock.On(
"GetItemAttribute",
collectionID,
itemID,
[]byte(nftv3.DocumentIDAttributeKey),
).
Return([]byte(documentID), nil).
Once()

res, err := investorAccessValidator.Validate(req, token)
assert.NoError(t, err)
assert.Equal(t, borrowerAccountID, res)
}

func TestInvestorAccessValidator_Validate_WithCreatedLoan(t *testing.T) {
loansAPIMock := loans.NewAPIMock(t)
permissionsAPIMock := permissions.NewAPIMock(t)
uniquesAPIMock := uniques.NewAPIMock(t)

investorAccessValidator := NewInvestorAccessValidator(loansAPIMock, permissionsAPIMock, uniquesAPIMock)

poolID := types.U64(rand.Uint32())
loanID := types.U64(rand.Uint32())
documentID := "document_id"

reqURL, err := url.Parse("http://localhost/v3/investors")
assert.NoError(t, err)

reqURL.RawQuery = fmt.Sprintf(
"%s=%d&%s=%d&%s=%s",
coreapi.PoolIDQueryParam, poolID,
coreapi.LoanIDQueryParam, loanID,
coreapi.AssetIDQueryParam, hexutil.Encode([]byte(documentID)),
)

req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil)
assert.NoError(t, err)

investorAccountID, err := types.NewAccountID(utils.RandomSlice(32))
assert.NoError(t, err)

investSSS58Address := subkey.SS58Encode(investorAccountID.ToBytes(), authToken.CentrifugeNetworkID)

token := &authToken.JW3Token{
Payload: &authToken.JW3TPayload{
Address: investSSS58Address,
},
}

permissionRoles := &permissions.PermissionRoles{PoolAdmin: permissions.PodReadAccess}

permissionsAPIMock.On("GetPermissionRoles", investorAccountID, poolID).
Return(permissionRoles, nil).
Once()

borrowerAccountID, err := types.NewAccountID(utils.RandomSlice(32))
assert.NoError(t, err)

collectionID := types.U64(rand.Uint32())
itemID := types.NewU128(*big.NewInt(rand.Int63()))

loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(nil, loans.ErrActiveLoanNotFound).
Once()

loan := &loans.CreatedLoanStorageEntry{
Info: loanTypes.LoanInfo{
Collateral: loanTypes.Asset{
Expand Down Expand Up @@ -341,6 +418,56 @@ func TestInvestorAccessValidator_Validate_InvalidPoolPermissions(t *testing.T) {
assert.ErrorIs(t, err, ErrInvalidPoolPermissions)
}

func TestInvestorAccessValidator_Validate_ActiveLoanRetrievalError(t *testing.T) {
loansAPIMock := loans.NewAPIMock(t)
permissionsAPIMock := permissions.NewAPIMock(t)
uniquesAPIMock := uniques.NewAPIMock(t)

investorAccessValidator := NewInvestorAccessValidator(loansAPIMock, permissionsAPIMock, uniquesAPIMock)

poolID := types.U64(rand.Uint32())
loanID := types.U64(rand.Uint32())
documentID := "document_id"

reqURL, err := url.Parse("http://localhost/v3/investors")
assert.NoError(t, err)

reqURL.RawQuery = fmt.Sprintf(
"%s=%d&%s=%d&%s=%s",
coreapi.PoolIDQueryParam, poolID,
coreapi.LoanIDQueryParam, loanID,
coreapi.AssetIDQueryParam, hexutil.Encode([]byte(documentID)),
)

req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil)
assert.NoError(t, err)

investorAccountID, err := types.NewAccountID(utils.RandomSlice(32))
assert.NoError(t, err)

investorSSS58Address := subkey.SS58Encode(investorAccountID.ToBytes(), authToken.CentrifugeNetworkID)

token := &authToken.JW3Token{
Payload: &authToken.JW3TPayload{
Address: investorSSS58Address,
},
}

permissionRoles := &permissions.PermissionRoles{PoolAdmin: permissions.PodReadAccess}

permissionsAPIMock.On("GetPermissionRoles", investorAccountID, poolID).
Return(permissionRoles, nil).
Once()

loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(nil, loans.ErrActiveLoansRetrieval).
Once()

res, err := investorAccessValidator.Validate(req, token)
assert.Nil(t, res)
assert.ErrorIs(t, err, loans.ErrActiveLoansRetrieval)
}

func TestInvestorAccessValidator_Validate_CreatedLoanRetrievalError(t *testing.T) {
loansAPIMock := loans.NewAPIMock(t)
permissionsAPIMock := permissions.NewAPIMock(t)
Expand Down Expand Up @@ -382,13 +509,17 @@ func TestInvestorAccessValidator_Validate_CreatedLoanRetrievalError(t *testing.T
Return(permissionRoles, nil).
Once()

loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(nil, loans.ErrActiveLoanNotFound).
Once()

loansAPIMock.On("GetCreatedLoan", poolID, loanID).
Return(nil, errors.New("error")).
Return(nil, loans.ErrCreatedLoanRetrieval).
Once()

res, err := investorAccessValidator.Validate(req, token)
assert.Nil(t, res)
assert.ErrorIs(t, err, ErrCreatedLoanRetrieval)
assert.ErrorIs(t, err, loans.ErrCreatedLoanRetrieval)
}

func TestInvestorAccessValidator_Validate_DocumentIDRetrievalError(t *testing.T) {
Expand Down Expand Up @@ -438,18 +569,16 @@ func TestInvestorAccessValidator_Validate_DocumentIDRetrievalError(t *testing.T)
collectionID := types.U64(rand.Uint32())
itemID := types.NewU128(*big.NewInt(rand.Int63()))

loan := &loans.CreatedLoanStorageEntry{
Info: loanTypes.LoanInfo{
Collateral: loanTypes.Asset{
CollectionID: collectionID,
ItemID: itemID,
},
activeLoan := &loanTypes.ActiveLoan{
Collateral: loanTypes.Asset{
CollectionID: collectionID,
ItemID: itemID,
},
Borrower: *borrowerAccountID,
}

loansAPIMock.On("GetCreatedLoan", poolID, loanID).
Return(loan, nil).
loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(activeLoan, nil).
Once()

uniquesAPIMock.On(
Expand Down Expand Up @@ -513,18 +642,16 @@ func TestInvestorAccessValidator_Validate_DocumentIDMismatch(t *testing.T) {
collectionID := types.U64(rand.Uint32())
itemID := types.NewU128(*big.NewInt(rand.Int63()))

loan := &loans.CreatedLoanStorageEntry{
Info: loanTypes.LoanInfo{
Collateral: loanTypes.Asset{
CollectionID: collectionID,
ItemID: itemID,
},
activeLoan := &loanTypes.ActiveLoan{
Collateral: loanTypes.Asset{
CollectionID: collectionID,
ItemID: itemID,
},
Borrower: *borrowerAccountID,
}

loansAPIMock.On("GetCreatedLoan", poolID, loanID).
Return(loan, nil).
loansAPIMock.On("GetActiveLoan", poolID, loanID).
Return(activeLoan, nil).
Once()

uniquesAPIMock.On(
Expand Down
Loading

0 comments on commit bccfb87

Please sign in to comment.