github.com/CycloneDX/sbom-utility@v0.16.0/cmd/patch_verify_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 /* 3 * Licensed to the Apache Software Foundation (ASF) under one or more 4 * contributor license agreements. See the NOTICE file distributed with 5 * this work for additional information regarding copyright ownership. 6 * The ASF licenses this file to You under the Apache License, Version 2.0 7 * (the "License"); you may not use this file except in compliance with 8 * the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package cmd 20 21 import ( 22 "bytes" 23 "fmt" 24 "reflect" 25 "slices" 26 "testing" 27 28 "github.com/CycloneDX/sbom-utility/common" 29 "github.com/CycloneDX/sbom-utility/utils" 30 ) 31 32 // ------------------------------------------- 33 // test helper functions 34 // ------------------------------------------- 35 36 func VerifyPatchedOutputFileResult(t *testing.T, originalTest PatchTestInfo) (outputBuffer bytes.Buffer, err error) { 37 getLogger().Enter() 38 defer getLogger().Exit() 39 40 patchDocument := NewIETFRFC6902PatchDocument(originalTest.PatchFile) 41 if err = patchDocument.UnmarshalRecords(); err != nil { 42 return 43 } 44 45 // If no patch records were found after unmarshal 46 if patchDocument.Records == nil { 47 return 48 } 49 50 // Create a new test info. structure copying in data from the original test 51 queryTestInfo := NewCommonTestInfo() 52 queryTestInfo.InputFile = originalTest.OutputFile 53 54 // Load and Query temporary "patched" output BOM file using the "from" path 55 // Default to "root" (i.e,, "") path if none selected. 56 DEFAULT_PATH_DOC_ROOT := "" 57 request, err := common.NewQueryRequestSelectFromWhere( 58 common.QUERY_TOKEN_WILDCARD, DEFAULT_PATH_DOC_ROOT, "") 59 if err != nil { 60 t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err) 61 return 62 } 63 64 // Verify each key was removed 65 var pResult interface{} 66 for _, record := range patchDocument.Records { 67 var queryPath, key string 68 queryPath, key, err = retrieveQueryPathFromPatchRecord(record.Path) 69 //fmt.Printf("queryPath: %s, key: %s\n", queryPath, key) 70 if err != nil { 71 t.Errorf("%s: %v", "unable to parse patch record path.", err) 72 return 73 } 74 request.SetRawFromPaths(queryPath) 75 76 // use a buffered query on the temp. output file on the (parent) path 77 pResult, outputBuffer, err = innerQuery(t, queryTestInfo, request) 78 79 // NOTE: Query typically does NOT support non JSON map or slice 80 // we need to allow float64, bool and string for "patch" validation 81 if err != nil && !ErrorTypesMatch(err, &common.QueryResultInvalidTypeError{}) { 82 t.Errorf("%s: %v", ERR_TYPE_UNEXPECTED_ERROR, err) 83 return 84 } 85 86 // short-circuit if the "from" path dereferenced to a non-existent key 87 if pResult == nil { 88 t.Errorf("empty (nil) found at from clause: %s", request.String()) 89 return 90 } 91 92 // verify the "key" was removed from the (parent) JSON map 93 err = VerifyPatched(record, pResult, key) 94 if err != nil { 95 return 96 } 97 } 98 99 return 100 } 101 102 func VerifyPatched(record IETF6902Record, pResult interface{}, key string) (err error) { 103 getLogger().Enter() 104 defer getLogger().Exit() 105 106 // verify the "key" was removed from the (parent) JSON map 107 if pResult != nil { 108 var contains bool 109 switch typedResult := pResult.(type) { 110 case map[string]interface{}: 111 // NOTE: this is for "Add" operation only 112 switch record.Operation { 113 case IETF_RFC6902_OP_ADD: 114 if _, ok := typedResult[key]; !ok { 115 formattedResult, _ := utils.EncodeAnyToDefaultIndentedJSONStr(typedResult) 116 err = getLogger().Errorf("patch failed. Key `%s`, found in: `%s`", key, formattedResult.String()) 117 return 118 } 119 case IETF_RFC6902_OP_REMOVE: 120 return 121 } 122 case []interface{}: 123 // NOTE: this is for "Add" operation only 124 switch record.Operation { 125 case IETF_RFC6902_OP_ADD: 126 if len(typedResult) == 0 { 127 err = getLogger().Errorf("verify failed. Record slice value is empty.") 128 return 129 } 130 131 if record.Value == nil { 132 err = getLogger().Errorf("verify failed. Document slice test value is nil.") 133 return 134 } 135 _, _, contains, err = sliceContainsValue(typedResult, record.Value) 136 if !contains { 137 err = getLogger().Errorf("verify failed. Document value (%v) does not contain expected value (%v).", typedResult, record.Value) 138 return 139 } 140 case IETF_RFC6902_OP_REMOVE: 141 // { "op": "remove", "path": "/metadata/properties/1" } 142 return 143 } 144 case string: 145 switch record.Operation { 146 case IETF_RFC6902_OP_ADD: 147 if record.Value != typedResult { 148 err = getLogger().Errorf("verify failed. Document value (%v) does not contain expected value (%v).", typedResult, record.Value) 149 return 150 } 151 case IETF_RFC6902_OP_REMOVE: 152 return 153 } 154 return 155 case float64: // NOTE: encoding/json turns int64 to float64 156 switch record.Operation { 157 case IETF_RFC6902_OP_ADD: 158 if record.Value != typedResult { 159 err = getLogger().Errorf("verify failed. Document value (%v) does not contain expected value (%v).", typedResult, record.Value) 160 return 161 } 162 case IETF_RFC6902_OP_REMOVE: 163 return 164 } 165 return 166 case bool: 167 switch record.Operation { 168 case IETF_RFC6902_OP_ADD: 169 if record.Value != typedResult { 170 err = getLogger().Errorf("verify failed. Document value (%v) does not contain expected value (%v).", typedResult, record.Value) 171 return 172 } 173 case IETF_RFC6902_OP_REMOVE: 174 return 175 } 176 return 177 default: 178 err = getLogger().Errorf("verify failed. Unexpected JSON type: `%T`", typedResult) 179 return 180 } 181 } else { 182 // TODO: return typed error 183 getLogger().Trace("nil results") 184 } 185 return 186 } 187 188 func sliceContainsValue(slice []interface{}, value interface{}) (foundValue interface{}, index int, contains bool, err error) { 189 getLogger().Enter() 190 defer getLogger().Exit() 191 192 switch typedValue := value.(type) { 193 case map[string]interface{}: 194 var ok bool 195 for i, entry := range slice { 196 if foundValue, ok = entry.(map[string]interface{}); !ok { 197 err = fmt.Errorf("type mismatch error. Slice values: %v (%T), value: %v (%T)", entry, entry, foundValue, foundValue) 198 return 199 } 200 if reflect.DeepEqual(foundValue, typedValue) { 201 contains = true 202 index = i 203 return 204 } 205 } 206 return 207 case []interface{}: 208 if reflect.DeepEqual(slice, typedValue) { 209 contains = true 210 return 211 } 212 return 213 case string: 214 foundValue = value 215 contains = slices.Contains(slice, value) 216 return 217 case bool: 218 foundValue = value 219 contains = slices.Contains(slice, value) 220 return 221 case float64: 222 foundValue = value 223 contains = slices.Contains(slice, value) 224 return 225 default: 226 getLogger().Errorf("contains test failed. Unexpected JSON type: `%T`", typedValue) 227 } 228 return 229 }