github.com/anchore/syft@v1.38.2/internal/jsonschema/comments_test.go (about) 1 package main 2 3 import ( 4 "os" 5 "path/filepath" 6 "testing" 7 8 "github.com/iancoleman/orderedmap" 9 "github.com/invopop/jsonschema" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 // TestCopyAliasFieldComments verifies that field comments from source types are correctly copied to alias types. 15 // This is important for type aliases like `type RpmArchive RpmDBEntry` where the alias should inherit all field descriptions. 16 func TestCopyAliasFieldComments(t *testing.T) { 17 tests := []struct { 18 name string 19 commentMap map[string]string 20 aliases map[string]string 21 wantComments map[string]string 22 }{ 23 { 24 name: "copies field comments from source type to alias", 25 commentMap: map[string]string{ 26 "github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.", 27 "github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.", 28 "github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.", 29 }, 30 aliases: map[string]string{ 31 "RpmArchive": "RpmDBEntry", 32 }, 33 wantComments: map[string]string{ 34 "github.com/anchore/syft/syft/pkg.RpmDBEntry": "RpmDBEntry represents all captured data from a RPM DB package entry.", 35 "github.com/anchore/syft/syft/pkg.RpmDBEntry.Name": "Name is the RPM package name.", 36 "github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.", 37 "github.com/anchore/syft/syft/pkg.RpmArchive.Name": "Name is the RPM package name.", 38 "github.com/anchore/syft/syft/pkg.RpmArchive.Epoch": "Epoch is the version epoch.", 39 }, 40 }, 41 { 42 name: "handles multiple aliases", 43 commentMap: map[string]string{ 44 "github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.", 45 "github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.", 46 "github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.", 47 }, 48 aliases: map[string]string{ 49 "DpkgArchiveEntry": "DpkgDBEntry", 50 "DpkgSnapshot": "DpkgDBEntry", 51 }, 52 wantComments: map[string]string{ 53 "github.com/anchore/syft/syft/pkg.DpkgDBEntry": "DpkgDBEntry represents data from dpkg.", 54 "github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package": "Package is the package name.", 55 "github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.", 56 "github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Package": "Package is the package name.", 57 "github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Architecture": "Architecture is the target arch.", 58 "github.com/anchore/syft/syft/pkg.DpkgSnapshot.Package": "Package is the package name.", 59 "github.com/anchore/syft/syft/pkg.DpkgSnapshot.Architecture": "Architecture is the target arch.", 60 }, 61 }, 62 { 63 name: "does not copy non-field comments", 64 commentMap: map[string]string{ 65 "github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.", 66 "github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.", 67 }, 68 aliases: map[string]string{ 69 "AliasType": "SomeType", 70 }, 71 wantComments: map[string]string{ 72 "github.com/anchore/syft/syft/pkg.SomeType": "SomeType struct comment.", 73 "github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.", 74 "github.com/anchore/syft/syft/pkg.AliasType.Field": "Field comment.", 75 }, 76 }, 77 } 78 79 for _, tt := range tests { 80 t.Run(tt.name, func(t *testing.T) { 81 // create temp dir for testing 82 tmpDir := t.TempDir() 83 84 // create a test go file with type aliases 85 testFile := filepath.Join(tmpDir, "test.go") 86 content := "package test\n\n" 87 for alias, source := range tt.aliases { 88 content += "type " + alias + " " + source + "\n" 89 } 90 err := os.WriteFile(testFile, []byte(content), 0644) 91 require.NoError(t, err) 92 93 // make a copy of the comment map since the function modifies it 94 commentMap := make(map[string]string) 95 for k, v := range tt.commentMap { 96 commentMap[k] = v 97 } 98 99 // run the function 100 copyAliasFieldComments(commentMap, tmpDir) 101 102 // verify results 103 assert.Equal(t, tt.wantComments, commentMap) 104 }) 105 } 106 } 107 108 func TestFindTypeAliases(t *testing.T) { 109 tests := []struct { 110 name string 111 fileContent string 112 wantAliases map[string]string 113 }{ 114 { 115 name: "finds simple type alias", 116 fileContent: `package test 117 118 type RpmArchive RpmDBEntry 119 type DpkgArchiveEntry DpkgDBEntry 120 `, 121 wantAliases: map[string]string{ 122 "RpmArchive": "RpmDBEntry", 123 "DpkgArchiveEntry": "DpkgDBEntry", 124 }, 125 }, 126 { 127 name: "ignores struct definitions", 128 fileContent: `package test 129 130 type MyStruct struct { 131 Field string 132 } 133 134 type AliasType BaseType 135 `, 136 wantAliases: map[string]string{ 137 "AliasType": "BaseType", 138 }, 139 }, 140 { 141 name: "ignores interface definitions", 142 fileContent: `package test 143 144 type MyInterface interface { 145 Method() 146 } 147 148 type AliasType BaseType 149 `, 150 wantAliases: map[string]string{ 151 "AliasType": "BaseType", 152 }, 153 }, 154 { 155 name: "handles multiple files", 156 fileContent: `package test 157 158 type Alias1 Base1 159 type Alias2 Base2 160 `, 161 wantAliases: map[string]string{ 162 "Alias1": "Base1", 163 "Alias2": "Base2", 164 }, 165 }, 166 } 167 168 for _, tt := range tests { 169 t.Run(tt.name, func(t *testing.T) { 170 // create temp dir 171 tmpDir := t.TempDir() 172 173 // write test file 174 testFile := filepath.Join(tmpDir, "test.go") 175 err := os.WriteFile(testFile, []byte(tt.fileContent), 0644) 176 require.NoError(t, err) 177 178 // run function 179 aliases := findTypeAliases(tmpDir) 180 181 // verify 182 assert.Equal(t, tt.wantAliases, aliases) 183 }) 184 } 185 } 186 187 func TestHasDescriptionInAlternatives(t *testing.T) { 188 tests := []struct { 189 name string 190 schema *jsonschema.Schema 191 want bool 192 }{ 193 { 194 name: "returns true when oneOf has description", 195 schema: &jsonschema.Schema{ 196 OneOf: []*jsonschema.Schema{ 197 {Description: "First alternative"}, 198 {Type: "null"}, 199 }, 200 }, 201 want: true, 202 }, 203 { 204 name: "returns true when anyOf has description", 205 schema: &jsonschema.Schema{ 206 AnyOf: []*jsonschema.Schema{ 207 {Description: "First alternative"}, 208 {Type: "null"}, 209 }, 210 }, 211 want: true, 212 }, 213 { 214 name: "returns false when no alternatives have descriptions", 215 schema: &jsonschema.Schema{ 216 OneOf: []*jsonschema.Schema{ 217 {Type: "integer"}, 218 {Type: "null"}, 219 }, 220 }, 221 want: false, 222 }, 223 { 224 name: "returns false when no oneOf or anyOf", 225 schema: &jsonschema.Schema{ 226 Type: "string", 227 }, 228 want: false, 229 }, 230 { 231 name: "returns true when any alternative in oneOf has description", 232 schema: &jsonschema.Schema{ 233 OneOf: []*jsonschema.Schema{ 234 {Type: "integer"}, 235 {Type: "string", Description: "Second alternative"}, 236 {Type: "null"}, 237 }, 238 }, 239 want: true, 240 }, 241 } 242 243 for _, tt := range tests { 244 t.Run(tt.name, func(t *testing.T) { 245 got := hasDescriptionInAlternatives(tt.schema) 246 assert.Equal(t, tt.want, got) 247 }) 248 } 249 } 250 251 func TestWarnMissingDescriptions(t *testing.T) { 252 tests := []struct { 253 name string 254 schema *jsonschema.Schema 255 metadataNames []string 256 wantTypeWarnings int 257 wantFieldWarnings int 258 }{ 259 { 260 name: "no warnings when all types have descriptions", 261 schema: &jsonschema.Schema{ 262 Definitions: map[string]*jsonschema.Schema{ 263 "TypeA": { 264 Description: "Type A description", 265 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 266 "field1": {Type: "string", Description: "Field 1"}, 267 }), 268 }, 269 }, 270 }, 271 metadataNames: []string{"TypeA"}, 272 wantTypeWarnings: 0, 273 wantFieldWarnings: 0, 274 }, 275 { 276 name: "warns about missing type description", 277 schema: &jsonschema.Schema{ 278 Definitions: map[string]*jsonschema.Schema{ 279 "TypeA": { 280 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 281 "field1": {Type: "string", Description: "Field 1"}, 282 }), 283 }, 284 }, 285 }, 286 metadataNames: []string{"TypeA"}, 287 wantTypeWarnings: 1, 288 wantFieldWarnings: 0, 289 }, 290 { 291 name: "warns about missing field description", 292 schema: &jsonschema.Schema{ 293 Definitions: map[string]*jsonschema.Schema{ 294 "TypeA": { 295 Description: "Type A description", 296 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 297 "field1": {Type: "string"}, 298 }), 299 }, 300 }, 301 }, 302 metadataNames: []string{"TypeA"}, 303 wantTypeWarnings: 0, 304 wantFieldWarnings: 1, 305 }, 306 { 307 name: "skips fields with references", 308 schema: &jsonschema.Schema{ 309 Definitions: map[string]*jsonschema.Schema{ 310 "TypeA": { 311 Description: "Type A description", 312 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 313 "field1": {Ref: "#/$defs/OtherType"}, 314 }), 315 }, 316 }, 317 }, 318 metadataNames: []string{"TypeA"}, 319 wantTypeWarnings: 0, 320 wantFieldWarnings: 0, 321 }, 322 { 323 name: "skips fields with items that are references", 324 schema: &jsonschema.Schema{ 325 Definitions: map[string]*jsonschema.Schema{ 326 "TypeA": { 327 Description: "Type A description", 328 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 329 "field1": { 330 Type: "array", 331 Items: &jsonschema.Schema{Ref: "#/$defs/OtherType"}, 332 }, 333 }), 334 }, 335 }, 336 }, 337 metadataNames: []string{"TypeA"}, 338 wantTypeWarnings: 0, 339 wantFieldWarnings: 0, 340 }, 341 { 342 name: "skips fields with oneOf containing descriptions", 343 schema: &jsonschema.Schema{ 344 Definitions: map[string]*jsonschema.Schema{ 345 "TypeA": { 346 Description: "Type A description", 347 Properties: newOrderedMap(map[string]*jsonschema.Schema{ 348 "field1": { 349 OneOf: []*jsonschema.Schema{ 350 {Type: "integer", Description: "Integer value"}, 351 {Type: "null"}, 352 }, 353 }, 354 }), 355 }, 356 }, 357 }, 358 metadataNames: []string{"TypeA"}, 359 wantTypeWarnings: 0, 360 wantFieldWarnings: 0, 361 }, 362 } 363 364 for _, tt := range tests { 365 t.Run(tt.name, func(t *testing.T) { 366 // capture stderr output would require more complex testing 367 // for now, just verify the function runs without panicking 368 require.NotPanics(t, func() { 369 warnMissingDescriptions(tt.schema, tt.metadataNames) 370 }) 371 }) 372 } 373 } 374 375 // helper to create an ordered map from a regular map 376 func newOrderedMap(m map[string]*jsonschema.Schema) *orderedmap.OrderedMap { 377 om := orderedmap.New() 378 for k, v := range m { 379 om.Set(k, v) 380 } 381 return om 382 }