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  }