diff --git a/cip20.go b/cip20.go index f070643..f1aaf9a 100644 --- a/cip20.go +++ b/cip20.go @@ -14,10 +14,29 @@ package models +import "github.com/go-playground/validator/v10" + type Cip20Metadata struct { - Num674 Num674 `cbor:"674,keyasint" json:"674"` + Num674 Num674 `cbor:"674,keyasint" json:"674" validate:"required"` } type Num674 struct { - Msg []string `cbor:"msg" json:"msg"` + Msg []string `cbor:"msg" json:"msg" validate:"required,gt=0,dive,max=64"` +} + +func NewCip20Metadata(messages []string) (*Cip20Metadata, error) { + validate := validator.New() + + metadata := &Cip20Metadata{Num674: Num674{Msg: messages}} + + if err := validate.Struct(metadata); err != nil { + return nil, err + } + + return metadata, nil +} + +func (c *Cip20Metadata) Validate() error { + validate := validator.New() + return validate.Struct(c) } diff --git a/cip20_test.go b/cip20_test.go new file mode 100644 index 0000000..716bdea --- /dev/null +++ b/cip20_test.go @@ -0,0 +1,257 @@ +package models + +import ( + "encoding/hex" + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/fxamacker/cbor/v2" +) + +func TestValidCip20Metadata(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cborHex string + expectedObj Cip20Metadata + jsonData string + expectCBORUnmarshalError bool + expectJSONUnmarshalError bool + expectValidationError bool + expectCBORDeepEqualError bool + expectJSONDeepEqualError bool + }{ + { + name: "Valid - Single Message", + expectedObj: Cip20Metadata{ + Num674: Num674{ + Msg: []string{"This is a comment for the transaction xyz, thank you very much!"}, + }, + }, + cborHex: "A163363734A1636D736781783F54686973206973206120636F6D6D656E7420666F7220746865207472616E73616374696F6E2078797A2C207468616E6B20796F752076657279206D75636821", + jsonData: `{ + "674": + { + "msg": + [ + "This is a comment for the transaction xyz, thank you very much!" + ] + } + }`, + expectCBORUnmarshalError: false, + expectJSONUnmarshalError: false, + expectValidationError: false, + expectCBORDeepEqualError: false, + expectJSONDeepEqualError: false, + }, + { + name: "Valid - Multiple Messages", + expectedObj: Cip20Metadata{ + Num674: Num674{ + Msg: []string{ + "Invoice-No: 1234567890", + "Customer-No: 555-1234", + "P.S.: i will shop again at your store :-)", + }, + }, + }, + jsonData: `{ + "674": + { + "msg": + [ + "Invoice-No: 1234567890", + "Customer-No: 555-1234", + "P.S.: i will shop again at your store :-)" + ] + } + }`, + cborHex: "A163363734A1636D73678376496E766F6963652D4E6F3A203132333435363738393075437573746F6D65722D4E6F3A203535352D313233347829502E532E3A20692077696C6C2073686F7020616761696E20617420796F75722073746F7265203A2D29", + expectCBORUnmarshalError: false, + expectJSONUnmarshalError: false, + expectValidationError: false, + expectCBORDeepEqualError: false, + expectJSONDeepEqualError: false, + }, + { + name: "Invalid - Message Exceeds 64 Bytes", + expectedObj: Cip20Metadata{ + Num674: Num674{ + Msg: []string{strings.Repeat("a", 65)}, // 65 'a's to exceed the limit + }, + }, + jsonData: `{ + "674": { + "msg": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + } + }`, + cborHex: "A163363734A1636D73678178416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + expectCBORUnmarshalError: false, + expectJSONUnmarshalError: false, + expectValidationError: true, + expectCBORDeepEqualError: false, + expectJSONDeepEqualError: false, + }, + { + name: "Invalid - Message Deep error", + expectedObj: Cip20Metadata{ + Num674: Num674{ + Msg: []string{strings.Repeat("b", 65)}, + }, + }, + jsonData: `{ + "674": { + "msg": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + } + }`, + cborHex: "A163363734A1636D73678178416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + expectCBORUnmarshalError: false, + expectJSONUnmarshalError: false, + expectValidationError: true, + expectCBORDeepEqualError: true, + expectJSONDeepEqualError: true, + }, + { + name: "Invalid - Message Deep error 675", + expectedObj: Cip20Metadata{ + Num674: Num674{ + Msg: []string{strings.Repeat("b", 65)}, + }, + }, + jsonData: `{ + "675": { + "msg": ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + } + }`, + cborHex: "A163363734A1636D73678178416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + expectCBORUnmarshalError: false, + expectJSONUnmarshalError: false, + expectValidationError: true, + expectCBORDeepEqualError: true, + expectJSONDeepEqualError: true, + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable for goroutines + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // Decode the CBOR hex string + cborData, err := hex.DecodeString(tc.cborHex) + if err != nil { + t.Errorf("failed to decode CBOR hex string for %s, error: %v", tc.name, err) + } + + // CBOR Unmarshal + var decodedMetadata Cip20Metadata + err = cbor.Unmarshal(cborData, &decodedMetadata) + if tc.expectCBORUnmarshalError { + if err == nil { + t.Errorf("expected CBOR unmarshal error but got none for test: %s", tc.name) + } + } else { + if err != nil { + t.Errorf("did not expect CBOR unmarshal error but got one for test: %s, error: %v", tc.name, err) + } + } + + // CBOR Validate + err = decodedMetadata.Validate() + if tc.expectValidationError { + if err == nil { + t.Errorf("expected validation error but got none for test: %s", tc.name) + } + } else { + if err != nil { + t.Errorf("did not expect validation error but got one for test: %s, error: %v", tc.name, err) + } + } + + // Deep Equality Check for CBOR + deepEqual := reflect.DeepEqual(decodedMetadata, tc.expectedObj) + if tc.expectCBORDeepEqualError && deepEqual { + t.Errorf("Cbor expected deep equal error but objects are identical for test: %s", tc.name) + } + if !tc.expectCBORDeepEqualError && !deepEqual { + t.Errorf("Cbor expected objects to be identical but they are not for test: %s, expected: %v, got: %v", tc.name, tc.expectedObj, decodedMetadata) + } + + // Reset the object for JSON testing + decodedMetadata = Cip20Metadata{} + // Decode the JSON string + err = json.Unmarshal([]byte(tc.jsonData), &decodedMetadata) + if err != nil { + t.Errorf("unexpected result unmarshaling JSON to Cip20Metadata for test %s, error: %v", tc.name, err) + } + if tc.expectJSONUnmarshalError { + if err == nil { + t.Errorf("expected JSON unmarshal error but got none for test: %s", tc.name) + } + } else { + if err != nil { + t.Errorf("did not expect JSON unmarshal error but got one for test: %s, error: %v", tc.name, err) + } + } + + // JSON Validate + err = decodedMetadata.Validate() + if tc.expectValidationError { + if err == nil { + t.Errorf("expected validation error but got none for test: %s", tc.name) + } + } else { + if err != nil { + t.Errorf("did not expect validation error but got one for test: %s, error: %v", tc.name, err) + } + } + + // Deep Equality Check for JSON + deepEqual = reflect.DeepEqual(decodedMetadata, tc.expectedObj) + if tc.expectJSONDeepEqualError && deepEqual { + t.Errorf("Json expected deep equal error but objects are identical for test: %s", tc.name) + } + if !tc.expectJSONDeepEqualError && !deepEqual { + t.Errorf("Json expected objects to be identical but they are not for test: %s, expected: %v, got: %v", tc.name, tc.expectedObj, decodedMetadata) + } + }) + } +} + +func TestNewCip20Metadata(t *testing.T) { + t.Parallel() + // Test case: Non-empty messages + messages := []string{"message1", "message2"} + expectedJSON := `{"674":{"msg":["message1","message2"]}}` + + metadata, err := NewCip20Metadata(messages) + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + + actualJSON, err := json.Marshal(metadata) + if err != nil { + t.Errorf("Failed to marshal metadata to JSON: %v", err) + } + + if string(actualJSON) != expectedJSON { + t.Errorf("Expected JSON: %s, but got: %s", expectedJSON, string(actualJSON)) + } + + // Test case: Invalid metadata + messages = []string{} + expectedErr := "Key: 'Cip20Metadata.Num674.Msg' Error:Field validation for 'Msg' failed on the 'gt' tag" + + _, err = NewCip20Metadata(messages) + if err == nil { + t.Errorf("Expected validation error, but got no error") + } else { + actualErr := err.Error() + if actualErr != expectedErr { + t.Errorf("Expected error: %s, but got: %s", expectedErr, actualErr) + } + } +} diff --git a/go.mod b/go.mod index c4310f1..f4284b3 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,21 @@ module github.com/blinklabs-io/cardano-models go 1.20 -require github.com/blinklabs-io/gouroboros v0.67.1 +require ( + github.com/blinklabs-io/gouroboros v0.67.1 + github.com/fxamacker/cbor/v2 v2.5.0 + github.com/go-playground/validator/v10 v10.16.0 +) require ( - github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/jinzhu/copier v0.4.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 5bc834c..aded3c1 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,43 @@ github.com/blinklabs-io/gouroboros v0.67.1 h1:SyzxCIEa2rph3KQr1D+PgqsPNFORN/OdwNtrL0h42g8= github.com/blinklabs-io/gouroboros v0.67.1/go.mod h1:Q154NJPs7gB93Tggt8ts9RGIlW2kkknr6M90Q3jh0s4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=