github.com/google/osv-scalibr@v0.4.1/clients/datasource/npmrc_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 datasource_test
    16  
    17  import (
    18  	"encoding/base64"
    19  	"fmt"
    20  	"net/http"
    21  	"os"
    22  	"path/filepath"
    23  	"testing"
    24  
    25  	"github.com/google/osv-scalibr/clients/datasource"
    26  )
    27  
    28  // These tests rely on using 'globalconfig' and 'userconfig' in the package .npmrc to override their default locations.
    29  // It's also possible for environment variables or the builtin npmrc to mess with these tests.
    30  
    31  func createTempNpmrc(t *testing.T, filename string) string {
    32  	t.Helper()
    33  	dir := t.TempDir()
    34  	file := filepath.Join(dir, filename)
    35  	f, err := os.Create(file)
    36  	if err != nil {
    37  		t.Fatalf("could not create test npmrc file: %v", err)
    38  	}
    39  	f.Close()
    40  
    41  	return file
    42  }
    43  
    44  func writeToNpmrc(t *testing.T, file string, lines ...string) {
    45  	t.Helper()
    46  	f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY, 0666)
    47  	if err != nil {
    48  		t.Fatalf("could not write to test npmrc file: %v", err)
    49  	}
    50  	defer f.Close()
    51  	for _, line := range lines {
    52  		if _, err := fmt.Fprintln(f, line); err != nil {
    53  			t.Fatalf("could not write to test npmrc file: %v", err)
    54  		}
    55  	}
    56  }
    57  
    58  type testNpmrcFiles struct {
    59  	global  string
    60  	user    string
    61  	project string
    62  }
    63  
    64  func makeBlankNpmrcFiles(t *testing.T) testNpmrcFiles {
    65  	t.Helper()
    66  	var files testNpmrcFiles
    67  	files.global = createTempNpmrc(t, "npmrc")
    68  	files.user = createTempNpmrc(t, ".npmrc")
    69  	files.project = createTempNpmrc(t, ".npmrc")
    70  	writeToNpmrc(t, files.project, "globalconfig="+files.global, "userconfig="+files.user)
    71  
    72  	return files
    73  }
    74  
    75  func checkNPMRegistryRequest(t *testing.T, config datasource.NPMRegistryConfig, urlComponents []string, wantURL string, wantAuth string) {
    76  	t.Helper()
    77  	mt := &mockTransport{}
    78  	httpClient := &http.Client{Transport: mt}
    79  	resp, err := config.MakeRequest(t.Context(), httpClient, urlComponents...)
    80  	if err != nil {
    81  		t.Fatalf("error making request: %v", err)
    82  	}
    83  	defer resp.Body.Close()
    84  	if len(mt.Requests) != 1 {
    85  		t.Fatalf("unexpected number of requests made: %v", len(mt.Requests))
    86  	}
    87  	req := mt.Requests[0]
    88  	gotURL := req.URL.String()
    89  	if gotURL != wantURL {
    90  		t.Errorf("MakeRequest() URL was %s, want %s", gotURL, wantURL)
    91  	}
    92  	gotAuth := req.Header.Get("Authorization")
    93  	if gotAuth != wantAuth {
    94  		t.Errorf("MakeRequest() Authorization was \"%s\", want \"%s\"", gotAuth, wantAuth)
    95  	}
    96  }
    97  
    98  func TestLoadNPMRegistryConfig_WithNoRegistries(t *testing.T) {
    99  	npmrcFiles := makeBlankNpmrcFiles(t)
   100  
   101  	config, err := datasource.LoadNPMRegistryConfig(filepath.Dir(npmrcFiles.project))
   102  	if err != nil {
   103  		t.Fatalf("could not parse npmrc: %v", err)
   104  	}
   105  
   106  	if nRegs := len(config.ScopeURLs); nRegs != 1 {
   107  		t.Errorf("expected 1 npm registry, got %v", nRegs)
   108  	}
   109  
   110  	checkNPMRegistryRequest(t, config, []string{"@test/package", "1.2.3"},
   111  		"https://registry.npmjs.org/@test%2fpackage/1.2.3", "")
   112  }
   113  
   114  func TestLoadNPMRegistryConfig_WithAuth(t *testing.T) {
   115  	npmrcFiles := makeBlankNpmrcFiles(t)
   116  	writeToNpmrc(t, npmrcFiles.project,
   117  		"registry=https://registry1.test.com",
   118  		"//registry1.test.com/:_auth=bXVjaDphdXRoCg==",
   119  		"@test1:registry=https://registry2.test.com",
   120  		"//registry2.test.com/:_authToken=c3VjaCB0b2tlbgo=",
   121  		"@test2:registry=https://sub.registry2.test.com",
   122  		"//sub.registry2.test.com:username=user",
   123  		"//sub.registry2.test.com:_password=d293Cg==",
   124  	)
   125  
   126  	config, err := datasource.LoadNPMRegistryConfig(filepath.Dir(npmrcFiles.project))
   127  	if err != nil {
   128  		t.Fatalf("could not parse npmrc: %v", err)
   129  	}
   130  
   131  	checkNPMRegistryRequest(t, config, []string{"foo"}, "https://registry1.test.com/foo", "Basic bXVjaDphdXRoCg==")
   132  	checkNPMRegistryRequest(t, config, []string{"@test0/bar"}, "https://registry1.test.com/@test0%2fbar", "Basic bXVjaDphdXRoCg==")
   133  	checkNPMRegistryRequest(t, config, []string{"@test1/baz"}, "https://registry2.test.com/@test1%2fbaz", "Bearer c3VjaCB0b2tlbgo=")
   134  	checkNPMRegistryRequest(t, config, []string{"@test2/test"}, "https://sub.registry2.test.com/@test2%2ftest", "Basic dXNlcjp3b3cK")
   135  }
   136  
   137  // Do not make this test parallel because it calls t.Setenv()
   138  func TestLoadNPMRegistryConfig_WithOverrides(t *testing.T) {
   139  	check := func(t *testing.T, npmrcFiles testNpmrcFiles, wantURLs [5]string) {
   140  		t.Helper()
   141  		config, err := datasource.LoadNPMRegistryConfig(filepath.Dir(npmrcFiles.project))
   142  		if err != nil {
   143  			t.Fatalf("could not parse npmrc: %v", err)
   144  		}
   145  		checkNPMRegistryRequest(t, config, []string{"pkg"}, wantURLs[0], "")
   146  		checkNPMRegistryRequest(t, config, []string{"@general/pkg"}, wantURLs[1], "")
   147  		checkNPMRegistryRequest(t, config, []string{"@global/pkg"}, wantURLs[2], "")
   148  		checkNPMRegistryRequest(t, config, []string{"@user/pkg"}, wantURLs[3], "")
   149  		checkNPMRegistryRequest(t, config, []string{"@project/pkg"}, wantURLs[4], "")
   150  	}
   151  
   152  	npmrcFiles := makeBlankNpmrcFiles(t)
   153  	writeToNpmrc(t, npmrcFiles.project, "@project:registry=https://project.registry.com")
   154  	writeToNpmrc(t, npmrcFiles.user, "@user:registry=https://user.registry.com")
   155  	writeToNpmrc(t, npmrcFiles.global,
   156  		"@global:registry=https://global.registry.com",
   157  		"@general:registry=https://general.global.registry.com",
   158  		"registry=https://global.registry.com",
   159  	)
   160  	wantURLs := [5]string{
   161  		"https://global.registry.com/pkg",
   162  		"https://general.global.registry.com/@general%2fpkg",
   163  		"https://global.registry.com/@global%2fpkg",
   164  		"https://user.registry.com/@user%2fpkg",
   165  		"https://project.registry.com/@project%2fpkg",
   166  	}
   167  	check(t, npmrcFiles, wantURLs)
   168  
   169  	// override global in user
   170  	writeToNpmrc(t, npmrcFiles.user,
   171  		"@general:registry=https://general.user.registry.com",
   172  		"registry=https://user.registry.com",
   173  	)
   174  	wantURLs[0] = "https://user.registry.com/pkg"
   175  	wantURLs[1] = "https://general.user.registry.com/@general%2fpkg"
   176  	check(t, npmrcFiles, wantURLs)
   177  
   178  	// override global/user in project
   179  	writeToNpmrc(t, npmrcFiles.project,
   180  		"@general:registry=https://general.project.registry.com",
   181  		"registry=https://project.registry.com",
   182  	)
   183  	wantURLs[0] = "https://project.registry.com/pkg"
   184  	wantURLs[1] = "https://general.project.registry.com/@general%2fpkg"
   185  	check(t, npmrcFiles, wantURLs)
   186  
   187  	// override global/user/project in environment variable
   188  	t.Setenv("NPM_CONFIG_REGISTRY", "https://environ.registry.com")
   189  	wantURLs[0] = "https://environ.registry.com/pkg"
   190  	check(t, npmrcFiles, wantURLs)
   191  }
   192  
   193  func TestNPMRegistryAuths(t *testing.T) {
   194  	b64enc := func(s string) string {
   195  		t.Helper()
   196  		return base64.StdEncoding.EncodeToString([]byte(s))
   197  	}
   198  	tests := []struct {
   199  		name       string
   200  		config     datasource.NpmrcConfig
   201  		requestURL string
   202  		wantAuth   string
   203  	}{
   204  		// Auth tests adapted from npm-registry-fetch
   205  		// https://github.com/npm/npm-registry-fetch/blob/237d33b45396caa00add61e0549cf09fbf9deb4f/test/auth.js
   206  		{
   207  			name: "basic_auth",
   208  			config: datasource.NpmrcConfig{
   209  				"//my.custom.registry/here/:username":  "user",
   210  				"//my.custom.registry/here/:_password": b64enc("pass"),
   211  			},
   212  			requestURL: "https://my.custom.registry/here/",
   213  			wantAuth:   "Basic " + b64enc("user:pass"),
   214  		},
   215  		{
   216  			name: "token_auth",
   217  			config: datasource.NpmrcConfig{
   218  				"//my.custom.registry/here/:_authToken": "c0ffee",
   219  				"//my.custom.registry/here/:token":      "nope",
   220  				"//my.custom.registry/:_authToken":      "7ea",
   221  				"//my.custom.registry/:token":           "nope",
   222  			},
   223  			requestURL: "https://my.custom.registry/here//foo/-/foo.tgz",
   224  			wantAuth:   "Bearer c0ffee",
   225  		},
   226  		{
   227  			name: "_auth_auth",
   228  			config: datasource.NpmrcConfig{
   229  				"//my.custom.registry/:_auth":      "decafbad",
   230  				"//my.custom.registry/here/:_auth": "c0ffee",
   231  			},
   232  			requestURL: "https://my.custom.registry/here//asdf/foo/bard/baz",
   233  			wantAuth:   "Basic c0ffee",
   234  		},
   235  		{
   236  			name: "_auth_username:pass_auth",
   237  			config: datasource.NpmrcConfig{
   238  				"//my.custom.registry/here/:_auth": b64enc("foo:bar"),
   239  			},
   240  			requestURL: "https://my.custom.registry/here/",
   241  			wantAuth:   "Basic " + b64enc("foo:bar"),
   242  		},
   243  		{
   244  			name: "ignore_user/pass_when__auth_is_set",
   245  			config: datasource.NpmrcConfig{
   246  				"//registry/:_auth":     b64enc("not:foobar"),
   247  				"//registry/:username":  "foo",
   248  				"//registry/:_password": b64enc("bar"),
   249  			},
   250  			requestURL: "http://registry/pkg/-/pkg-1.2.3.tgz",
   251  			wantAuth:   "Basic " + b64enc("not:foobar"),
   252  		},
   253  		{
   254  			name: "different_hosts_for_uri_vs_registry",
   255  			config: datasource.NpmrcConfig{
   256  				"//my.custom.registry/here/:_authToken": "c0ffee",
   257  				"//my.custom.registry/here/:token":      "nope",
   258  			},
   259  			requestURL: "https://some.other.host/",
   260  			wantAuth:   "",
   261  		},
   262  		{
   263  			name: "do_not_be_thrown_by_other_weird_configs",
   264  			config: datasource.NpmrcConfig{
   265  				"@asdf:_authToken":                 "does this work?",
   266  				"//registry.npmjs.org:_authToken":  "do not share this",
   267  				"_authToken":                       "definitely do not share this, either",
   268  				"//localhost:15443:_authToken":     "wrong",
   269  				"//localhost:15443/foo:_authToken": "correct bearer token",
   270  				"//localhost:_authToken":           "not this one",
   271  				"//other-registry:_authToken":      "this should not be used",
   272  				"@asdf:registry":                   "https://other-registry/",
   273  			},
   274  			requestURL: "http://localhost:15443/foo/@asdf/bar/-/bar-1.2.3.tgz",
   275  			wantAuth:   "Bearer correct bearer token",
   276  		},
   277  		// Some extra tests, based on experimentation with npm config
   278  		{
   279  			name: "exact_package_path_uri",
   280  			config: datasource.NpmrcConfig{
   281  				"//custom.registry/:_authToken":         "less specific match",
   282  				"//custom.registry/package:_authToken":  "exact match",
   283  				"//custom.registry/package/:_authToken": "no match trailing slash",
   284  			},
   285  			requestURL: "http://custom.registry/package",
   286  			wantAuth:   "Bearer exact match",
   287  		},
   288  		{
   289  			name: "percent-encoding_case-sensitivity",
   290  			config: datasource.NpmrcConfig{
   291  				"//custom.registry/:_authToken":                 "expected",
   292  				"//custom.registry/@scope%2Fpackage:_authToken": "bad config",
   293  			},
   294  			requestURL: "http://custom.registry/@scope%2fpackage",
   295  			wantAuth:   "Bearer expected",
   296  		},
   297  		{
   298  			name: "require_both_user_and_pass",
   299  			config: datasource.NpmrcConfig{
   300  				"//custom.registry/:_authToken":  "fallback",
   301  				"//custom.registry/foo:username": "user",
   302  			},
   303  			requestURL: "https://custom.registry/foo/bar",
   304  			wantAuth:   "Bearer fallback",
   305  		},
   306  		{
   307  			name: "don't_inherit_username",
   308  			config: datasource.NpmrcConfig{
   309  				"//custom.registry/:_authToken":       "fallback",
   310  				"//custom.registry/foo:username":      "user",
   311  				"//custom.registry/foo/bar:_password": b64enc("pass"),
   312  			},
   313  			requestURL: "https://custom.registry/foo/bar/baz",
   314  			wantAuth:   "Bearer fallback",
   315  		},
   316  	}
   317  	for _, tt := range tests {
   318  		t.Run(tt.name, func(t *testing.T) {
   319  			config := datasource.ParseNPMRegistryInfo(tt.config)
   320  			// Send off requests to mockTransport to see the auth headers being added.
   321  			mt := &mockTransport{}
   322  			httpClient := &http.Client{Transport: mt}
   323  			resp, err := config.Auths.GetAuth(tt.requestURL).Get(t.Context(), httpClient, tt.requestURL)
   324  			if err != nil {
   325  				t.Fatalf("error making request: %v", err)
   326  			}
   327  			defer resp.Body.Close()
   328  			if len(mt.Requests) != 1 {
   329  				t.Fatalf("unexpected number of requests made: %v", len(mt.Requests))
   330  			}
   331  			header := mt.Requests[0].Header
   332  			if got := header.Get("Authorization"); got != tt.wantAuth {
   333  				t.Errorf("authorization header got = \"%s\", want \"%s\"", got, tt.wantAuth)
   334  			}
   335  		})
   336  	}
   337  }