github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/manifest/npm/packagejson_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 npm_test
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"testing"
    22  
    23  	"deps.dev/util/resolve"
    24  	"deps.dev/util/resolve/dep"
    25  	"github.com/google/go-cmp/cmp"
    26  	scalibrfs "github.com/google/osv-scalibr/fs"
    27  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    28  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest/npm"
    29  	"github.com/google/osv-scalibr/guidedremediation/result"
    30  )
    31  
    32  func aliasType(t *testing.T, aliasedName string) dep.Type {
    33  	t.Helper()
    34  	var typ dep.Type
    35  	typ.AddAttr(dep.KnownAs, aliasedName)
    36  
    37  	return typ
    38  }
    39  
    40  func makeVK(t *testing.T, name, version string, versionType resolve.VersionType) resolve.VersionKey {
    41  	t.Helper()
    42  	return resolve.VersionKey{
    43  		PackageKey: resolve.PackageKey{
    44  			System: resolve.NPM,
    45  			Name:   name,
    46  		},
    47  		Version:     version,
    48  		VersionType: versionType,
    49  	}
    50  }
    51  
    52  func makeReqKey(t *testing.T, name, knownAs string) manifest.RequirementKey {
    53  	t.Helper()
    54  	var typ dep.Type
    55  	if knownAs != "" {
    56  		typ.AddAttr(dep.KnownAs, knownAs)
    57  	}
    58  
    59  	return npm.MakeRequirementKey(resolve.RequirementVersion{
    60  		VersionKey: resolve.VersionKey{
    61  			PackageKey: resolve.PackageKey{
    62  				Name:   name,
    63  				System: resolve.NPM,
    64  			},
    65  		},
    66  		Type: typ,
    67  	})
    68  }
    69  
    70  type testManifest struct {
    71  	FilePath       string
    72  	Root           resolve.Version
    73  	System         resolve.System
    74  	Requirements   []resolve.RequirementVersion
    75  	Groups         map[manifest.RequirementKey][]string
    76  	LocalManifests []testManifest
    77  }
    78  
    79  func checkManifest(t *testing.T, name string, got manifest.Manifest, want testManifest) {
    80  	t.Helper()
    81  	if want.FilePath != got.FilePath() {
    82  		t.Errorf("%s.FilePath() = %q, want %q", name, got.FilePath(), want.FilePath)
    83  	}
    84  	if diff := cmp.Diff(want.Root, got.Root()); diff != "" {
    85  		t.Errorf("%s.Root() (-want +got):\n%s", name, diff)
    86  	}
    87  	if want.System != got.System() {
    88  		t.Errorf("%s.System() = %v, want %v", name, got.System(), want.System)
    89  	}
    90  	if diff := cmp.Diff(want.Requirements, got.Requirements()); diff != "" {
    91  		t.Errorf("%s.Requirements() (-want +got):\n%s", name, diff)
    92  	}
    93  	if diff := cmp.Diff(want.Groups, got.Groups()); diff != "" {
    94  		t.Errorf("%s.Groups() (-want +got):\n%s", name, diff)
    95  	}
    96  	gotLocal := got.LocalManifests()
    97  	if len(want.LocalManifests) != len(got.LocalManifests()) {
    98  		t.Errorf("got %d %s.LocalManifests(), want %d", len(gotLocal), name, len(want.LocalManifests))
    99  	}
   100  	n := min(len(gotLocal), len(want.LocalManifests))
   101  	for i := range n {
   102  		checkManifest(t, fmt.Sprintf("%s.LocalManifests[%d]", name, i), gotLocal[i], want.LocalManifests[i])
   103  	}
   104  }
   105  
   106  func TestRead(t *testing.T) {
   107  	rw, err := npm.GetReadWriter()
   108  	if err != nil {
   109  		t.Fatalf("error creating ReadWriter: %v", err)
   110  	}
   111  	fsys := scalibrfs.DirFS("./testdata")
   112  	got, err := rw.Read("package.json", fsys)
   113  	if err != nil {
   114  		t.Fatalf("error reading manifest: %v", err)
   115  	}
   116  
   117  	want := testManifest{
   118  		FilePath: "package.json",
   119  		Root: resolve.Version{
   120  			VersionKey: makeVK(t, "npm-manifest", "1.0.0", resolve.Concrete),
   121  		},
   122  		System: resolve.NPM,
   123  		Requirements: []resolve.RequirementVersion{
   124  			{
   125  				Type:       aliasType(t, "cliui"), // sorts on aliased name, not real package name
   126  				VersionKey: makeVK(t, "@isaacs/cliui", "^8.0.2", resolve.Requirement),
   127  			},
   128  			{
   129  				// Type: dep.NewType(dep.Dev), devDependencies treated as prod to make resolution work
   130  				VersionKey: makeVK(t, "eslint", "^8.57.0", resolve.Requirement),
   131  			},
   132  			{
   133  				Type:       dep.NewType(dep.Opt),
   134  				VersionKey: makeVK(t, "glob", "^10.3.10", resolve.Requirement),
   135  			},
   136  			{
   137  				VersionKey: makeVK(t, "jquery", "latest", resolve.Requirement),
   138  			},
   139  			{
   140  				VersionKey: makeVK(t, "lodash", "4.17.17", resolve.Requirement),
   141  			},
   142  			{
   143  				VersionKey: makeVK(t, "string-width", "^5.1.2", resolve.Requirement),
   144  			},
   145  			{
   146  				Type:       aliasType(t, "string-width-aliased"),
   147  				VersionKey: makeVK(t, "string-width", "^4.2.3", resolve.Requirement),
   148  			},
   149  		},
   150  		Groups: map[manifest.RequirementKey][]string{
   151  			makeReqKey(t, "eslint", ""): {"dev"},
   152  			makeReqKey(t, "glob", ""):   {"optional"},
   153  		},
   154  	}
   155  
   156  	checkManifest(t, "Manifest", got, want)
   157  }
   158  
   159  func TestReadWithWorkspaces(t *testing.T) {
   160  	rw, err := npm.GetReadWriter()
   161  	if err != nil {
   162  		t.Fatalf("error creating ReadWriter: %v", err)
   163  	}
   164  	fsys := scalibrfs.DirFS("./testdata/workspaces")
   165  	got, err := rw.Read("package.json", fsys)
   166  	if err != nil {
   167  		t.Fatalf("error reading manifest: %v", err)
   168  	}
   169  
   170  	want := testManifest{
   171  		FilePath: "package.json",
   172  		Root: resolve.Version{
   173  			VersionKey: makeVK(t, "npm-workspace-test", "1.0.0", resolve.Concrete),
   174  		},
   175  		System: resolve.NPM,
   176  		Requirements: []resolve.RequirementVersion{
   177  			// root dependencies always before workspace
   178  			{
   179  				Type:       aliasType(t, "jquery-real"),
   180  				VersionKey: makeVK(t, "jquery", "^3.7.1", resolve.Requirement),
   181  			},
   182  			// workspaces in path order
   183  			{
   184  				VersionKey: makeVK(t, "jquery:workspace", "^3.7.1", resolve.Requirement),
   185  			},
   186  			{
   187  				VersionKey: makeVK(t, "@workspace/ugh:workspace", "*", resolve.Requirement),
   188  			},
   189  			{
   190  				VersionKey: makeVK(t, "z-z-z:workspace", "*", resolve.Requirement),
   191  			},
   192  		},
   193  		Groups: map[manifest.RequirementKey][]string{
   194  			makeReqKey(t, "jquery", "jquery-real"): {"dev"},
   195  			// excludes workspace dev dependency
   196  		},
   197  		LocalManifests: []testManifest{
   198  			{
   199  				FilePath: "ws/jquery/package.json",
   200  				Root: resolve.Version{
   201  					VersionKey: makeVK(t, "jquery:workspace", "3.7.1", resolve.Concrete),
   202  				},
   203  				System: resolve.NPM,
   204  				Requirements: []resolve.RequirementVersion{
   205  					{
   206  						VersionKey: makeVK(t, "semver", "^7.6.0", resolve.Requirement),
   207  					},
   208  				},
   209  				Groups: map[manifest.RequirementKey][]string{},
   210  			},
   211  			{
   212  				FilePath: "ws/ugh/package.json",
   213  				Root: resolve.Version{
   214  					VersionKey: makeVK(t, "@workspace/ugh:workspace", "0.0.1", resolve.Concrete),
   215  				},
   216  				System: resolve.NPM,
   217  				Requirements: []resolve.RequirementVersion{
   218  					{
   219  						VersionKey: makeVK(t, "jquery:workspace", "*", resolve.Requirement),
   220  					},
   221  					{
   222  						VersionKey: makeVK(t, "semver", "^6.3.1", resolve.Requirement),
   223  					},
   224  				},
   225  				Groups: map[manifest.RequirementKey][]string{
   226  					makeReqKey(t, "jquery:workspace", ""): {"dev"},
   227  					makeReqKey(t, "semver", ""):           {"dev"},
   228  				},
   229  			},
   230  			{
   231  				FilePath: "z/package.json",
   232  				Root: resolve.Version{
   233  					VersionKey: makeVK(t, "z-z-z:workspace", "1.0.0", resolve.Concrete),
   234  				},
   235  				System: resolve.NPM,
   236  				Requirements: []resolve.RequirementVersion{
   237  					{
   238  						VersionKey: makeVK(t, "@workspace/ugh:workspace", "*", resolve.Requirement),
   239  					},
   240  					{
   241  						VersionKey: makeVK(t, "semver", "^5.7.2", resolve.Requirement),
   242  					},
   243  				},
   244  				Groups: map[manifest.RequirementKey][]string{},
   245  			},
   246  		},
   247  	}
   248  
   249  	checkManifest(t, "Manifest", got, want)
   250  }
   251  
   252  func TestWrite(t *testing.T) {
   253  	rw, err := npm.GetReadWriter()
   254  	if err != nil {
   255  		t.Fatalf("error creating ReadWriter: %v", err)
   256  	}
   257  	fsys := scalibrfs.DirFS("./testdata")
   258  	manif, err := rw.Read("package.json", fsys)
   259  	if err != nil {
   260  		t.Fatalf("error reading manifest: %v", err)
   261  	}
   262  
   263  	patches := []result.Patch{
   264  		{
   265  			PackageUpdates: []result.PackageUpdate{
   266  				{
   267  					Name:        "lodash",
   268  					VersionFrom: "4.17.17",
   269  					VersionTo:   "^4.17.21",
   270  				},
   271  				{
   272  					Name:        "eslint",
   273  					VersionFrom: "^8.57.0",
   274  					VersionTo:   "*",
   275  				},
   276  				{
   277  					Name:        "glob",
   278  					VersionFrom: "^10.3.10",
   279  					VersionTo:   "^1.0.0",
   280  				},
   281  				{
   282  					Name:        "jquery",
   283  					VersionFrom: "latest",
   284  					VersionTo:   "~0.0.1",
   285  				},
   286  			},
   287  		},
   288  		{
   289  			PackageUpdates: []result.PackageUpdate{
   290  				{
   291  					Name:        "@isaacs/cliui",
   292  					VersionFrom: "^8.0.2",
   293  					VersionTo:   "^9.0.0",
   294  					Type:        aliasType(t, "cliui"),
   295  				},
   296  				{
   297  					Name:        "string-width",
   298  					VersionFrom: "^5.1.2",
   299  					VersionTo:   "^7.1.0",
   300  				},
   301  				{
   302  					Name:        "string-width",
   303  					VersionFrom: "^4.2.3",
   304  					VersionTo:   "^6.1.0",
   305  					Type:        aliasType(t, "string-width-aliased"),
   306  				},
   307  			},
   308  		},
   309  	}
   310  	outDir := t.TempDir()
   311  	outFile := filepath.Join(outDir, "package.json")
   312  
   313  	if err := rw.Write(manif, fsys, patches, outFile); err != nil {
   314  		t.Fatalf("failed to write package.json: %v", err)
   315  	}
   316  
   317  	got, err := os.ReadFile(outFile)
   318  	if err != nil {
   319  		t.Fatalf("failed to read got package.json: %v", err)
   320  	}
   321  	want, err := os.ReadFile(filepath.Join("./testdata", "write_want.package.json"))
   322  	if err != nil {
   323  		t.Fatalf("failed to read want package.json: %v", err)
   324  	}
   325  	if diff := cmp.Diff(want, got); diff != "" {
   326  		t.Errorf("package.json (-want +got):\n%s", diff)
   327  	}
   328  }