github.com/anchore/syft@v1.38.2/syft/configuration_audit_trail_test.go (about) 1 package syft 2 3 import ( 4 "bytes" 5 "crypto" 6 "encoding/json" 7 "fmt" 8 "io" 9 "reflect" 10 "sort" 11 "testing" 12 13 "github.com/hashicorp/go-multierror" 14 "github.com/iancoleman/strcase" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 18 "github.com/anchore/syft/syft/cataloging/filecataloging" 19 "github.com/anchore/syft/syft/file" 20 ) 21 22 func Test_configurationAuditTrail_StructTags(t *testing.T) { 23 // we need to ensure that the output for any configuration is well-formed and follows conventions. 24 // We ensure that: 25 // 1. all fields have a JSON tag 26 // 2. the tag value follows lowercase kebab-case style 27 28 jsonTags := getJSONTags(t, configurationAuditTrail{}) 29 30 for _, tag := range jsonTags { 31 assertLowercaseKebab(t, tag) 32 } 33 34 } 35 36 func getJSONTags(t *testing.T, v interface{}) []string { 37 var tags []string 38 err := collectJSONTags(t, reflect.ValueOf(v), &tags, "", "") 39 require.NoError(t, err) 40 return tags 41 } 42 43 func collectJSONTags(t *testing.T, v reflect.Value, tags *[]string, parentTag string, path string) error { 44 var errs error 45 46 if v.Kind() == reflect.Ptr { 47 v = v.Elem() 48 } 49 50 if v.Kind() != reflect.Struct { 51 return errs 52 } 53 54 tType := v.Type() 55 for i := 0; i < v.NumField(); i++ { 56 field := v.Field(i) 57 fieldType := tType.Field(i) 58 59 curPath := path + "." + fieldType.Name 60 61 // account for embeddings 62 if fieldType.Anonymous { 63 embeddedField := field 64 65 if embeddedField.Kind() == reflect.Ptr { 66 // this can be enhanced in the future if the need arises... 67 errs = multierror.Append(errs, fmt.Errorf("field '%s' is a pointer to an embedded struct, this is not supported in the test helper", curPath)) 68 } 69 70 if embeddedField.Kind() == reflect.Struct { 71 err := collectJSONTags(t, field, tags, parentTag, curPath) 72 if err != nil { 73 errs = multierror.Append(errs, err) 74 } 75 } 76 77 continue 78 } 79 80 var tag string 81 var ok bool 82 if fieldType.PkgPath == "" { 83 tag, ok = fieldType.Tag.Lookup("json") 84 if !ok || (tag == "" && parentTag == "") { 85 errs = multierror.Append(errs, fmt.Errorf("field '%s' does not have a json tag", curPath)) 86 return errs 87 } 88 if tag != "" && tag != "-" { 89 *tags = append(*tags, tag) 90 } 91 } 92 93 if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) { 94 err := collectJSONTags(t, field, tags, tag, curPath) 95 if err != nil { 96 errs = multierror.Append(errs, err) 97 } 98 } 99 } 100 return errs 101 } 102 103 func assertLowercaseKebab(t *testing.T, tag string) { 104 t.Helper() 105 require.NotEmpty(t, tag) 106 assert.Equal(t, strcase.ToKebab(tag), tag) 107 } 108 109 func Test_collectJSONTags(t *testing.T) { 110 // though this is not used in production, this is a sensitive and complex enough of a check to warrant testing the test helper. 111 type good struct { 112 A string `json:"a"` 113 } 114 115 type missing struct { 116 A string `json:"a"` 117 B string 118 } 119 120 type exclude struct { 121 A string `json:"a"` 122 B string `json:"-"` 123 } 124 125 type goodEmbedded struct { 126 good `json:""` 127 } 128 129 type badEmbedded struct { 130 missing `json:""` 131 } 132 133 // simply not covered and require further development to support 134 type goodPtrEmbedded struct { 135 *good `json:""` 136 } 137 138 // simply not covered and require further development to support 139 type badPtrEmbedded struct { 140 *missing `json:""` 141 } 142 143 tests := []struct { 144 name string 145 v interface{} 146 want []string 147 wantErr require.ErrorAssertionFunc 148 }{ 149 { 150 name: "good", 151 v: good{}, 152 want: []string{ 153 "a", 154 }, 155 }, 156 { 157 name: "missing", 158 v: missing{}, 159 wantErr: require.Error, 160 }, 161 { 162 name: "exclude", 163 v: exclude{}, 164 want: []string{ 165 "a", 166 }, 167 }, 168 { 169 name: "bad embedded", 170 v: badEmbedded{}, 171 wantErr: require.Error, 172 }, 173 { 174 name: "good embedded", 175 v: goodEmbedded{}, 176 want: []string{ 177 "a", 178 }, 179 }, 180 // these cases are simply not covered and require further development to support 181 { 182 name: "bad ptr embedded", 183 v: badPtrEmbedded{}, 184 wantErr: require.Error, 185 }, 186 { 187 name: "good ptr embedded", 188 v: goodPtrEmbedded{}, 189 want: []string{ 190 "a", 191 }, 192 wantErr: require.Error, 193 }, 194 } 195 196 for _, tt := range tests { 197 t.Run(tt.name, func(t *testing.T) { 198 if tt.wantErr == nil { 199 tt.wantErr = require.NoError 200 } 201 202 var tags []string 203 204 err := collectJSONTags(t, reflect.ValueOf(tt.v), &tags, "", "") 205 206 tt.wantErr(t, err) 207 if err != nil { 208 return 209 } 210 211 assert.Equal(t, tt.want, tags) 212 }) 213 } 214 215 } 216 217 func Test_configurationAuditTrail_MarshalJSON(t *testing.T) { 218 219 tests := []struct { 220 name string 221 cfg configurationAuditTrail 222 assert func(t *testing.T, got []byte) 223 }{ 224 { 225 name: "ensure other marshallers are called", 226 cfg: configurationAuditTrail{ 227 228 Files: filecataloging.Config{ 229 Selection: file.FilesOwnedByPackageSelection, 230 Hashers: []crypto.Hash{ 231 crypto.SHA256, 232 }, 233 }, 234 }, 235 // the custom file marshaller swaps ints for strings for hashers 236 assert: func(t *testing.T, got []byte) { 237 assert.Contains(t, string(got), `"hashers":["sha-256"]`) 238 }, 239 }, 240 { 241 name: "ensure maps are sorted", 242 cfg: configurationAuditTrail{}, 243 assert: func(t *testing.T, got []byte) { 244 assert.NoError(t, assertJSONKeysSorted(got)) 245 }, 246 }, 247 } 248 for _, tt := range tests { 249 t.Run(tt.name, func(t *testing.T) { 250 251 got, err := tt.cfg.MarshalJSON() 252 require.NoError(t, err) 253 if tt.assert == nil { 254 t.Fatal("assert function must be provided") 255 } 256 tt.assert(t, got) 257 258 }) 259 } 260 } 261 262 // assertJSONKeysSorted asserts that all keys in JSON maps are sorted. 263 func assertJSONKeysSorted(jsonBytes []byte) error { 264 var errs error 265 decoder := json.NewDecoder(bytes.NewReader(jsonBytes)) 266 var keys []string 267 var inObject bool 268 269 for { 270 token, err := decoder.Token() 271 if err != nil { 272 if err == io.EOF { 273 break 274 } 275 errs = multierror.Append(errs, fmt.Errorf("error decoding JSON: %w", err)) 276 } 277 278 switch v := token.(type) { 279 case json.Delim: 280 switch v { 281 case '{': 282 inObject = true 283 keys = nil // Reset keys for a new object 284 case '}': 285 inObject = false 286 if !sort.StringsAreSorted(keys) { 287 errs = multierror.Append(errs, fmt.Errorf("Keys are not sorted: %v", keys)) 288 } 289 } 290 case string: 291 if inObject && v != "" { 292 keys = append(keys, v) 293 } 294 } 295 } 296 return errs 297 } 298 299 func Test_assertJSONKeysSorted(t *testing.T) { 300 // this test function is sufficiently complicated enough to warrant its own test... 301 302 sorted := []byte(`{"a":1,"b":2}`) 303 unsorted := []byte(`{"b":2,"a":1}`) 304 305 nestedSorted := []byte(`{"a":1,"b":{"a":1,"b":2}}`) 306 nestedUnsorted := []byte(`{"a":1,"b":{"b":2,"a":1}}`) 307 308 tests := []struct { 309 name string 310 json []byte 311 wantErr require.ErrorAssertionFunc 312 }{ 313 { 314 name: "sorted", 315 json: sorted, 316 wantErr: require.NoError, 317 }, 318 { 319 name: "unsorted", 320 json: unsorted, 321 wantErr: require.Error, 322 }, 323 { 324 name: "nested sorted", 325 json: nestedSorted, 326 wantErr: require.NoError, 327 }, 328 { 329 name: "nested unsorted", 330 json: nestedUnsorted, 331 wantErr: require.Error, 332 }, 333 } 334 335 for _, tt := range tests { 336 t.Run(tt.name, func(t *testing.T) { 337 if tt.wantErr == nil { 338 tt.wantErr = require.NoError 339 } 340 341 err := assertJSONKeysSorted(tt.json) 342 tt.wantErr(t, err) 343 }) 344 345 } 346 }