Skip to content
This repository was archived by the owner on Mar 28, 2023. It is now read-only.

Commit 7e7def3

Browse files
authored
Merge pull request #1858 from OpenBazaar/1839-moderator-info-validation
(#1839) Validate moderator fee schedule
2 parents c2c89b3 + d9ae824 commit 7e7def3

File tree

11 files changed

+982
-18
lines changed

11 files changed

+982
-18
lines changed

api/jsonapi_test.go

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func TestProfile(t *testing.T) {
129129
})
130130
}
131131

132-
func TestProfileCurrencyUpdate(t *testing.T) {
132+
func TestPatchProfileCurrencyUpdate(t *testing.T) {
133133
var (
134134
postProfile = `{
135135
"handle": "test",
@@ -154,7 +154,7 @@ func TestProfileCurrencyUpdate(t *testing.T) {
154154
},
155155
"currencies": ["LTC"]
156156
}`
157-
putProfile = `{"currencies": ["ETH"]}`
157+
patchProfile = `{"currencies": ["ETH"]}`
158158
validateProfile = func(testRepo *test.Repository) error {
159159
m, err := schema.NewCustomSchemaManager(schema.SchemaContext{
160160
DataPath: testRepo.Path,
@@ -189,10 +189,76 @@ func TestProfileCurrencyUpdate(t *testing.T) {
189189

190190
runAPITestsWithSetup(t, apiTests{
191191
{"POST", "/ob/profile", postProfile, 200, anyResponseJSON},
192-
{"PATCH", "/ob/profile", putProfile, 200, anyResponseJSON},
192+
{"PATCH", "/ob/profile", patchProfile, 200, anyResponseJSON},
193193
}, nil, validateProfile)
194194
}
195195

196+
func TestPatchProfileCanBeInvalid(t *testing.T) {
197+
var (
198+
// init profile for patch
199+
postProfile = `{
200+
"handle": "test",
201+
"name": "Test User",
202+
"location": "Internet",
203+
"about": "The test fixture",
204+
"shortDescription": "Fixture",
205+
"contactInfo": {
206+
"website": "internet.com",
207+
"email": "[email protected]",
208+
"phoneNumber": "687-5309"
209+
},
210+
"nsfw": false,
211+
"vendor": false,
212+
"moderator": false,
213+
"colors": {
214+
"primary": "#000000",
215+
"secondary": "#FFD700",
216+
"text": "#ffffff",
217+
"highlight": "#123ABC",
218+
"highlightText": "#DEAD00"
219+
},
220+
"currencies": ["LTC"]
221+
}`
222+
// test valid patch
223+
patchProfile = `{
224+
"moderator": true,
225+
"moderatorInfo": {
226+
"description": "Fix plus percentage. Test moderator account. DO NOT USE.",
227+
"fee": {
228+
"feeType": "FIXED_PLUS_PERCENTAGE",
229+
"fixedFee": {
230+
"amountCurrency": {
231+
"code": "USD",
232+
"divisibility": 2
233+
},
234+
"bigAmount": "2"
235+
},
236+
"percentage": 0.1
237+
},
238+
"languages": [
239+
"en-US"
240+
],
241+
"termsAndConditions": "Test moderator account. DO NOT USE."
242+
}
243+
}`
244+
// test invalid patch: percentage must be greater than 0
245+
invalidPatchProfile = `{
246+
"moderatorInfo": {
247+
"fee": {
248+
"percentage": -1
249+
}
250+
}
251+
}`
252+
)
253+
254+
expectedErr := fmt.Errorf("invalid profile: %s", repo.ErrModeratorFeeHasNegativePercentage)
255+
runAPITests(t, apiTests{
256+
{"POST", "/ob/profile", postProfile, 200, anyResponseJSON},
257+
{"PATCH", "/ob/profile", patchProfile, 200, anyResponseJSON},
258+
{"PATCH", "/ob/profile", invalidPatchProfile, 500, errorResponseJSON(expectedErr)},
259+
})
260+
}
261+
196262
func TestAvatar(t *testing.T) {
197263
// Setting an avatar fails if we don't have a profile
198264
runAPITests(t, apiTests{

core/moderation.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,12 @@ func (n *OpenBazaarNode) GetModeratorFee(transactionTotal *big.Int, txCurrencyCo
221221
}
222222
f := big.NewFloat(float64(profile.ModeratorInfo.Fee.Percentage))
223223
f.Mul(f, big.NewFloat(0.01))
224-
t.Mul(t, f)
225-
total, _ := t.Int(transactionTotal)
226-
if fixed.Add(fixed, total).Cmp(transactionTotal) > 0 {
224+
percentAmt, _ := new(big.Float).Mul(t, f).Int(nil)
225+
feeTotal := new(big.Int).Add(fixed, percentAmt)
226+
if feeTotal.Cmp(transactionTotal) > 0 {
227227
return big.NewInt(0), errors.New("Fixed moderator fee exceeds transaction amount")
228228
}
229-
return fixed.Add(fixed, total), nil
229+
return feeTotal, nil
230230
default:
231231
return big.NewInt(0), errors.New("Unrecognized fee type")
232232
}

core/profile.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ func (n *OpenBazaarNode) PatchProfile(patch map[string]interface{}) error {
204204
return err
205205
}
206206

207-
// Execute UpdateProfile with new profile
208207
newProfile, err := json.Marshal(patch)
209208
if err != nil {
210209
return err
@@ -213,6 +212,16 @@ func (n *OpenBazaarNode) PatchProfile(patch map[string]interface{}) error {
213212
if err := jsonpb.Unmarshal(bytes.NewReader(newProfile), p); err != nil {
214213
return err
215214
}
215+
216+
repoProfile, err := repo.ProfileFromProtobuf(p)
217+
if err != nil {
218+
return fmt.Errorf("building profile for validation: %s", err.Error())
219+
}
220+
221+
if err := repoProfile.Valid(); err != nil {
222+
return fmt.Errorf("invalid profile: %s", err.Error())
223+
}
224+
216225
return n.UpdateProfile(p)
217226
}
218227

repo/currency.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ const (
1717
)
1818

1919
var (
20-
ErrCurrencyValueInsufficientPrecision = errors.New("unable to accurately represent value as int64")
21-
ErrCurrencyValueNegativeRate = errors.New("conversion rate must be greater than zero")
22-
ErrCurrencyValueAmountInvalid = errors.New("invalid amount")
23-
ErrCurrencyValueDefinitionInvalid = errors.New("invalid currency definition")
20+
ErrCurrencyValueInsufficientPrecision = errors.New("unable to accurately represent value as int64")
21+
ErrCurrencyValueNegativeRate = errors.New("conversion rate must be greater than zero")
22+
ErrCurrencyValueAmountInvalid = errors.New("invalid amount")
23+
ErrCurrencyValueDefinitionInvalid = errors.New("invalid currency definition")
24+
ErrCurrencyValueInvalidCmpDifferentCurrencies = errors.New("unable to compare two different currencies")
2425
)
2526

2627
// CurrencyValue represents the amount and variety of currency
@@ -245,3 +246,26 @@ func (v *CurrencyValue) ConvertTo(final CurrencyDefinition, exchangeRatio float6
245246
roundedAcc := big.Accuracy(roundedAmount.Cmp(newAmount))
246247
return &CurrencyValue{Amount: roundedInt, Currency: final}, roundedAcc, nil
247248
}
249+
250+
// Cmp exposes the (*big.Int).Cmp behavior after verifying currency and adjusting
251+
// for different currency divisibilities.
252+
func (v *CurrencyValue) Cmp(other *CurrencyValue) (int, error) {
253+
if v.Currency.Code.String() != other.Currency.Code.String() {
254+
return 0, ErrCurrencyValueInvalidCmpDifferentCurrencies
255+
}
256+
if v.Currency.Equal(other.Currency) {
257+
return v.Amount.Cmp(other.Amount), nil
258+
}
259+
if v.Currency.Divisibility > other.Currency.Divisibility {
260+
adjOther, _, err := other.AdjustDivisibility(v.Currency.Divisibility)
261+
if err != nil {
262+
return 0, fmt.Errorf("adjusting other divisibility: %s", err.Error())
263+
}
264+
return v.Amount.Cmp(adjOther.Amount), nil
265+
}
266+
selfAdj, _, err := v.AdjustDivisibility(other.Currency.Divisibility)
267+
if err != nil {
268+
return 0, fmt.Errorf("adjusting self divisibility: %s", err.Error())
269+
}
270+
return selfAdj.Amount.Cmp(other.Amount), nil
271+
}

repo/currency_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,3 +483,51 @@ func TestCurrencyValueAdjustDivisibility(t *testing.T) {
483483
}
484484
}
485485
}
486+
487+
func TestCurrencyValueCmp(t *testing.T) {
488+
var examples = []struct {
489+
subject *repo.CurrencyValue
490+
other *repo.CurrencyValue
491+
expected int
492+
expectedErr error
493+
}{
494+
{ // success case
495+
subject: factory.MustNewCurrencyValue("1234", "USD"),
496+
other: factory.MustNewCurrencyValue("12345", "USD"),
497+
expected: -1, // subject.Amount.Cmp(other.Amount) == -1
498+
},
499+
{ // different divisibilities handled
500+
subject: factory.MustNewCurrencyValueUsingDiv("1234", "USD", 2),
501+
other: factory.MustNewCurrencyValueUsingDiv("123456", "USD", 4),
502+
expected: -1, // subject.AdjustDivisibility(4).Amount.Cmp(other.Amount) == -1
503+
},
504+
{ // different divisibilities (reverse suject/other) handled
505+
subject: factory.MustNewCurrencyValueUsingDiv("123456", "USD", 4),
506+
other: factory.MustNewCurrencyValueUsingDiv("1234", "USD", 2),
507+
expected: 1, // subject.AdjustDivisibility(4).Amount.Cmp(other.Amount) == 1
508+
},
509+
{ // different currencies return error
510+
subject: factory.MustNewCurrencyValue("1234", "USD"),
511+
other: factory.MustNewCurrencyValue("1234", "NOTUSD"),
512+
expectedErr: repo.ErrCurrencyValueInvalidCmpDifferentCurrencies,
513+
},
514+
}
515+
516+
for _, e := range examples {
517+
t.Logf("subject (%+v), other (%+v)", e.subject, e.other)
518+
actual, err := e.subject.Cmp(e.other)
519+
if e.expectedErr != nil {
520+
if err != e.expectedErr {
521+
t.Errorf("expected err (%s), but was (%s)", e.expectedErr.Error(), err.Error())
522+
}
523+
} else {
524+
if err != nil {
525+
t.Errorf("unexpected err: %s", err)
526+
continue
527+
}
528+
}
529+
if e.expected != actual {
530+
t.Errorf("expected comparison result (%d), but was (%d)", e.expected, actual)
531+
}
532+
}
533+
}

repo/dispute_resolution.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ import (
1010
// turns them into their v5 counterpart.
1111
func ToV5DisputeResolution(disputeResolution *pb.DisputeResolution) *pb.DisputeResolution {
1212
newDisputeResolution := proto.Clone(disputeResolution).(*pb.DisputeResolution)
13+
if disputeResolution.Payout == nil {
14+
return newDisputeResolution
15+
}
1316

14-
if disputeResolution.Payout.BuyerOutput.Amount != 0 && disputeResolution.Payout.BuyerOutput.BigAmount == "" {
17+
if disputeResolution.Payout.BuyerOutput != nil &&
18+
disputeResolution.Payout.BuyerOutput.Amount != 0 &&
19+
disputeResolution.Payout.BuyerOutput.BigAmount == "" {
1520
newDisputeResolution.Payout.BuyerOutput.BigAmount = big.NewInt(int64(disputeResolution.Payout.BuyerOutput.Amount)).String()
1621
newDisputeResolution.Payout.BuyerOutput.Amount = 0
1722
}
18-
if disputeResolution.Payout.VendorOutput.Amount != 0 && disputeResolution.Payout.VendorOutput.BigAmount == "" {
23+
if disputeResolution.Payout.VendorOutput != nil &&
24+
disputeResolution.Payout.VendorOutput.Amount != 0 &&
25+
disputeResolution.Payout.VendorOutput.BigAmount == "" {
1926
newDisputeResolution.Payout.VendorOutput.BigAmount = big.NewInt(int64(disputeResolution.Payout.VendorOutput.Amount)).String()
2027
newDisputeResolution.Payout.VendorOutput.Amount = 0
2128
}
22-
if disputeResolution.Payout.ModeratorOutput.Amount != 0 && disputeResolution.Payout.ModeratorOutput.BigAmount == "" {
29+
if disputeResolution.Payout.ModeratorOutput != nil &&
30+
disputeResolution.Payout.ModeratorOutput.Amount != 0 &&
31+
disputeResolution.Payout.ModeratorOutput.BigAmount == "" {
2332
newDisputeResolution.Payout.ModeratorOutput.BigAmount = big.NewInt(int64(disputeResolution.Payout.ModeratorOutput.Amount)).String()
2433
newDisputeResolution.Payout.ModeratorOutput.Amount = 0
2534
}

repo/dispute_resolution_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
package repo
1+
package repo_test
22

33
import (
4-
"github.com/OpenBazaar/openbazaar-go/pb"
54
"testing"
5+
6+
"github.com/OpenBazaar/openbazaar-go/pb"
7+
"github.com/OpenBazaar/openbazaar-go/repo"
68
)
79

810
func TestToV5DisputeResolution(t *testing.T) {
@@ -20,7 +22,7 @@ func TestToV5DisputeResolution(t *testing.T) {
2022
},
2123
},
2224
}
23-
newDisputeResolution = ToV5DisputeResolution(disputeResolution)
25+
newDisputeResolution = repo.ToV5DisputeResolution(disputeResolution)
2426
expected = "10000"
2527
)
2628

@@ -44,3 +46,52 @@ func TestToV5DisputeResolution(t *testing.T) {
4446
t.Errorf("Expected Amount of 0, got %d", newDisputeResolution.Payout.ModeratorOutput.Amount)
4547
}
4648
}
49+
50+
func TestToV5DisputeResolutionHandlesMissingOutputs(t *testing.T) {
51+
var (
52+
examples = []*pb.DisputeResolution{
53+
{ // missing BuyerOutput
54+
Payout: &pb.DisputeResolution_Payout{
55+
BuyerOutput: nil,
56+
VendorOutput: &pb.DisputeResolution_Payout_Output{
57+
Amount: 10000,
58+
},
59+
ModeratorOutput: &pb.DisputeResolution_Payout_Output{
60+
Amount: 10000,
61+
},
62+
},
63+
},
64+
{ // missing VendorOutput
65+
Payout: &pb.DisputeResolution_Payout{
66+
BuyerOutput: &pb.DisputeResolution_Payout_Output{
67+
Amount: 10000,
68+
},
69+
VendorOutput: nil,
70+
ModeratorOutput: &pb.DisputeResolution_Payout_Output{
71+
Amount: 10000,
72+
},
73+
},
74+
},
75+
{ // missing ModeratorOutput
76+
Payout: &pb.DisputeResolution_Payout{
77+
BuyerOutput: &pb.DisputeResolution_Payout_Output{
78+
Amount: 10000,
79+
},
80+
VendorOutput: &pb.DisputeResolution_Payout_Output{
81+
Amount: 10000,
82+
},
83+
ModeratorOutput: nil,
84+
},
85+
},
86+
}
87+
)
88+
89+
for _, e := range examples {
90+
repo.ToV5DisputeResolution(e)
91+
}
92+
}
93+
94+
func TestToV5DisputeResolutionHandlesMissingPayout(t *testing.T) {
95+
var example = &pb.DisputeResolution{Payout: nil}
96+
repo.ToV5DisputeResolution(example)
97+
}

0 commit comments

Comments
 (0)