diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a8c814e69..bea8cab45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ # build workflow builds docker images, pushes images to docker hub and updates swagger API on: push: - branches: [main, invest-endpoint-loan-retrieval-fix] + branches: [main] name: Build jobs: build: diff --git a/go.mod b/go.mod index c2f436a16..ad3eec0b5 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 2ef5cd099..dc5c4e30a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pallets/loans/api.go b/pallets/loans/api.go index 07d845d61..786e0d11c 100644 --- a/pallets/loans/api.go +++ b/pallets/loans/api.go @@ -39,41 +39,15 @@ type CreatedLoanStorageEntry struct { type ActiveLoanStorageEntry struct { LoanID types.U64 - ActiveLoan ActiveLoan -} - -type ActiveLoan struct { - Schedule loans.RepaymentSchedule - Collateral loans.Asset - Restrictions loans.LoanRestrictions - Borrower types.AccountID - WriteOffPercentage types.U128 - OriginationDate types.U64 - Pricing loans.Pricing - TotalBorrowed types.U128 - TotalRepaid RepaidAmount - RepaymentsOnScheduleUntil types.U64 -} - -type RepaidAmount struct { - Principal types.U128 - Interest types.U128 - Unscheduled types.U128 -} - -type ClosedLoan struct { - ClosedAt types.U32 - Info loans.LoanInfo - TotalBorrowed types.U128 - TotalRepaid RepaidAmount + ActiveLoan loans.ActiveLoan } //go:generate mockery --name API --structname APIMock --filename api_mock.go --inpackage type API interface { GetCreatedLoan(poolID types.U64, loanID types.U64) (*CreatedLoanStorageEntry, error) - GetActiveLoan(poolID types.U64, loanID types.U64) (*ActiveLoan, error) - GetClosedLoan(poolID types.U64, loanID types.U64) (*ClosedLoan, error) + GetActiveLoan(poolID types.U64, loanID types.U64) (*loans.ActiveLoan, error) + GetClosedLoan(poolID types.U64, loanID types.U64) (*loans.ClosedLoan, error) } type api struct { @@ -152,7 +126,7 @@ func (a *api) GetCreatedLoan(poolID types.U64, loanID types.U64) (*CreatedLoanSt return &createdLoan, nil } -func (a *api) GetActiveLoan(poolID types.U64, loanID types.U64) (*ActiveLoan, error) { +func (a *api) GetActiveLoan(poolID types.U64, loanID types.U64) (*loans.ActiveLoan, error) { err := validation.Validate( validation.NewValidator(poolID, validation.U64ValidationFn), ) @@ -219,7 +193,7 @@ func (a *api) GetActiveLoan(poolID types.U64, loanID types.U64) (*ActiveLoan, er return nil, ErrActiveLoanNotFound } -func (a *api) GetClosedLoan(poolID types.U64, loanID types.U64) (*ClosedLoan, error) { +func (a *api) GetClosedLoan(poolID types.U64, loanID types.U64) (*loans.ClosedLoan, error) { err := validation.Validate( validation.NewValidator(poolID, validation.U64ValidationFn), ) @@ -268,7 +242,7 @@ func (a *api) GetClosedLoan(poolID types.U64, loanID types.U64) (*ClosedLoan, er return nil, errors.ErrStorageKeyCreation } - var closedLoan ClosedLoan + var closedLoan loans.ClosedLoan ok, err := a.centAPI.GetStorageLatest(storageKey, &closedLoan) diff --git a/pallets/loans/api_mock.go b/pallets/loans/api_mock.go index 0f06fa174..8490be03a 100644 --- a/pallets/loans/api_mock.go +++ b/pallets/loans/api_mock.go @@ -3,6 +3,7 @@ package loans import ( + pkgloans "github.com/centrifuge/chain-custom-types/pkg/loans" types "github.com/centrifuge/go-substrate-rpc-client/v4/types" mock "github.com/stretchr/testify/mock" ) @@ -12,6 +13,52 @@ type APIMock struct { mock.Mock } +// GetActiveLoan provides a mock function with given fields: poolID, loanID +func (_m *APIMock) GetActiveLoan(poolID types.U64, loanID types.U64) (*pkgloans.ActiveLoan, error) { + ret := _m.Called(poolID, loanID) + + var r0 *pkgloans.ActiveLoan + if rf, ok := ret.Get(0).(func(types.U64, types.U64) *pkgloans.ActiveLoan); ok { + r0 = rf(poolID, loanID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pkgloans.ActiveLoan) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.U64, types.U64) error); ok { + r1 = rf(poolID, loanID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetClosedLoan provides a mock function with given fields: poolID, loanID +func (_m *APIMock) GetClosedLoan(poolID types.U64, loanID types.U64) (*pkgloans.ClosedLoan, error) { + ret := _m.Called(poolID, loanID) + + var r0 *pkgloans.ClosedLoan + if rf, ok := ret.Get(0).(func(types.U64, types.U64) *pkgloans.ClosedLoan); ok { + r0 = rf(poolID, loanID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*pkgloans.ClosedLoan) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.U64, types.U64) error); ok { + r1 = rf(poolID, loanID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetCreatedLoan provides a mock function with given fields: poolID, loanID func (_m *APIMock) GetCreatedLoan(poolID types.U64, loanID types.U64) (*CreatedLoanStorageEntry, error) { ret := _m.Called(poolID, loanID) diff --git a/pallets/loans/api_test.go b/pallets/loans/api_test.go index fb4461dc6..7e1dc4bc7 100644 --- a/pallets/loans/api_test.go +++ b/pallets/loans/api_test.go @@ -5,14 +5,13 @@ package loans import ( "testing" - "github.com/centrifuge/pod/errors" - - "github.com/centrifuge/pod/validation" - + "github.com/centrifuge/chain-custom-types/pkg/loans" "github.com/centrifuge/go-substrate-rpc-client/v4/types" "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" "github.com/centrifuge/pod/centchain" + "github.com/centrifuge/pod/errors" "github.com/centrifuge/pod/testingutils" + "github.com/centrifuge/pod/validation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -193,3 +192,395 @@ func TestApi_GetCreatedLoan_StorageEntryNotFound(t *testing.T) { assert.ErrorIs(t, err, ErrCreatedLoanNotFound) assert.Nil(t, res) } + +func TestApi_GetActiveLoan(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ActiveLoansStorageName, encodedPoolID) + assert.NoError(t, err) + + testStorageEntry := ActiveLoanStorageEntry{ + LoanID: loanID, + ActiveLoan: loans.ActiveLoan{}, + } + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + storageEntry, ok := args.Get(1).(*[]ActiveLoanStorageEntry) + assert.True(t, ok) + + *storageEntry = append(*storageEntry, testStorageEntry) + }). + Return(true, nil). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.NoError(t, err) + assert.Equal(t, &testStorageEntry.ActiveLoan, res) +} + +func TestApi_GetActiveLoan_InvalidPoolID(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(0) + loanID := types.U64(0) + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, validation.ErrInvalidU64) + assert.Nil(t, res) +} + +func TestApi_GetActiveLoan_MetadataRetrievalError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + centAPIMock. + On("GetMetadataLatest"). + Return(nil, errors.New("error")). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, errors.ErrMetadataRetrieval) + assert.Nil(t, res) +} + +func TestApi_GetActiveLoan_StorageKeyCreationError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + var meta types.Metadata + + // NOTE - types.MetadataV14Data does not have info on the Loans pallet, + // causing types.CreateStorageKey to fail. + err := codec.DecodeFromHex(types.MetadataV14Data, &meta) + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(&meta, nil). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, errors.ErrStorageKeyCreation) + assert.Nil(t, res) +} + +func TestApi_GetActiveLoan_StorageRetrievalError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ActiveLoansStorageName, encodedPoolID) + assert.NoError(t, err) + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + _, ok := args.Get(1).(*[]ActiveLoanStorageEntry) + assert.True(t, ok) + }). + Return(false, errors.New("error")). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, ErrActiveLoansRetrieval) + assert.Nil(t, res) +} + +func TestApi_GetActiveLoan_StorageEntryNotFound(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ActiveLoansStorageName, encodedPoolID) + assert.NoError(t, err) + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + _, ok := args.Get(1).(*[]ActiveLoanStorageEntry) + assert.True(t, ok) + }). + Return(false, nil). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, ErrActiveLoanNotFound) + assert.Nil(t, res) +} + +func TestApi_GetActiveLoan_ActiveLoanNotFound(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ActiveLoansStorageName, encodedPoolID) + assert.NoError(t, err) + + testStorageEntry := ActiveLoanStorageEntry{ + // Return an entry for a different loan ID. + LoanID: loanID + 1, + ActiveLoan: loans.ActiveLoan{}, + } + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + storageEntry, ok := args.Get(1).(*[]ActiveLoanStorageEntry) + assert.True(t, ok) + + *storageEntry = append(*storageEntry, testStorageEntry) + }). + Return(true, nil). + Once() + + res, err := api.GetActiveLoan(poolID, loanID) + assert.ErrorIs(t, err, ErrActiveLoanNotFound) + assert.Nil(t, res) +} + +func TestApi_GetClosedLoan(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + encodedLoanID, err := codec.Encode(loanID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ClosedLoanStorageName, encodedPoolID, encodedLoanID) + assert.NoError(t, err) + + testStorageEntry := loans.ClosedLoan{} + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + storageEntry, ok := args.Get(1).(*loans.ClosedLoan) + assert.True(t, ok) + + *storageEntry = testStorageEntry + }). + Return(true, nil). + Once() + + res, err := api.GetClosedLoan(poolID, loanID) + assert.NoError(t, err) + assert.Equal(t, &testStorageEntry, res) +} + +func TestApi_GetClosedLoan_InvalidPoolID(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(0) + loanID := types.U64(0) + + res, err := api.GetClosedLoan(poolID, loanID) + assert.ErrorIs(t, err, validation.ErrInvalidU64) + assert.Nil(t, res) +} + +func TestApi_GetClosedLoan_MetadataRetrievalError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + centAPIMock. + On("GetMetadataLatest"). + Return(nil, errors.New("error")). + Once() + + res, err := api.GetClosedLoan(poolID, loanID) + assert.ErrorIs(t, err, errors.ErrMetadataRetrieval) + assert.Nil(t, res) +} + +func TestApi_GetClosedLoan_StorageKeyCreationError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + var meta types.Metadata + + // NOTE - types.MetadataV14Data does not have info on the Loans pallet, + // causing types.CreateStorageKey to fail. + err := codec.DecodeFromHex(types.MetadataV14Data, &meta) + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(&meta, nil). + Once() + + res, err := api.GetClosedLoan(poolID, loanID) + assert.ErrorIs(t, err, errors.ErrStorageKeyCreation) + assert.Nil(t, res) +} + +func TestApi_GetClosedLoan_StorageRetrievalError(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + encodedLoanID, err := codec.Encode(loanID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ClosedLoanStorageName, encodedPoolID, encodedLoanID) + assert.NoError(t, err) + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + _, ok := args.Get(1).(*loans.ClosedLoan) + assert.True(t, ok) + }). + Return(false, errors.New("error")). + Once() + + res, err := api.GetClosedLoan(poolID, loanID) + assert.ErrorIs(t, err, ErrClosedLoanRetrieval) + assert.Nil(t, res) +} + +func TestApi_GetClosedLoan_StorageEntryNotFound(t *testing.T) { + centAPIMock := centchain.NewAPIMock(t) + + api := NewAPI(centAPIMock) + + poolID := types.U64(123) + loanID := types.U64(0) + + meta, err := testingutils.GetTestMetadata() + assert.NoError(t, err) + + centAPIMock. + On("GetMetadataLatest"). + Return(meta, nil). + Once() + + encodedPoolID, err := codec.Encode(poolID) + assert.NoError(t, err) + + encodedLoanID, err := codec.Encode(loanID) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(meta, PalletName, ClosedLoanStorageName, encodedPoolID, encodedLoanID) + assert.NoError(t, err) + + centAPIMock. + On("GetStorageLatest", storageKey, mock.Anything). + Run(func(args mock.Arguments) { + _, ok := args.Get(1).(*loans.ClosedLoan) + assert.True(t, ok) + }). + Return(false, nil). + Once() + + res, err := api.GetClosedLoan(poolID, loanID) + assert.ErrorIs(t, err, ErrClosedLoanNotFound) + assert.Nil(t, res) +}