github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/dotnet/depsjson/depsjson_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package depsjson_test
    16  
    17  import (
    18  	"io/fs"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/go-cmp/cmp/cmpopts"
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/language/dotnet/depsjson"
    29  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    30  	scalibrfs "github.com/google/osv-scalibr/fs"
    31  	"github.com/google/osv-scalibr/inventory"
    32  	"github.com/google/osv-scalibr/purl"
    33  	"github.com/google/osv-scalibr/stats"
    34  	"github.com/google/osv-scalibr/testing/fakefs"
    35  	"github.com/google/osv-scalibr/testing/testcollector"
    36  )
    37  
    38  func TestNew(t *testing.T) {
    39  	tests := []struct {
    40  		name    string
    41  		cfg     depsjson.Config
    42  		wantCfg depsjson.Config
    43  	}{
    44  		{
    45  			name: "default",
    46  			cfg:  depsjson.DefaultConfig(),
    47  			wantCfg: depsjson.Config{
    48  				MaxFileSizeBytes: 10 * units.MiB,
    49  			},
    50  		},
    51  		{
    52  			name: "custom",
    53  			cfg: depsjson.Config{
    54  				MaxFileSizeBytes: 10,
    55  			},
    56  			wantCfg: depsjson.Config{
    57  				MaxFileSizeBytes: 10,
    58  			},
    59  		},
    60  	}
    61  
    62  	for _, tt := range tests {
    63  		t.Run(tt.name, func(t *testing.T) {
    64  			got := depsjson.New(tt.cfg)
    65  			if diff := cmp.Diff(tt.wantCfg, got.Config()); diff != "" {
    66  				t.Errorf("New(%+v).Config(): (-want +got):\n%s", tt.cfg, diff)
    67  			}
    68  		})
    69  	}
    70  }
    71  
    72  func TestFileRequired(t *testing.T) {
    73  	tests := []struct {
    74  		name             string
    75  		path             string
    76  		fileSizeBytes    int64
    77  		maxFileSizeBytes int64
    78  		wantRequired     bool
    79  		wantResultMetric stats.FileRequiredResult
    80  	}{
    81  		{
    82  			name:             "application1.deps.json file",
    83  			path:             "application1.deps.json",
    84  			wantRequired:     true,
    85  			wantResultMetric: stats.FileRequiredResultOK,
    86  		},
    87  		{
    88  			name:             "path application1.deps.json file",
    89  			path:             "path/to/my/application1.deps.json",
    90  			wantRequired:     true,
    91  			wantResultMetric: stats.FileRequiredResultOK,
    92  		},
    93  		{
    94  			name:         "file not required",
    95  			path:         "/test.deps",
    96  			wantRequired: false,
    97  		},
    98  		{
    99  			name:             "application1.deps.json file required if file size < max file size",
   100  			path:             "application1.deps.json",
   101  			fileSizeBytes:    100 * units.KiB,
   102  			maxFileSizeBytes: 1000 * units.KiB,
   103  			wantRequired:     true,
   104  			wantResultMetric: stats.FileRequiredResultOK,
   105  		},
   106  		{
   107  			name:             "application1.deps.json file required if file size == max file size",
   108  			path:             "application1.deps.json",
   109  			fileSizeBytes:    1000 * units.KiB,
   110  			maxFileSizeBytes: 1000 * units.KiB,
   111  			wantRequired:     true,
   112  			wantResultMetric: stats.FileRequiredResultOK,
   113  		},
   114  		{
   115  			name:             "application1.deps.json file not required if file size > max file size",
   116  			path:             "application1.deps.json",
   117  			fileSizeBytes:    1000 * units.KiB,
   118  			maxFileSizeBytes: 100 * units.KiB,
   119  			wantRequired:     false,
   120  			wantResultMetric: stats.FileRequiredResultSizeLimitExceeded,
   121  		},
   122  		{
   123  			name:             "application1.deps.json file required if max file size set to 0",
   124  			path:             "application1.deps.json",
   125  			fileSizeBytes:    100 * units.KiB,
   126  			maxFileSizeBytes: 0,
   127  			wantRequired:     true,
   128  			wantResultMetric: stats.FileRequiredResultOK,
   129  		},
   130  	}
   131  
   132  	for _, tt := range tests {
   133  		t.Run(tt.name, func(t *testing.T) {
   134  			collector := testcollector.New()
   135  			var e filesystem.Extractor = depsjson.New(depsjson.Config{
   136  				Stats:            collector,
   137  				MaxFileSizeBytes: tt.maxFileSizeBytes,
   138  			})
   139  
   140  			fileSizeBytes := tt.fileSizeBytes
   141  			if fileSizeBytes == 0 {
   142  				fileSizeBytes = 1000
   143  			}
   144  
   145  			isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{
   146  				FileName: filepath.Base(tt.path),
   147  				FileMode: fs.ModePerm,
   148  				FileSize: fileSizeBytes,
   149  			}))
   150  			if isRequired != tt.wantRequired {
   151  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired)
   152  			}
   153  
   154  			gotResultMetric := collector.FileRequiredResult(tt.path)
   155  			if tt.wantResultMetric != "" && gotResultMetric != tt.wantResultMetric {
   156  				t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
   157  			}
   158  		})
   159  	}
   160  }
   161  
   162  func TestExtract(t *testing.T) {
   163  	tests := []struct {
   164  		name             string
   165  		path             string
   166  		osrelease        string
   167  		cfg              depsjson.Config
   168  		wantPackages     []*extractor.Package
   169  		wantErr          error
   170  		wantResultMetric stats.FileExtractedResult
   171  	}{
   172  		{
   173  			name: "valid_application1.deps.json_file",
   174  			path: "testdata/valid",
   175  			wantPackages: []*extractor.Package{
   176  				{
   177  					Name:     "TestLibrary",
   178  					Version:  "1.0.0",
   179  					PURLType: purl.TypeNuget,
   180  					Metadata: &depsjson.Metadata{
   181  						PackageName:    "TestLibrary",
   182  						PackageVersion: "1.0.0",
   183  						Type:           "project",
   184  					},
   185  					Locations: []string{"testdata/valid"},
   186  				},
   187  				{
   188  					Name:     "AWSSDK.Core",
   189  					Version:  "3.7.10.6",
   190  					PURLType: purl.TypeNuget,
   191  					Metadata: &depsjson.Metadata{
   192  						PackageName:    "AWSSDK.Core",
   193  						PackageVersion: "3.7.10.6",
   194  						Type:           "package",
   195  					},
   196  					Locations: []string{"testdata/valid"},
   197  				},
   198  				{
   199  					Name:     "Microsoft.Extensions.DependencyInjection",
   200  					Version:  "6.0.0",
   201  					PURLType: purl.TypeNuget,
   202  					Metadata: &depsjson.Metadata{
   203  						PackageName:    "Microsoft.Extensions.DependencyInjection",
   204  						PackageVersion: "6.0.0",
   205  						Type:           "package",
   206  					},
   207  					Locations: []string{"testdata/valid"},
   208  				},
   209  			},
   210  			wantResultMetric: stats.FileExtractedResultSuccess,
   211  		},
   212  		{
   213  			name:             "application1.deps.json file not json",
   214  			path:             "testdata/invalid",
   215  			wantErr:          cmpopts.AnyError,
   216  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
   217  		},
   218  		{
   219  			name:             "application1.deps.json file empty",
   220  			path:             "testdata/empty",
   221  			wantErr:          cmpopts.AnyError,
   222  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
   223  		},
   224  		{
   225  			name: "valid_application1.deps.json_file_with_an_invalid_package",
   226  			path: "testdata/nopackagename",
   227  			wantPackages: []*extractor.Package{
   228  				{
   229  					Name:     "TestLibrary",
   230  					Version:  "1.0.0",
   231  					PURLType: purl.TypeNuget,
   232  					Metadata: &depsjson.Metadata{
   233  						PackageName:    "TestLibrary",
   234  						PackageVersion: "1.0.0",
   235  						Type:           "project",
   236  					},
   237  					Locations: []string{"testdata/nopackagename"},
   238  				},
   239  				{
   240  					Name:     "AWSSDK.Core",
   241  					Version:  "3.7.10.6",
   242  					PURLType: purl.TypeNuget,
   243  					Metadata: &depsjson.Metadata{
   244  						PackageName:    "AWSSDK.Core",
   245  						PackageVersion: "3.7.10.6",
   246  						Type:           "package",
   247  					},
   248  					Locations: []string{"testdata/nopackagename"},
   249  				},
   250  			},
   251  			wantResultMetric: stats.FileExtractedResultSuccess,
   252  		},
   253  	}
   254  
   255  	for _, tt := range tests {
   256  		t.Run(tt.name, func(t *testing.T) {
   257  			collector := testcollector.New()
   258  			var e filesystem.Extractor = depsjson.New(depsjson.Config{
   259  				Stats:            collector,
   260  				MaxFileSizeBytes: 100,
   261  			})
   262  
   263  			d := t.TempDir()
   264  
   265  			// Opening and Reading the Test File
   266  			r, err := os.Open(tt.path)
   267  			defer func() {
   268  				if err = r.Close(); err != nil {
   269  					t.Errorf("Close(): %v", err)
   270  				}
   271  			}()
   272  			if err != nil {
   273  				t.Fatal(err)
   274  			}
   275  
   276  			info, err := os.Stat(tt.path)
   277  			if err != nil {
   278  				t.Fatalf("Failed to stat test file: %v", err)
   279  			}
   280  
   281  			input := &filesystem.ScanInput{
   282  				FS: scalibrfs.DirFS(d), Path: tt.path, Reader: r, Root: d, Info: info,
   283  			}
   284  
   285  			got, err := e.Extract(t.Context(), input)
   286  
   287  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   288  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(pkgLess)); diff != "" {
   289  				t.Errorf("Package mismatch (-want +got):\n%s", diff)
   290  			}
   291  		})
   292  	}
   293  }
   294  
   295  func pkgLess(i1, i2 *extractor.Package) bool {
   296  	return i1.Name < i2.Name
   297  }