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 }