diff --git a/assets/assetList.go b/assets/assetList.go index 2babd41..3e3e009 100644 --- a/assets/assetList.go +++ b/assets/assetList.go @@ -1,4 +1,9 @@ package assets +import "time" + // assetTypeList is the list which should contain all defined asset types var assetTypeList = []AssetType{} + +// listUpdateTime is the last time the assetTypeList was updated +var listUpdateTime time.Time diff --git a/assets/assetListFuncs.go b/assets/assetListFuncs.go index cb165f4..e255deb 100644 --- a/assets/assetListFuncs.go +++ b/assets/assetListFuncs.go @@ -1,5 +1,13 @@ package assets +import ( + "net/http" + "time" + + "github.com/goledgerdev/cc-tools/errors" + sw "github.com/goledgerdev/cc-tools/stubwrapper" +) + // AssetTypeList returns a copy of the assetTypeList variable. func AssetTypeList() []AssetType { listCopy := make([]AssetType, len(assetTypeList)) @@ -19,5 +27,194 @@ func FetchAssetType(assetTypeTag string) *AssetType { // InitAssetList appends custom assets to assetTypeList to avoid initialization loop. func InitAssetList(l []AssetType) { + if GetEnabledDynamicAssetType() { + l = append(l, GetListAssetType()) + } + assetTypeList = l +} + +// ReplaceAssetList replace assetTypeList to for a new one +func ReplaceAssetList(l []AssetType) { assetTypeList = l } + +// UpdateAssetList updates the assetTypeList variable on runtime +func UpdateAssetList(l []AssetType) { + assetTypeList = append(assetTypeList, l...) +} + +// RemoveAssetType removes an asset type from an AssetType List and returns the new list +func RemoveAssetType(tag string, l []AssetType) []AssetType { + for i, assetType := range l { + if assetType.Tag == tag { + l = append(l[:i], l[i+1:]...) + } + } + return l +} + +// ReplaceAssetType replaces an asset type from an AssetType List with an updated version and returns the new list +// This function does not automatically update the assetTypeList variable +func ReplaceAssetType(assetType AssetType, l []AssetType) []AssetType { + for i, v := range l { + if v.Tag == assetType.Tag { + l[i] = assetType + } + } + return l +} + +// StoreAssetList stores the current assetList on the blockchain +func StoreAssetList(stub *sw.StubWrapper) errors.ICCError { + assetList := AssetTypeList() + l := ArrayFromAssetTypeList(assetList) + + txTimestamp, err := stub.Stub.GetTxTimestamp() + if err != nil { + return errors.WrapError(err, "failed to get tx timestamp") + } + txTime := txTimestamp.AsTime() + txTimeStr := txTime.Format(time.RFC3339) + + listKey, err := NewKey(map[string]interface{}{ + "@assetType": "assetTypeListData", + "id": "primary", + }) + if err != nil { + return errors.NewCCError("error getting asset list key", http.StatusInternalServerError) + } + + exists, err := listKey.ExistsInLedger(stub) + if err != nil { + return errors.NewCCError("error checking if asset list exists", http.StatusInternalServerError) + } + + if exists { + listAsset, err := listKey.Get(stub) + if err != nil { + return errors.WrapError(err, "error getting asset list") + } + listMap := (map[string]interface{})(*listAsset) + + listMap["list"] = l + listMap["lastUpdated"] = txTimeStr + + _, err = listAsset.Update(stub, listMap) + if err != nil { + return errors.WrapError(err, "error updating asset list") + } + } else { + listMap := map[string]interface{}{ + "@assetType": "assetTypeListData", + "id": "primary", + "list": l, + "lastUpdated": txTimeStr, + } + + listAsset, err := NewAsset(listMap) + if err != nil { + return errors.WrapError(err, "error creating asset list") + } + + _, err = listAsset.PutNew(stub) + if err != nil { + return errors.WrapError(err, "error putting asset list") + } + } + + SetAssetListUpdateTime(txTime) + + return nil +} + +// RestoreAssetList restores the assetList from the blockchain +func RestoreAssetList(stub *sw.StubWrapper, init bool) errors.ICCError { + listKey, err := NewKey(map[string]interface{}{ + "@assetType": "assetTypeListData", + "id": "primary", + }) + if err != nil { + return errors.NewCCError("error gettin asset list key", http.StatusInternalServerError) + } + + exists, err := listKey.ExistsInLedger(stub) + if err != nil { + return errors.NewCCError("error checking if asset list exists", http.StatusInternalServerError) + } + + if exists { + listAsset, err := listKey.Get(stub) + if err != nil { + return errors.NewCCError("error getting asset list", http.StatusInternalServerError) + } + listMap := (map[string]interface{})(*listAsset) + + txTime := listMap["lastUpdated"].(time.Time) + + if GetAssetListUpdateTime().After(txTime) { + return nil + } + + l := AssetTypeListFromArray(listMap["list"].([]interface{})) + + l = getRestoredList(l, init) + + ReplaceAssetList(l) + + SetAssetListUpdateTime(txTime) + } + + return nil +} + +// getRestoredList reconstructs the assetTypeList from the stored list comparing to the current list +func getRestoredList(storedList []AssetType, init bool) []AssetType { + assetList := AssetTypeList() + + deleteds := AssetTypeList() + + for _, assetTypeStored := range storedList { + if !assetTypeStored.Dynamic { + continue + } + + exists := false + for i, assetType := range assetList { + if assetType.Tag == assetTypeStored.Tag { + exists = true + + assetTypeStored.Validate = assetType.Validate + assetList[i] = assetTypeStored + + deleteds = RemoveAssetType(assetType.Tag, deleteds) + + break + } + } + if !exists { + assetList = append(assetList, assetTypeStored) + } + } + + if !init { + for _, deleted := range deleteds { + if !deleted.Dynamic { + continue + } + + assetList = RemoveAssetType(deleted.Tag, assetList) + } + } + + return assetList +} + +// GetAssetListUpdateTime returns the last time the asset list was updated +func GetAssetListUpdateTime() time.Time { + return listUpdateTime +} + +// SetAssetListUpdateTime sets the last time the asset list was updated +func SetAssetListUpdateTime(t time.Time) { + listUpdateTime = t +} diff --git a/assets/assetProp.go b/assets/assetProp.go index c01c64e..a5b77f6 100644 --- a/assets/assetProp.go +++ b/assets/assetProp.go @@ -43,3 +43,84 @@ type AssetProp struct { // Validate is a function called when validating property format. Validate func(interface{}) error `json:"-"` } + +// ToMap converts an AssetProp to a map[string]interface{} +func (p AssetProp) ToMap() map[string]interface{} { + return map[string]interface{}{ + "tag": p.Tag, + "label": p.Label, + "description": p.Description, + "isKey": p.IsKey, + "required": p.Required, + "readOnly": p.ReadOnly, + "defaultValue": p.DefaultValue, + "dataType": p.DataType, + "writers": p.Writers, + } +} + +// AssetPropFromMap converts a map[string]interface{} to an AssetProp +func AssetPropFromMap(m map[string]interface{}) AssetProp { + description, ok := m["description"].(string) + if !ok { + description = "" + } + label, ok := m["label"].(string) + if !ok { + label = "" + } + isKey, ok := m["isKey"].(bool) + if !ok { + isKey = false + } + required, ok := m["required"].(bool) + if !ok { + required = false + } + readOnly, ok := m["readOnly"].(bool) + if !ok { + readOnly = false + } + + res := AssetProp{ + Tag: m["tag"].(string), + Label: label, + Description: description, + IsKey: isKey, + Required: required, + ReadOnly: readOnly, + DefaultValue: m["defaultValue"], + DataType: m["dataType"].(string), + } + + writers := make([]string, 0) + writersArr, ok := m["writers"].([]interface{}) + if ok { + for _, w := range writersArr { + writers = append(writers, w.(string)) + } + } + if len(writers) > 0 { + res.Writers = writers + } + + return res +} + +// ArrayFromAssetPropList converts an array of AssetProp to an array of map[string]interface +func ArrayFromAssetPropList(a []AssetProp) []map[string]interface{} { + list := []map[string]interface{}{} + for _, m := range a { + list = append(list, m.ToMap()) + } + return list +} + +// AssetPropListFromArray converts an array of map[string]interface to an array of AssetProp +func AssetPropListFromArray(a []interface{}) []AssetProp { + list := []AssetProp{} + for _, m := range a { + list = append(list, AssetPropFromMap(m.(map[string]interface{}))) + } + return list +} diff --git a/assets/assetType.go b/assets/assetType.go index 5727a49..e73a8dc 100644 --- a/assets/assetType.go +++ b/assets/assetType.go @@ -27,6 +27,9 @@ type AssetType struct { // Validate is a function called when validating asset as a whole. Validate func(Asset) error `json:"-"` + + // Dynamic is a flag that indicates if the asset type is dynamic. + Dynamic bool `json:"dynamic,omitempty"` } // Keys returns a list of asset properties which are defined as primary keys. (IsKey == true) @@ -77,3 +80,68 @@ func (t AssetType) GetPropDef(propTag string) *AssetProp { func (t AssetType) IsPrivate() bool { return len(t.Readers) > 0 } + +// ToMap returns a map representation of the asset type. +func (t AssetType) ToMap() map[string]interface{} { + return map[string]interface{}{ + "tag": t.Tag, + "label": t.Label, + "description": t.Description, + "props": ArrayFromAssetPropList(t.Props), + "readers": t.Readers, + "dynamic": t.Dynamic, + } +} + +// AssetTypeFromMap returns an asset type from a map representation. +func AssetTypeFromMap(m map[string]interface{}) AssetType { + label, ok := m["label"].(string) + if !ok { + label = "" + } + description, ok := m["description"].(string) + if !ok { + description = "" + } + dynamic, ok := m["dynamic"].(bool) + if !ok { + dynamic = false + } + + res := AssetType{ + Tag: m["tag"].(string), + Label: label, + Description: description, + Props: AssetPropListFromArray(m["props"].([]interface{})), + Dynamic: dynamic, + } + + readers := make([]string, 0) + readersArr, ok := m["readers"].([]interface{}) + if ok { + for _, r := range readersArr { + readers = append(readers, r.(string)) + } + } + if len(readers) > 0 { + res.Readers = readers + } + + return res +} + +// ArrayFromAssetTypeList converts an array of AssetType to an array of map[string]interface +func ArrayFromAssetTypeList(assetTypes []AssetType) (array []map[string]interface{}) { + for _, assetType := range assetTypes { + array = append(array, assetType.ToMap()) + } + return +} + +// AssetTypeListFromArray converts an array of map[string]interface to an array of AssetType +func AssetTypeListFromArray(array []interface{}) (assetTypes []AssetType) { + for _, v := range array { + assetTypes = append(assetTypes, AssetTypeFromMap(v.(map[string]interface{}))) + } + return +} diff --git a/assets/dynamicAssetType.go b/assets/dynamicAssetType.go new file mode 100644 index 0000000..138dfaf --- /dev/null +++ b/assets/dynamicAssetType.go @@ -0,0 +1,15 @@ +package assets + +// DynamicAssetType is the configuration for the Dynamic AssetTypes +type DynamicAssetType struct { + // Enabled defines whether the Dynamic AssetTypes feature is active + Enabled bool `json:"enabled"` + + // AssetAdmins is an array that specifies which organizations can operate the Dynamic AssetTyper feature. + // Accepts either basic strings for exact matches + // eg. []string{'org1MSP', 'org2MSP'} + // or regular expressions + // eg. []string{`$org\dMSP`} and cc-tools will + // check for a match with regular expression `org\dMSP` + AssetAdmins []string `json:"assetAdmins"` +} diff --git a/assets/dynamicAssetTypeConfig.go b/assets/dynamicAssetTypeConfig.go new file mode 100644 index 0000000..4e61aa9 --- /dev/null +++ b/assets/dynamicAssetTypeConfig.go @@ -0,0 +1,54 @@ +package assets + +// dynamicAssetTypeConfig is the configuration data for the Dynamic assetTypes feature +var dynamicAssetTypeConfig = DynamicAssetType{} + +// InitDynamicAssetTypeConfig initilizes the dynamicAssetTypeConfig variable +func InitDynamicAssetTypeConfig(c DynamicAssetType) { + dynamicAssetTypeConfig = c +} + +// GetEnabledDynamicAssetType returns the value of the Enabled field +func GetEnabledDynamicAssetType() bool { + return dynamicAssetTypeConfig.Enabled +} + +// GetAssetAdminsDynamicAssetType returns the value of the AssetAdmins field +func GetAssetAdminsDynamicAssetType() []string { + return dynamicAssetTypeConfig.AssetAdmins +} + +// GetListAssetType returns the Dynamic AssetType meta type +func GetListAssetType() AssetType { + var AssetTypeListData = AssetType{ + Tag: "assetTypeListData", + Label: "AssetTypeListData", + Description: "AssetTypeListData", + + Props: []AssetProp{ + { + Required: true, + IsKey: true, + Tag: "id", + Label: "ID", + DataType: "string", + Writers: dynamicAssetTypeConfig.AssetAdmins, + }, + { + Required: true, + Tag: "list", + Label: "List", + DataType: "[]@object", + Writers: dynamicAssetTypeConfig.AssetAdmins, + }, + { + Required: true, + Tag: "lastUpdated", + Label: "Last Updated", + DataType: "datetime", + Writers: dynamicAssetTypeConfig.AssetAdmins, + }, + }, + } + return AssetTypeListData +} diff --git a/assets/dynamicAssetTypeFuncs.go b/assets/dynamicAssetTypeFuncs.go new file mode 100644 index 0000000..4419fbd --- /dev/null +++ b/assets/dynamicAssetTypeFuncs.go @@ -0,0 +1,223 @@ +package assets + +import ( + "fmt" + "net/http" + "strings" + + "github.com/goledgerdev/cc-tools/errors" +) + +// BuildAssetProp builds an AssetProp from an object with the required fields +func BuildAssetProp(propMap map[string]interface{}, newTypesList []interface{}) (AssetProp, errors.ICCError) { + // Tag + tagValue, err := CheckValue(propMap["tag"], true, "string", "tag") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid tag value") + } + + // Label + labelValue, err := CheckValue(propMap["label"], true, "string", "label") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid label value") + } + + // Description + descriptionValue, err := CheckValue(propMap["description"], false, "string", "description") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid description value") + } + + // Required + requiredValue, err := CheckValue(propMap["required"], false, "boolean", "required") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid required value") + } + + // IsKey + isKeyValue, err := CheckValue(propMap["isKey"], false, "boolean", "isKey") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid isKey value") + } + + // ReadOnly + readOnlyValue, err := CheckValue(propMap["readOnly"], false, "boolean", "readOnly") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid readOnly value") + } + + // DataType + dataTypeValue, err := CheckValue(propMap["dataType"], true, "string", "dataType") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid dataType value") + } + err = CheckDataType(dataTypeValue.(string), newTypesList) + if err != nil { + return AssetProp{}, errors.WrapError(err, "failed checking data type") + } + + assetProp := AssetProp{ + Tag: tagValue.(string), + Label: labelValue.(string), + Description: descriptionValue.(string), + Required: requiredValue.(bool), + IsKey: isKeyValue.(bool), + ReadOnly: readOnlyValue.(bool), + DataType: dataTypeValue.(string), + } + + // Writers + writers := make([]string, 0) + writersArr, ok := propMap["writers"].([]interface{}) + if ok { + for _, writer := range writersArr { + writerValue, err := CheckValue(writer, false, "string", "writer") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid writer value") + } + + writers = append(writers, writerValue.(string)) + } + } + if len(writers) > 0 { + assetProp.Writers = writers + } + + // Validate Default Value + if propMap["defaultValue"] != nil { + defaultValue, err := validateProp(propMap["defaultValue"], assetProp) + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid Default Value") + } + + assetProp.DefaultValue = defaultValue + } + + return assetProp, nil +} + +// HandlePropUpdate updates an AssetProp with the values of the propMap +func HandlePropUpdate(assetProps AssetProp, propMap map[string]interface{}) (AssetProp, errors.ICCError) { + handleDefaultValue := false + for k, v := range propMap { + switch k { + case "defaultValue": + handleDefaultValue = true + case "label": + labelValue, err := CheckValue(v, true, "string", "label") + if err != nil { + return assetProps, errors.WrapError(err, "invalid label value") + } + assetProps.Label = labelValue.(string) + case "description": + descriptionValue, err := CheckValue(v, true, "string", "description") + if err != nil { + return assetProps, errors.WrapError(err, "invalid description value") + } + assetProps.Description = descriptionValue.(string) + case "required": + requiredValue, err := CheckValue(v, true, "boolean", "required") + if err != nil { + return assetProps, errors.WrapError(err, "invalid required value") + } + assetProps.Required = requiredValue.(bool) + case "readOnly": + readOnlyValue, err := CheckValue(v, true, "boolean", "readOnly") + if err != nil { + return assetProps, errors.WrapError(err, "invalid readOnly value") + } + assetProps.ReadOnly = readOnlyValue.(bool) + case "writers": + writers := make([]string, 0) + writersArr, ok := v.([]interface{}) + if ok { + for _, writer := range writersArr { + writerValue, err := CheckValue(writer, false, "string", "writer") + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid writer value") + } + + writers = append(writers, writerValue.(string)) + } + } + assetProps.Writers = writers + default: + continue + } + } + + if handleDefaultValue { + defaultValue, err := validateProp(propMap["defaultValue"], assetProps) + if err != nil { + return AssetProp{}, errors.WrapError(err, "invalid Default Value") + } + assetProps.DefaultValue = defaultValue + } + + return assetProps, nil +} + +// CheckDataType verifies if dataType is valid among the ones availiable in the chaincode +func CheckDataType(dataType string, newTypesList []interface{}) errors.ICCError { + trimDataType := strings.TrimPrefix(dataType, "[]") + + if strings.HasPrefix(trimDataType, "->") { + trimDataType = strings.TrimPrefix(trimDataType, "->") + + assetType := FetchAssetType(trimDataType) + if assetType == nil { + foundDataType := false + for _, newTypeInterface := range newTypesList { + newType := newTypeInterface.(map[string]interface{}) + if newType["tag"] == trimDataType { + foundDataType = true + break + } + } + if !foundDataType { + return errors.NewCCError(fmt.Sprintf("invalid dataType value '%s'", dataType), http.StatusBadRequest) + } + } + } else { + dataTypeObj := FetchDataType(trimDataType) + if dataTypeObj == nil { + return errors.NewCCError(fmt.Sprintf("invalid dataType value '%s'", dataType), http.StatusBadRequest) + } + } + + return nil +} + +// CheckValue verifies if parameter value is of the expected type +func CheckValue(value interface{}, required bool, expectedType, fieldName string) (interface{}, errors.ICCError) { + if value == nil { + if required { + return nil, errors.NewCCError(fmt.Sprintf("required value %s missing", fieldName), http.StatusBadRequest) + } + switch expectedType { + case "string": + return "", nil + case "number": + return 0, nil + case "boolean": + return false, nil + } + } + + switch expectedType { + case "string": + if _, ok := value.(string); !ok { + return nil, errors.NewCCError(fmt.Sprintf("value %s is not a string", fieldName), http.StatusBadRequest) + } + case "number": + if _, ok := value.(float64); !ok { + return nil, errors.NewCCError(fmt.Sprintf("value %s is not a number", fieldName), http.StatusBadRequest) + } + case "boolean": + if _, ok := value.(bool); !ok { + return nil, errors.NewCCError(fmt.Sprintf("value %s is not a boolean", fieldName), http.StatusBadRequest) + } + } + + return value, nil +} diff --git a/test/assets_assetProp_test.go b/test/assets_assetProp_test.go new file mode 100644 index 0000000..e0dba69 --- /dev/null +++ b/test/assets_assetProp_test.go @@ -0,0 +1,50 @@ +package test + +import ( + "log" + "reflect" + "testing" + + "github.com/goledgerdev/cc-tools/assets" +) + +func TestAssetPropToMap(t *testing.T) { + propMap := testAssetList[0].GetPropDef("id").ToMap() + expectedMap := map[string]interface{}{ + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "cpf", + "writers": []string{"org1MSP"}, + } + + if !reflect.DeepEqual(propMap, expectedMap) { + log.Println("these should be deeply equal") + log.Println(propMap) + log.Println(expectedMap) + t.FailNow() + } +} + +func TestAssetPropFromMap(t *testing.T) { + testMap := map[string]interface{}{ + "tag": "secretName", + "isKey": true, + "label": "Secret Name", + "dataType": "string", + "writers": []interface{}{"org2MSP"}, + } + testAssetProp := assets.AssetPropFromMap(testMap) + expectedProp := *testAssetList[3].GetPropDef("secretName") + + if !reflect.DeepEqual(testAssetProp, expectedProp) { + log.Println("these should be deeply equal") + log.Println(testAssetProp) + log.Println(expectedProp) + t.FailNow() + } +} diff --git a/test/assets_assetType_test.go b/test/assets_assetType_test.go index c88acf8..47fadf4 100644 --- a/test/assets_assetType_test.go +++ b/test/assets_assetType_test.go @@ -4,6 +4,8 @@ import ( "log" "reflect" "testing" + + "github.com/goledgerdev/cc-tools/assets" ) func TestGetPropDef(t *testing.T) { @@ -17,3 +19,289 @@ func TestGetPropDef(t *testing.T) { t.FailNow() } } + +func TestAssetTypeToMap(t *testing.T) { + var emptySlice []string + typeMap := testAssetList[0].ToMap() + expectedMap := map[string]interface{}{ + "tag": "person", + "label": "Person", + "description": "Personal data of someone", + "props": []map[string]interface{}{ + { + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "cpf", + "writers": []string{"org1MSP"}, + }, + { + "tag": "name", + "label": "Name of the person", + "description": "", + "isKey": false, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "string", + "writers": emptySlice, + }, + { + "tag": "dateOfBirth", + "label": "Date of Birth", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": nil, + "dataType": "datetime", + "writers": []string{"org1MSP"}, + }, + { + "tag": "height", + "label": "Person's height", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": 0, + "dataType": "number", + "writers": emptySlice, + }, + { + "tag": "info", + "label": "Other Info", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": nil, + "dataType": "@object", + "writers": emptySlice, + }, + }, + "readers": emptySlice, + "dynamic": false, + } + + if !reflect.DeepEqual(typeMap, expectedMap) { + log.Println("these should be deeply equal") + log.Println(typeMap) + log.Println(expectedMap) + t.FailNow() + } +} + +func TestAssetTypeFromMap(t *testing.T) { + testMap := map[string]interface{}{ + "tag": "secret", + "label": "Secret", + "description": "Secret between Org2 and Org3", + "props": []interface{}{ + map[string]interface{}{ + "tag": "secretName", + "isKey": true, + "label": "Secret Name", + "dataType": "string", + "writers": []interface{}{"org2MSP"}, + }, + map[string]interface{}{ + "tag": "secret", + "label": "Secret", + "dataType": "string", + "required": true, + }, + }, + "readers": []interface{}{"org2MSP", "org3MSP"}, + } + testAssetType := assets.AssetTypeFromMap(testMap) + expectedType := testAssetList[3] + + if !reflect.DeepEqual(testAssetType, expectedType) { + log.Println("these should be deeply equal") + log.Println(testAssetType) + log.Println(expectedType) + t.FailNow() + } +} + +func TestAssetTypeListToMap(t *testing.T) { + assetList := []assets.AssetType{ + testAssetList[0], + testAssetList[3], + } + + mapList := assets.ArrayFromAssetTypeList(assetList) + var emptySlice []string + expectedMap := []map[string]interface{}{ + { + "tag": "person", + "label": "Person", + "description": "Personal data of someone", + "props": []map[string]interface{}{ + { + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "cpf", + "writers": []string{"org1MSP"}, + }, + { + "tag": "name", + "label": "Name of the person", + "description": "", + "isKey": false, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "string", + "writers": emptySlice, + }, + { + "tag": "dateOfBirth", + "label": "Date of Birth", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": nil, + "dataType": "datetime", + "writers": []string{"org1MSP"}, + }, + { + "tag": "height", + "label": "Person's height", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": 0, + "dataType": "number", + "writers": emptySlice, + }, + { + "tag": "info", + "label": "Other Info", + "description": "", + "isKey": false, + "required": false, + "readOnly": false, + "defaultValue": nil, + "dataType": "@object", + "writers": emptySlice, + }, + }, + "readers": emptySlice, + "dynamic": false, + }, + { + "tag": "secret", + "label": "Secret", + "description": "Secret between Org2 and Org3", + "props": []map[string]interface{}{ + { + "tag": "secretName", + "label": "Secret Name", + "description": "", + "isKey": true, + "required": false, + "readOnly": false, + "defaultValue": nil, + "dataType": "string", + "writers": []string{"org2MSP"}, + }, + { + "tag": "secret", + "label": "Secret", + "description": "", + "isKey": false, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "string", + "writers": emptySlice, + }, + }, + "readers": []string{"org2MSP", "org3MSP"}, + "dynamic": false, + }, + } + + if !reflect.DeepEqual(mapList, expectedMap) { + log.Println("these should be deeply equal") + log.Println(mapList) + log.Println(expectedMap) + t.FailNow() + } +} + +func TestArrayFromAssetTypeList(t *testing.T) { + testArray := []interface{}{ + map[string]interface{}{ + "tag": "secret", + "label": "Secret", + "description": "Secret between Org2 and Org3", + "props": []interface{}{ + map[string]interface{}{ + "tag": "secretName", + "isKey": true, + "label": "Secret Name", + "dataType": "string", + "writers": []interface{}{"org2MSP"}, + }, + map[string]interface{}{ + "tag": "secret", + "label": "Secret", + "dataType": "string", + "required": true, + }, + }, + "readers": []interface{}{"org2MSP", "org3MSP"}, + }, + map[string]interface{}{ + "tag": "library", + "label": "Library", + "description": "Library as a collection of books", + "props": []interface{}{ + map[string]interface{}{ + "tag": "name", + "isKey": true, + "required": true, + "label": "Library Name", + "dataType": "string", + "writers": []interface{}{"org3MSP"}, + }, + map[string]interface{}{ + "tag": "books", + "label": "Book Collection", + "dataType": "[]->book", + }, + map[string]interface{}{ + "tag": "entranceCode", + "label": "Entrance Code for the Library", + "dataType": "->secret", + }, + }, + }, + } + array := assets.AssetTypeListFromArray(testArray) + expectedList := []assets.AssetType{ + testAssetList[3], + testAssetList[1], + } + + if !reflect.DeepEqual(array, expectedList) { + log.Println("these should be deeply equal") + log.Println(array) + log.Println(expectedList) + t.FailNow() + } +} diff --git a/test/assets_dynamicAssetType_test.go b/test/assets_dynamicAssetType_test.go new file mode 100644 index 0000000..9b7741d --- /dev/null +++ b/test/assets_dynamicAssetType_test.go @@ -0,0 +1,158 @@ +package test + +import ( + "log" + "reflect" + "testing" + + "github.com/goledgerdev/cc-tools/assets" +) + +func TestBuildAssetPropValid(t *testing.T) { + propMap := map[string]interface{}{ + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "cpf", + "writers": []interface{}{"org1MSP"}, + } + prop, err := assets.BuildAssetProp(propMap, nil) + + expectedProp := assets.AssetProp{ + Tag: "id", + Label: "CPF (Brazilian ID)", + Description: "", + IsKey: true, + Required: true, + ReadOnly: false, + DefaultValue: nil, + DataType: "cpf", + Writers: []string{"org1MSP"}, + } + + if err != nil { + log.Println("an error should not have occurred") + log.Println(err) + t.FailNow() + } + + if !reflect.DeepEqual(prop, expectedProp) { + log.Println("these should be deeply equal") + log.Println(prop) + log.Println(expectedProp) + t.FailNow() + } +} + +func TestBuildAssetPropInvalid(t *testing.T) { + propMap := map[string]interface{}{ + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "inexistant", + "writers": []interface{}{"org1MSP"}, + } + _, err := assets.BuildAssetProp(propMap, nil) + + err.Status() + if err.Status() != 400 { + log.Println(err) + t.FailNow() + } + + if err.Message() != "failed checking data type: invalid dataType value 'inexistant'" { + log.Printf("error message different from expected: %s", err.Message()) + t.FailNow() + } +} + +func TestHandlePropUpdate(t *testing.T) { + prop := assets.AssetProp{ + Tag: "id", + Label: "CPF (Brazilian ID)", + Description: "", + IsKey: true, + Required: true, + ReadOnly: false, + DefaultValue: nil, + DataType: "cpf", + } + + propUpdateMap := map[string]interface{}{ + "writers": []interface{}{"org1MSP"}, + "defaultValue": "12345678901", + } + + updatedProp, err := assets.HandlePropUpdate(prop, propUpdateMap) + + if err != nil { + log.Println("an error should not have occurred") + log.Println(err) + t.FailNow() + } + + prop.DefaultValue = "12345678901" + prop.Writers = []string{"org1MSP"} + + if !reflect.DeepEqual(updatedProp, prop) { + log.Println("these should be deeply equal") + log.Println(updatedProp) + log.Println(prop) + t.FailNow() + } +} + +func TestBuildAssetPropWithReferenceList(t *testing.T) { + newTypeList := []interface{}{ + map[string]interface{}{ + "tag": "newType", + "label": "New Type", + }, + } + + propMap := map[string]interface{}{ + "tag": "id", + "label": "CPF (Brazilian ID)", + "description": "", + "isKey": true, + "required": true, + "readOnly": false, + "defaultValue": nil, + "dataType": "->newType", + "writers": []interface{}{"org1MSP"}, + } + prop, err := assets.BuildAssetProp(propMap, newTypeList) + + expectedProp := assets.AssetProp{ + Tag: "id", + Label: "CPF (Brazilian ID)", + Description: "", + IsKey: true, + Required: true, + ReadOnly: false, + DefaultValue: nil, + DataType: "->newType", + Writers: []string{"org1MSP"}, + } + + if err != nil { + log.Println("an error should not have occurred") + log.Println(err) + t.FailNow() + } + + if !reflect.DeepEqual(prop, expectedProp) { + log.Println("these should be deeply equal") + log.Println(prop) + log.Println(expectedProp) + t.FailNow() + } +} diff --git a/test/chaincode_test.go b/test/chaincode_test.go index 4fb017f..254e96c 100644 --- a/test/chaincode_test.go +++ b/test/chaincode_test.go @@ -18,6 +18,10 @@ var testTxList = []tx.Transaction{ tx.CreateAsset, tx.UpdateAsset, tx.DeleteAsset, + tx.CreateAssetType, + tx.UpdateAssetType, + tx.DeleteAssetType, + tx.LoadAssetTypeList, } var testAssetList = []assets.AssetType{ @@ -170,6 +174,36 @@ var testAssetList = []assets.AssetType{ }, }, }, + { + Tag: "assetTypeListData", + Label: "AssetTypeListData", + Description: "AssetTypeListData", + + Props: []assets.AssetProp{ + { + Required: true, + IsKey: true, + Tag: "id", + Label: "ID", + DataType: "string", + Writers: []string{`org1MSP`}, + }, + { + Required: true, + Tag: "list", + Label: "List", + DataType: "[]@object", + Writers: []string{`org1MSP`}, + }, + { + Required: true, + Tag: "lastUpdated", + Label: "Last Updated", + DataType: "datetime", + Writers: []string{`org1MSP`}, + }, + }, + }, } var testCustomDataTypes = map[string]assets.DataType{ diff --git a/test/tx_createAssetType_test.go b/test/tx_createAssetType_test.go new file mode 100644 index 0000000..6a72ec5 --- /dev/null +++ b/test/tx_createAssetType_test.go @@ -0,0 +1,283 @@ +package test + +import ( + "encoding/json" + "log" + "reflect" + "testing" + "time" + + "github.com/goledgerdev/cc-tools/mock" +) + +func TestCreateAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + expectedResponse := map[string]interface{}{ + "description": "Magazine definition", + "dynamic": true, + "label": "Magazine", + "props": []interface{}{ + map[string]interface{}{ + "dataType": "string", + "description": "", + "isKey": true, + "label": "Name", + "readOnly": false, + "required": true, + "tag": "name", + "writers": []interface{}{"org1MSP"}, + }, + map[string]interface{}{ + "dataType": "[]string", + "description": "", + "isKey": false, + "label": "Images", + "readOnly": false, + "required": false, + "tag": "images", + "writers": nil, + }, + }, + "tag": "magazine", + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resPayload []map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resPayload) + if err != nil { + log.Println(err) + t.FailNow() + } + + if len(resPayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(resPayload[0], expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", resPayload[0]) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } + + // Create Asset + asset := map[string]interface{}{ + "@assetType": "magazine", + "name": "MAG", + "images": []string{"url.com/1", "url.com/2"}, + } + req = map[string]interface{}{ + "asset": []map[string]interface{}{asset}, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("createAsset", [][]byte{ + []byte("createAsset"), + reqBytes, + }) + lastUpdated, _ := stub.GetTxTimestamp() + expectedResponse = map[string]interface{}{ + "@key": "magazine:236a29db-f53c-59e1-ac6d-a4f264dbc477", + "@lastTouchBy": "org1MSP", + "@lastTx": "createAsset", + "@lastUpdated": lastUpdated.AsTime().Format(time.RFC3339), + "@assetType": "magazine", + "name": "MAG", + "images": []interface{}{ + "url.com/1", + "url.com/2", + }, + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resAssetPayload []map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resAssetPayload) + if err != nil { + log.Println(err) + t.FailNow() + } + + if len(resAssetPayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(resAssetPayload[0], expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", resAssetPayload[0]) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } + + var state map[string]interface{} + stateBytes := stub.State["magazine:236a29db-f53c-59e1-ac6d-a4f264dbc477"] + err = json.Unmarshal(stateBytes, &state) + if err != nil { + log.Println(err) + t.FailNow() + } + + if !reflect.DeepEqual(state, expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", state) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } +} + +func TestCreateAssetTypeEmptyList(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "unable to get args: required argument 'assetTypes' must be non-empty" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestCreateExistingAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "library", + "label": "Library", + "description": "Library definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "isKey": true, + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resPayload []map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resPayload) + if err != nil { + log.Println(err) + t.FailNow() + } + + if len(resPayload) != 0 { + log.Println("response length should be 0") + t.FailNow() + } +} + +func TestCreateAssetTypeWithoutKey(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "library", + "label": "Library", + "description": "Library definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "failed to build asset type: asset type must have a key" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} diff --git a/test/tx_deleteAssetType_test.go b/test/tx_deleteAssetType_test.go new file mode 100644 index 0000000..fa168bf --- /dev/null +++ b/test/tx_deleteAssetType_test.go @@ -0,0 +1,180 @@ +package test + +import ( + "encoding/json" + "log" + "reflect" + "testing" + + "github.com/goledgerdev/cc-tools/mock" +) + +func TestDeleteAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Delete Type + deleteReq := map[string]interface{}{ + "tag": "magazine", + "force": true, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{deleteReq}, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("deleteAssetType", [][]byte{ + []byte("deleteAssetType"), + reqBytes, + }) + expectedResponse := map[string]interface{}{ + "assetType": map[string]interface{}{ + "description": "Magazine definition", + "dynamic": true, + "label": "Magazine", + "props": []interface{}{ + map[string]interface{}{ + "dataType": "string", + "description": "", + "isKey": true, + "label": "Name", + "readOnly": false, + "required": true, + "tag": "name", + "writers": []interface{}{"org1MSP"}, + }, + map[string]interface{}{ + "dataType": "[]string", + "description": "", + "isKey": false, + "label": "Images", + "readOnly": false, + "required": false, + "tag": "images", + "writers": nil, + }, + }, + "tag": "magazine", + }, + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resDeletePayload []map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resDeletePayload) + if err != nil { + log.Println(err) + t.FailNow() + } + + if len(resDeletePayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(resDeletePayload[0], expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", resDeletePayload[0]) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } +} + +func TestDeleteAssetTypeEmptyList(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("deleteAssetType", [][]byte{ + []byte("deleteAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "unable to get args: required argument 'assetTypes' must be non-empty" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestDeleteNonExistingAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + deleteTag := map[string]interface{}{ + "tag": "inexistent", + "force": true, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{deleteTag}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("deleteAssetType", [][]byte{ + []byte("deleteAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "asset type 'inexistent' not found" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} diff --git a/test/tx_getSchema_test.go b/test/tx_getSchema_test.go index e0fe468..375efe5 100644 --- a/test/tx_getSchema_test.go +++ b/test/tx_getSchema_test.go @@ -39,6 +39,12 @@ func TestGetSchema(t *testing.T) { "tag": "secret", "writers": nil, }, + map[string]interface{}{ + "description": "AssetTypeListData", + "label": "AssetTypeListData", + "tag": "assetTypeListData", + "writers": nil, + }, } err := invokeAndVerify(stub, "getSchema", nil, expectedResponse, 200) if err != nil { diff --git a/test/tx_getTx_test.go b/test/tx_getTx_test.go index 99462c9..7bde712 100644 --- a/test/tx_getTx_test.go +++ b/test/tx_getTx_test.go @@ -26,6 +26,26 @@ func TestGetTx(t *testing.T) { "label": "Delete Asset", "tag": "deleteAsset", }, + map[string]interface{}{ + "description": "", + "label": "Create Asset Type", + "tag": "createAssetType", + }, + map[string]interface{}{ + "description": "", + "label": "Update Asset Type", + "tag": "updateAssetType", + }, + map[string]interface{}{ + "description": "", + "label": "Delete Asset Type", + "tag": "deleteAssetType", + }, + map[string]interface{}{ + "description": "", + "label": "Load Asset Type List from blockchain", + "tag": "loadAssetTypeList", + }, map[string]interface{}{ "description": "", "label": "Get Tx", diff --git a/test/tx_loadAssetTypeList_test.go b/test/tx_loadAssetTypeList_test.go new file mode 100644 index 0000000..6b9de27 --- /dev/null +++ b/test/tx_loadAssetTypeList_test.go @@ -0,0 +1,74 @@ +package test + +import ( + "encoding/json" + "log" + "testing" + + "github.com/goledgerdev/cc-tools/assets" + "github.com/goledgerdev/cc-tools/mock" +) + +func TestLoadAssetTypeList(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + stubOrg2 := mock.NewMockStub("org2MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Load List + reqBytes, err = json.Marshal(map[string]interface{}{}) + if err != nil { + t.FailNow() + } + res = stubOrg2.MockInvoke("loadAssetTypeList", [][]byte{ + []byte("loadAssetTypeList"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + assetTypeList := assets.AssetTypeList() + if len(assetTypeList) != 6 { + log.Println("Expected 6 asset types, got", len(assetTypeList)) + log.Println(assetTypeList) + t.FailNow() + } +} diff --git a/test/tx_updateAssetType_test.go b/test/tx_updateAssetType_test.go new file mode 100644 index 0000000..5cb75d4 --- /dev/null +++ b/test/tx_updateAssetType_test.go @@ -0,0 +1,543 @@ +package test + +import ( + "encoding/json" + "log" + "reflect" + "testing" + + "github.com/goledgerdev/cc-tools/mock" +) + +func TestUpdateAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Update Type + updateReq := map[string]interface{}{ + "tag": "magazine", + "label": "Magazines", + "props": []map[string]interface{}{ + { + "tag": "images", + "delete": true, + }, + { + "tag": "name", + "label": "Magazine Name", + }, + { + "tag": "pages", + "label": "Pages", + "dataType": "[]string", + }, + }, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateReq}, + "skipAssetEmptyValidation": true, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + expectedResponse := map[string]interface{}{ + "description": "Magazine definition", + "dynamic": true, + "label": "Magazines", + "props": []interface{}{ + map[string]interface{}{ + "dataType": "string", + "description": "", + "isKey": true, + "label": "Magazine Name", + "readOnly": false, + "required": true, + "tag": "name", + "writers": []interface{}{"org1MSP"}, + }, + map[string]interface{}{ + "dataType": "[]string", + "description": "", + "isKey": false, + "label": "Pages", + "readOnly": false, + "required": false, + "tag": "pages", + "writers": nil, + }, + }, + "tag": "magazine", + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resUpdatePayload map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resUpdatePayload) + if err != nil { + log.Println(err) + t.FailNow() + } + assetTypesPayload := resUpdatePayload["assetTypes"].([]interface{}) + + if len(assetTypesPayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(assetTypesPayload[0].(map[string]interface{}), expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", assetTypesPayload[0].(map[string]interface{})) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } +} + +func TestUpdateAssetTypeEmptyList(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "unable to get args: required argument 'assetTypes' must be non-empty" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestUpdateNonExistingAssetType(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + updateTag := map[string]interface{}{ + "tag": "inexistent", + "label": "New Label", + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateTag}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "asset type 'inexistent' not found" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestDeleteNonExistingProp(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Update prop + updateReq := map[string]interface{}{ + "tag": "magazine", + "label": "Magazines", + "props": []map[string]interface{}{ + { + "tag": "inexistant", + "delete": true, + }, + }, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateReq}, + "skipAssetEmptyValidation": true, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "invalid props array: attempt to delete inexistent prop" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestDeleteKeyProp(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "magazine", + "label": "Magazine", + "description": "Magazine definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Update prop + updateReq := map[string]interface{}{ + "tag": "magazine", + "label": "Magazines", + "props": []map[string]interface{}{ + { + "tag": "name", + "delete": true, + }, + }, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateReq}, + "skipAssetEmptyValidation": true, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "invalid props array: cannot delete key prop" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} + +func TestAttemptToUpdateInvalidPropSpecs(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "rack", + "label": "Rack", + "description": "Rack definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Update prop + updateReq := map[string]interface{}{ + "tag": "rack", + "props": []map[string]interface{}{ + { + "tag": "images", + "dataType": "[]string", + "isKey": true, + }, + }, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateReq}, + "skipAssetEmptyValidation": true, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + expectedResponse := map[string]interface{}{ + "description": "Rack definition", + "dynamic": true, + "label": "Rack", + "props": []interface{}{ + map[string]interface{}{ + "dataType": "string", + "description": "", + "isKey": true, + "label": "Name", + "readOnly": false, + "required": true, + "tag": "name", + "writers": []interface{}{"org1MSP"}, + }, + map[string]interface{}{ + "dataType": "[]string", + "description": "", + "isKey": false, + "label": "Images", + "readOnly": false, + "required": false, + "tag": "images", + "writers": nil, + }, + }, + "tag": "rack", + } + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + var resUpdatePayload map[string]interface{} + err = json.Unmarshal(res.GetPayload(), &resUpdatePayload) + if err != nil { + log.Println(err) + t.FailNow() + } + assetTypesPayload := resUpdatePayload["assetTypes"].([]interface{}) + + if len(assetTypesPayload) != 1 { + log.Println("response length should be 1") + t.FailNow() + } + + if !reflect.DeepEqual(assetTypesPayload[0].(map[string]interface{}), expectedResponse) { + log.Println("these should be equal") + log.Printf("%#v\n", assetTypesPayload[0].(map[string]interface{})) + log.Printf("%#v\n", expectedResponse) + t.FailNow() + } +} + +func TestCreateKeyProp(t *testing.T) { + stub := mock.NewMockStub("org1MSP", new(testCC)) + newType := map[string]interface{}{ + "tag": "page", + "label": "Page", + "description": "Page definition", + "props": []map[string]interface{}{ + { + "tag": "name", + "label": "Name", + "dataType": "string", + "required": true, + "writers": []string{"org1MSP"}, + "isKey": true, + }, + { + "tag": "images", + "label": "Images", + "dataType": "[]string", + }, + }, + } + req := map[string]interface{}{ + "assetTypes": []map[string]interface{}{newType}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + t.FailNow() + } + + res := stub.MockInvoke("createAssetType", [][]byte{ + []byte("createAssetType"), + reqBytes, + }) + + if res.GetStatus() != 200 { + log.Println(res) + t.FailNow() + } + + // Update prop + updateReq := map[string]interface{}{ + "tag": "page", + "props": []map[string]interface{}{ + { + "tag": "index", + "label": "Index", + "dataType": "[]number", + "isKey": true, + }, + }, + } + req = map[string]interface{}{ + "assetTypes": []map[string]interface{}{updateReq}, + "skipAssetEmptyValidation": true, + } + reqBytes, err = json.Marshal(req) + if err != nil { + t.FailNow() + } + + res = stub.MockInvoke("updateAssetType", [][]byte{ + []byte("updateAssetType"), + reqBytes, + }) + + if res.GetStatus() != 400 { + log.Println(res) + t.FailNow() + } + + if res.GetMessage() != "invalid props array: cannot create key prop" { + log.Printf("error message different from expected: %s", res.GetMessage()) + t.FailNow() + } +} diff --git a/transactions/createAssetType.go b/transactions/createAssetType.go new file mode 100644 index 0000000..b9c706c --- /dev/null +++ b/transactions/createAssetType.go @@ -0,0 +1,134 @@ +package transactions + +import ( + "encoding/json" + "net/http" + + "github.com/goledgerdev/cc-tools/assets" + "github.com/goledgerdev/cc-tools/errors" + sw "github.com/goledgerdev/cc-tools/stubwrapper" +) + +// CreateAssetType is the transaction which creates a dynamic Asset Type +var CreateAssetType = Transaction{ + Tag: "createAssetType", + Label: "Create Asset Type", + Description: "", + Method: "POST", + + MetaTx: true, + Args: ArgList{ + { + Tag: "assetTypes", + Description: "Asset Types to be created.", + DataType: "[]@object", + Required: true, + }, + }, + Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { + assetTypes := req["assetTypes"].([]interface{}) + list := make([]assets.AssetType, 0) + + for _, assetType := range assetTypes { + assetTypeMap := assetType.(map[string]interface{}) + + newAssetType, err := buildAssetType(assetTypeMap, assetTypes) + if err != nil { + return nil, errors.WrapError(err, "failed to build asset type") + } + + // Verify Asset Type existance + assetTypeCheck := assets.FetchAssetType(newAssetType.Tag) + if assetTypeCheck == nil { + list = append(list, newAssetType) + } + } + + if len(list) > 0 { + assets.UpdateAssetList(list) + + err := assets.StoreAssetList(stub) + if err != nil { + return nil, errors.WrapError(err, "failed to store asset list") + } + } + + resBytes, nerr := json.Marshal(list) + if nerr != nil { + return nil, errors.WrapError(nerr, "failed to marshal response") + } + + return resBytes, nil + }, +} + +func buildAssetType(typeMap map[string]interface{}, newTypesList []interface{}) (assets.AssetType, errors.ICCError) { + // Build Props Array + propsArr, ok := typeMap["props"].([]interface{}) + if !ok { + return assets.AssetType{}, errors.NewCCError("invalid props array", http.StatusBadRequest) + } + + hasKey := false + props := make([]assets.AssetProp, len(propsArr)) + for i, prop := range propsArr { + propMap := prop.(map[string]interface{}) + assetProp, err := assets.BuildAssetProp(propMap, newTypesList) + if err != nil { + return assets.AssetType{}, errors.WrapError(err, "failed to build asset prop") + } + if assetProp.IsKey { + hasKey = true + } + props[i] = assetProp + } + + if !hasKey { + return assets.AssetType{}, errors.NewCCError("asset type must have a key", http.StatusBadRequest) + } + + // Tag + tagValue, err := assets.CheckValue(typeMap["tag"], true, "string", "tag") + if err != nil { + return assets.AssetType{}, errors.WrapError(err, "invalid tag value") + } + + // Label + labelValue, err := assets.CheckValue(typeMap["label"], true, "string", "label") + if err != nil { + return assets.AssetType{}, errors.WrapError(err, "invalid label value") + } + + // Description + descriptionValue, err := assets.CheckValue(typeMap["description"], false, "string", "description") + if err != nil { + return assets.AssetType{}, errors.WrapError(err, "invalid description value") + } + + assetType := assets.AssetType{ + Tag: tagValue.(string), + Label: labelValue.(string), + Description: descriptionValue.(string), + Props: props, + Dynamic: true, + } + + // Readers + readers := make([]string, 0) + readersArr, ok := typeMap["readers"].([]interface{}) + if ok { + for _, reader := range readersArr { + readerValue, err := assets.CheckValue(reader, false, "string", "reader") + if err != nil { + return assets.AssetType{}, errors.WrapError(err, "invalid reader value") + } + + readers = append(readers, readerValue.(string)) + } + } + if len(readers) > 0 { + assetType.Readers = readers + } + + return assetType, nil +} diff --git a/transactions/deleteAssetType.go b/transactions/deleteAssetType.go new file mode 100644 index 0000000..657551f --- /dev/null +++ b/transactions/deleteAssetType.go @@ -0,0 +1,133 @@ +package transactions + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/goledgerdev/cc-tools/assets" + "github.com/goledgerdev/cc-tools/errors" + sw "github.com/goledgerdev/cc-tools/stubwrapper" +) + +// DeleteAssetType is the transaction which deletes a dynamic Asset Type +var DeleteAssetType = Transaction{ + Tag: "deleteAssetType", + Label: "Delete Asset Type", + Description: "", + Method: "POST", + + MetaTx: true, + Args: ArgList{ + { + Tag: "assetTypes", + Description: "Asset Types to be deleted.", + DataType: "[]@object", + Required: true, + }, + }, + Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { + assetTypes := req["assetTypes"].([]interface{}) + + assetTypeList := assets.AssetTypeList() + + resArr := make([]map[string]interface{}, 0) + for _, assetType := range assetTypes { + res := make(map[string]interface{}) + + assetTypeMap := assetType.(map[string]interface{}) + + tagValue, err := assets.CheckValue(assetTypeMap["tag"], true, "string", "tag") + if err != nil { + return nil, errors.NewCCError("no tag value in item", http.StatusBadRequest) + } + + forceValue, err := assets.CheckValue(assetTypeMap["force"], false, "boolean", "force") + if err != nil { + return nil, errors.WrapError(err, "error getting force value") + } + + // Verify Asset Type existance + assetTypeCheck := assets.FetchAssetType(tagValue.(string)) + if assetTypeCheck == nil { + return nil, errors.NewCCError(fmt.Sprintf("asset type '%s' not found", tagValue.(string)), http.StatusBadRequest) + } + + // Verify if Asset Type allows dynamic modifications + if !assetTypeCheck.Dynamic { + return nil, errors.NewCCError(fmt.Sprintf("asset type '%s' does not allows dynamic modifications", tagValue.(string)), http.StatusBadRequest) + } + + // Verify Asset Type usage + if !forceValue.(bool) { + err = handleRegisteredAssets(stub, tagValue.(string)) + if err != nil { + return nil, errors.WrapError(err, "error checking asset type usage") + } + } + + // Verify Asset Type references + err = checkAssetTypeReferences(tagValue.(string)) + if err != nil { + return nil, errors.WrapError(err, "error checking asset type references") + } + + // Delete Asset Type + assetTypeList = assets.RemoveAssetType(tagValue.(string), assetTypeList) + + res["assetType"] = assetTypeCheck + resArr = append(resArr, res) + } + + assets.ReplaceAssetList(assetTypeList) + + err := assets.StoreAssetList(stub) + if err != nil { + return nil, errors.WrapError(err, "failed to store asset list") + } + + resBytes, nerr := json.Marshal(resArr) + if nerr != nil { + return nil, errors.WrapError(err, "failed to marshal response") + } + + return resBytes, nil + }, +} + +func handleRegisteredAssets(stub *sw.StubWrapper, tag string) errors.ICCError { + query := fmt.Sprintf( + `{ + "selector": { + "@assetType": "%s" + } + }`, + tag, + ) + + resultsIterator, err := stub.GetQueryResult(query) + if err != nil { + return errors.WrapError(err, "failed to get query result") + } + + if resultsIterator.HasNext() { + return errors.NewCCError(fmt.Sprintf("asset type '%s' is in use", tag), http.StatusBadRequest) + } + + return nil +} + +func checkAssetTypeReferences(tag string) errors.ICCError { + assetTypeList := assets.AssetTypeList() + + for _, assetType := range assetTypeList { + subAssets := assetType.SubAssets() + for _, subAsset := range subAssets { + if subAsset.Tag == tag { + return errors.NewCCError(fmt.Sprintf("asset type '%s' is referenced by asset type '%s'", tag, assetType.Tag), http.StatusBadRequest) + } + } + } + + return nil +} diff --git a/transactions/loadAssetTypeList.go b/transactions/loadAssetTypeList.go new file mode 100644 index 0000000..dfa89bd --- /dev/null +++ b/transactions/loadAssetTypeList.go @@ -0,0 +1,34 @@ +package transactions + +import ( + "encoding/json" + + "github.com/goledgerdev/cc-tools/assets" + "github.com/goledgerdev/cc-tools/errors" + sw "github.com/goledgerdev/cc-tools/stubwrapper" +) + +// LoadAssetTypeList is the transaction which loads the asset Type list from the blockchain +var LoadAssetTypeList = Transaction{ + Tag: "loadAssetTypeList", + Label: "Load Asset Type List from blockchain", + Description: "", + Method: "POST", + + MetaTx: true, + Args: ArgList{}, + Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { + + err := assets.RestoreAssetList(stub, false) + if err != nil { + return nil, errors.WrapError(err, "failed to restore asset list") + } + + resBytes, nerr := json.Marshal("Asset Type List loaded successfully") + if nerr != nil { + return nil, errors.WrapError(err, "failed to marshal response") + } + + return resBytes, nil + }, +} diff --git a/transactions/run.go b/transactions/run.go index e03c106..c6f114a 100644 --- a/transactions/run.go +++ b/transactions/run.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" + "github.com/goledgerdev/cc-tools/assets" "github.com/goledgerdev/cc-tools/errors" sw "github.com/goledgerdev/cc-tools/stubwrapper" "github.com/hyperledger/fabric-chaincode-go/shim" @@ -31,6 +32,13 @@ func Run(stub shim.ChaincodeStubInterface) ([]byte, errors.ICCError) { Stub: stub, } + if assets.GetEnabledDynamicAssetType() { + err := assets.RestoreAssetList(sw, false) + if err != nil { + return nil, errors.WrapError(err, "failed to restore asset list") + } + } + // Verify callers permissions if tx.Callers != nil { // Get tx caller MSP ID diff --git a/transactions/txList.go b/transactions/txList.go index adaf697..cd685ff 100644 --- a/transactions/txList.go +++ b/transactions/txList.go @@ -1,5 +1,7 @@ package transactions +import "github.com/goledgerdev/cc-tools/assets" + var txList = []Transaction{} var basicTxs = []Transaction{ @@ -12,6 +14,13 @@ var basicTxs = []Transaction{ Search, } +var dynamicAssetTypesTxs = []Transaction{ + CreateAssetType, + UpdateAssetType, + DeleteAssetType, + LoadAssetTypeList, +} + // TxList returns a copy of the txList variable func TxList() []Transaction { listCopy := []Transaction{} @@ -32,4 +41,13 @@ func FetchTx(txName string) *Transaction { // InitTxList appends GetTx to txList to avoid initialization loop func InitTxList(l []Transaction) { txList = append(l, basicTxs...) + if assets.GetEnabledDynamicAssetType() { + callers := assets.GetAssetAdminsDynamicAssetType() + for i := range dynamicAssetTypesTxs { + if dynamicAssetTypesTxs[i].Tag != "loadAssetTypeList" { + dynamicAssetTypesTxs[i].Callers = callers + } + } + txList = append(txList, dynamicAssetTypesTxs...) + } } diff --git a/transactions/updateAssetType.go b/transactions/updateAssetType.go new file mode 100644 index 0000000..a748995 --- /dev/null +++ b/transactions/updateAssetType.go @@ -0,0 +1,335 @@ +package transactions + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/goledgerdev/cc-tools/assets" + "github.com/goledgerdev/cc-tools/errors" + sw "github.com/goledgerdev/cc-tools/stubwrapper" +) + +// UpdateAssetType is the transaction which updates a dynamic Asset Type +var UpdateAssetType = Transaction{ + Tag: "updateAssetType", + Label: "Update Asset Type", + Description: "", + Method: "POST", + + MetaTx: true, + Args: ArgList{ + { + Tag: "assetTypes", + Description: "Asset Types to be updated.", + DataType: "[]@object", + Required: true, + }, + { + Tag: "skipAssetEmptyValidation", + Description: "Do not validate existing assets on the update. Its use should be avoided.", + DataType: "boolean", + }, + }, + Routine: func(stub *sw.StubWrapper, req map[string]interface{}) ([]byte, errors.ICCError) { + assetTypes := req["assetTypes"].([]interface{}) + skipAssetsValidation, ok := req["skipAssetEmptyValidation"].(bool) + if !ok { + skipAssetsValidation = false + } + + assetTypeList := assets.AssetTypeList() + assetTypeListFallback := assets.AssetTypeList() + + resAssetArr := make([]assets.AssetType, 0) + requiredValues := make(map[string]interface{}, 0) + + for _, assetType := range assetTypes { + assetTypeMap := assetType.(map[string]interface{}) + + tagValue, err := assets.CheckValue(assetTypeMap["tag"], true, "string", "tag") + if err != nil { + return nil, errors.WrapError(err, "no tag value in item") + } + + // Verify Asset Type existance + assetTypeCheck := assets.FetchAssetType(tagValue.(string)) + if assetTypeCheck == nil { + return nil, errors.NewCCError(fmt.Sprintf("asset type '%s' not found", tagValue.(string)), http.StatusBadRequest) + } + assetTypeObj := *assetTypeCheck + + // Verify if Asset Type allows dynamic modifications + if !assetTypeObj.Dynamic { + return nil, errors.WrapError(err, fmt.Sprintf("asset type '%s' does not allows dynamic modifications", tagValue.(string))) + } + + for key, value := range assetTypeMap { + switch key { + case "label": + labelValue, err := assets.CheckValue(value, true, "string", "label") + if err != nil { + return nil, errors.WrapError(err, "invalid label value") + } + assetTypeObj.Label = labelValue.(string) + case "description": + descriptionValue, err := assets.CheckValue(value, true, "string", "description") + if err != nil { + return nil, errors.WrapError(err, "invalid description value") + } + assetTypeObj.Description = descriptionValue.(string) + case "readers": + readers := make([]string, 0) + readersArr, ok := value.([]interface{}) + if ok { + for _, reader := range readersArr { + readerValue, err := assets.CheckValue(reader, false, "string", "reader") + if err != nil { + return nil, errors.WrapError(err, "invalid reader value") + } + + readers = append(readers, readerValue.(string)) + } + assetTypeObj.Readers = readers + } + case "props": + propsArr, ok := value.([]interface{}) + if !ok { + return nil, errors.NewCCError("invalid props array", http.StatusBadRequest) + } + var emptyAssets bool + if skipAssetsValidation { + emptyAssets = true + } else { + emptyAssets, err = checkEmptyAssets(stub, tagValue.(string)) + if err != nil { + return nil, errors.WrapError(err, "failed to check if there assets for tag") + } + } + newAssetType, newRequiredValues, err := handleProps(assetTypeObj, propsArr, emptyAssets) + if err != nil { + return nil, errors.WrapError(err, "invalid props array") + } + requiredValues[tagValue.(string)] = newRequiredValues + assetTypeObj = newAssetType + default: + continue + } + } + + // Update Asset Type + assets.ReplaceAssetType(assetTypeObj, assetTypeList) + resAssetArr = append(resAssetArr, assetTypeObj) + } + + response := map[string]interface{}{ + "assetTypes": resAssetArr, + } + + assets.ReplaceAssetList(assetTypeList) + + for k, v := range requiredValues { + requiredValuesMap := v.([]map[string]interface{}) + if len(requiredValuesMap) > 0 && !skipAssetsValidation { + updatedAssets, err := initilizeDefaultValues(stub, k, requiredValuesMap) + if err != nil { + // Rollback Asset Type List + assets.ReplaceAssetList(assetTypeListFallback) + return nil, errors.WrapError(err, "failed to initialize default values") + } + response["assets"] = updatedAssets + } + } + + err := assets.StoreAssetList(stub) + if err != nil { + return nil, errors.WrapError(err, "failed to store asset list") + } + + resBytes, nerr := json.Marshal(response) + if nerr != nil { + return nil, errors.WrapError(err, "failed to marshal response") + } + + return resBytes, nil + }, +} + +func handleProps(assetType assets.AssetType, propMap []interface{}, emptyAssets bool) (assets.AssetType, []map[string]interface{}, errors.ICCError) { + propObj := assetType.Props + requiredValues := make([]map[string]interface{}, 0) + + for _, p := range propMap { + p, ok := p.(map[string]interface{}) + if !ok { + return assetType, nil, errors.NewCCError("invalid prop object", http.StatusBadRequest) + } + + tag, err := assets.CheckValue(p["tag"], false, "string", "tag") + if err != nil { + return assetType, nil, errors.WrapError(err, "invalid tag value") + } + tagValue := tag.(string) + + delete, err := assets.CheckValue(p["delete"], false, "boolean", "delete") + if err != nil { + return assetType, nil, errors.WrapError(err, "invalid delete info") + } + deleteVal := delete.(bool) + + hasProp := assetType.HasProp(tagValue) + + if deleteVal && !hasProp { + // Handle inexistant prop deletion + return assetType, nil, errors.NewCCError("attempt to delete inexistent prop", http.StatusBadRequest) + } else if deleteVal && hasProp { + // Delete prop + for i, prop := range propObj { + if prop.Tag == tagValue { + if prop.IsKey { + return assetType, nil, errors.NewCCError("cannot delete key prop", http.StatusBadRequest) + } + propObj = append(propObj[:i], propObj[i+1:]...) + } + } + } else if !hasProp && !deleteVal { + // Create new prop + required, err := assets.CheckValue(p["required"], false, "boolean", "required") + if err != nil { + return assetType, nil, errors.WrapError(err, "invalid required info") + } + requiredVal := required.(bool) + + if requiredVal { + defaultValue, ok := p["defaultValue"] + if !ok && !emptyAssets { + return assetType, nil, errors.NewCCError("required prop must have a default value in case of existing assets", http.StatusBadRequest) + } + + requiredValue := map[string]interface{}{ + "tag": tagValue, + "defaultValue": defaultValue, + } + requiredValues = append(requiredValues, requiredValue) + } + + newProp, err := assets.BuildAssetProp(p, nil) + if err != nil { + return assetType, nil, errors.WrapError(err, "failed to build prop") + } + + if newProp.IsKey { + return assetType, nil, errors.NewCCError("cannot create key prop", http.StatusBadRequest) + } + + propObj = append(propObj, newProp) + } else { + // Update prop + for i, prop := range propObj { + if prop.Tag == tagValue { + required, err := assets.CheckValue(p["required"], false, "boolean", "required") + if err != nil { + return assetType, nil, errors.WrapError(err, "invalid required info") + } + requiredVal := required.(bool) + + defaultValue, ok := p["defaultValue"] + if !ok { + defaultValue = prop.DefaultValue + } + + if !prop.Required && requiredVal { + if defaultValue == nil && !emptyAssets { + return assetType, nil, errors.NewCCError("required prop must have a default value in case of existing assets", http.StatusBadRequest) + } + + requiredValue := map[string]interface{}{ + "tag": tagValue, + "defaultValue": defaultValue, + } + requiredValues = append(requiredValues, requiredValue) + } + + updatedProp, err := assets.HandlePropUpdate(prop, p) + if err != nil { + return assetType, nil, errors.WrapError(err, "failed to update prop") + } + propObj[i] = updatedProp + } + } + } + } + + assetType.Props = propObj + return assetType, requiredValues, nil +} + +func checkEmptyAssets(stub *sw.StubWrapper, tag string) (bool, errors.ICCError) { + query := fmt.Sprintf( + `{ + "selector": { + "@assetType": "%s" + } + }`, + tag, + ) + + resultsIterator, err := stub.GetQueryResult(query) + if err != nil { + return false, errors.WrapError(err, "failed to get query result") + } + + return !resultsIterator.HasNext(), nil +} + +func initilizeDefaultValues(stub *sw.StubWrapper, assetTag string, defaultValuesMap []map[string]interface{}) ([]interface{}, errors.ICCError) { + query := fmt.Sprintf( + `{ + "selector": { + "@assetType": "%s" + } + }`, + assetTag, + ) + + resultsIterator, err := stub.GetQueryResult(query) + if err != nil { + return nil, errors.WrapError(err, "failed to get query result") + } + + res := make([]interface{}, 0) + for resultsIterator.HasNext() { + queryResponse, err := resultsIterator.Next() + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "error iterating response", http.StatusInternalServerError) + } + + var data map[string]interface{} + + err = json.Unmarshal(queryResponse.Value, &data) + if err != nil { + return nil, errors.WrapErrorWithStatus(err, "failed to unmarshal queryResponse values", http.StatusInternalServerError) + } + + for _, propMap := range defaultValuesMap { + propTag := propMap["tag"].(string) + if _, ok := data[propTag]; !ok { + data[propTag] = propMap["defaultValue"] + } + } + + asset, err := assets.NewAsset(data) + if err != nil { + return nil, errors.WrapError(err, "could not assemble asset type") + } + assetMap := (map[string]interface{})(asset) + + assetMap, err = asset.Update(stub, assetMap) + if err != nil { + return nil, errors.WrapError(err, "failed to update asset") + } + res = append(res, assetMap) + } + + return res, nil +}