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  }