cuelang.org/go@v0.13.0/mod/modregistry/client_test.go (about) 1 // Copyright 2023 CUE Authors 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 modregistry 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "io" 22 "net/http" 23 "net/http/httptest" 24 "net/url" 25 "os" 26 "path" 27 "testing" 28 "time" 29 30 "github.com/go-quicktest/qt" 31 32 "golang.org/x/tools/txtar" 33 34 "cuelabs.dev/go/oci/ociregistry/ociclient" 35 "cuelabs.dev/go/oci/ociregistry/ocimem" 36 37 "cuelang.org/go/internal/mod/semver" 38 "cuelang.org/go/mod/module" 39 "cuelang.org/go/mod/modzip" 40 ) 41 42 func newTestClient(t *testing.T) *Client { 43 return NewClient(ocimem.NewWithConfig(&ocimem.Config{ImmutableTags: true})) 44 } 45 46 func TestPutGetModule(t *testing.T) { 47 const testMod = ` 48 -- cue.mod/module.cue -- 49 module: "example.com/module@v1" 50 language: version: "v0.8.0" 51 52 -- x.cue -- 53 x: 42 54 ` 55 ctx := context.Background() 56 mv := module.MustParseVersion("example.com/module@v1.2.3") 57 c := newTestClient(t) 58 zipData := putModule(t, c, mv, testMod) 59 60 m, err := c.GetModule(ctx, mv) 61 qt.Assert(t, qt.IsNil(err)) 62 63 r, err := m.GetZip(ctx) 64 qt.Assert(t, qt.IsNil(err)) 65 data, err := io.ReadAll(r) 66 qt.Assert(t, qt.IsNil(err)) 67 qt.Assert(t, qt.DeepEquals(data, zipData)) 68 69 tags, err := c.ModuleVersions(ctx, mv.Path()) 70 qt.Assert(t, qt.IsNil(err)) 71 qt.Assert(t, qt.DeepEquals(tags, []string{"v1.2.3"})) 72 } 73 74 func TestModuleVersions(t *testing.T) { 75 ctx := context.Background() 76 c := newTestClient(t) 77 for _, v := range []string{"v1.0.0", "v2.3.3-alpha", "v1.2.3", "v0.23.676", "v3.2.1"} { 78 mpath := "example.com/module@" + semver.Major(v) 79 modContents := fmt.Sprintf(` 80 -- cue.mod/module.cue -- 81 module: %q 82 language: version: "v0.8.0" 83 84 -- x.cue -- 85 x: 42 86 `, mpath) 87 putModule(t, c, module.MustParseVersion("example.com/module@"+v), modContents) 88 } 89 tags, err := c.ModuleVersions(ctx, "example.com/module") 90 qt.Assert(t, qt.IsNil(err)) 91 qt.Assert(t, qt.DeepEquals(tags, []string{"v0.23.676", "v1.0.0", "v1.2.3", "v2.3.3-alpha", "v3.2.1"})) 92 93 tags, err = c.ModuleVersions(ctx, "example.com/module@v1") 94 qt.Assert(t, qt.IsNil(err)) 95 qt.Assert(t, qt.DeepEquals(tags, []string{"v1.0.0", "v1.2.3"})) 96 } 97 98 func TestPutGetWithDependencies(t *testing.T) { 99 const testMod = ` 100 -- cue.mod/module.cue -- 101 module: "foo.com/bar@v0" 102 language: version: "v0.8.0" 103 deps: "example.com@v1": v: "v1.2.3" 104 deps: "other.com/something@v0": v: "v0.2.3" 105 106 -- x.cue -- 107 package bar 108 109 import ( 110 a "example.com" 111 "other.com/something" 112 ) 113 x: a.foo + something.bar 114 ` 115 ctx := context.Background() 116 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 117 c := newTestClient(t) 118 zipData := putModule(t, c, mv, testMod) 119 120 m, err := c.GetModule(ctx, mv) 121 qt.Assert(t, qt.IsNil(err)) 122 123 r, err := m.GetZip(ctx) 124 qt.Assert(t, qt.IsNil(err)) 125 data, err := io.ReadAll(r) 126 qt.Assert(t, qt.IsNil(err)) 127 qt.Assert(t, qt.DeepEquals(data, zipData)) 128 129 tags, err := c.ModuleVersions(ctx, mv.Path()) 130 qt.Assert(t, qt.IsNil(err)) 131 qt.Assert(t, qt.DeepEquals(tags, []string{"v0.5.100"})) 132 } 133 134 func TestMirror(t *testing.T) { 135 const testMod = ` 136 -- cue.mod/module.cue -- 137 module: "example.com/module@v1" 138 language: version: "v0.8.0" 139 140 -- x.cue -- 141 x: 42 142 ` 143 ctx := context.Background() 144 mv := module.MustParseVersion("example.com/module@v1.2.3") 145 c := newTestClient(t) 146 zipData := putModule(t, c, mv, testMod) 147 148 c2 := newTestClient(t) 149 err := c.Mirror(ctx, c2, mv) 150 qt.Assert(t, qt.IsNil(err)) 151 152 m, err := c2.GetModule(ctx, mv) 153 qt.Assert(t, qt.IsNil(err)) 154 155 r, err := m.GetZip(ctx) 156 qt.Assert(t, qt.IsNil(err)) 157 data, err := io.ReadAll(r) 158 qt.Assert(t, qt.IsNil(err)) 159 qt.Assert(t, qt.DeepEquals(data, zipData)) 160 161 tags, err := c2.ModuleVersions(ctx, mv.Path()) 162 qt.Assert(t, qt.IsNil(err)) 163 qt.Assert(t, qt.DeepEquals(tags, []string{"v1.2.3"})) 164 } 165 166 func TestNotFound(t *testing.T) { 167 // Check that we get appropriate not-found behavior when the 168 // HTTP response isn't entirely according to spec. 169 // See https://cuelang.org/issue/2982 for an example. 170 var writeResponse func(w http.ResponseWriter) 171 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 172 writeResponse(w) 173 })) 174 defer srv.Close() 175 u, _ := url.Parse(srv.URL) 176 reg, err := ociclient.New(u.Host, &ociclient.Options{ 177 Insecure: true, 178 }) 179 qt.Assert(t, qt.IsNil(err)) 180 client := NewClient(reg) 181 checkNotFound := func(writeResp func(w http.ResponseWriter)) { 182 ctx := context.Background() 183 writeResponse = writeResp 184 mv := module.MustNewVersion("foo.com/bar@v1", "v1.2.3") 185 _, err := client.GetModule(ctx, mv) 186 qt.Assert(t, qt.ErrorIs(err, ErrNotFound)) 187 versions, err := client.ModuleVersions(ctx, "foo/bar") 188 qt.Assert(t, qt.IsNil(err)) 189 qt.Assert(t, qt.HasLen(versions, 0)) 190 } 191 192 checkNotFound(func(w http.ResponseWriter) { 193 // issue 2982 194 w.WriteHeader(http.StatusNotFound) 195 w.Write([]byte(`{"errors":[{"code":"NOT_FOUND","message":"repository playground/cue/github.com not found"}]}`)) 196 }) 197 checkNotFound(func(w http.ResponseWriter) { 198 w.WriteHeader(http.StatusForbidden) 199 w.Write([]byte(`some other message`)) 200 }) 201 } 202 203 func TestPutWithMetadata(t *testing.T) { 204 const testMod = ` 205 -- cue.mod/module.cue -- 206 module: "foo.com/bar@v0" 207 language: version: "v0.8.0" 208 209 -- x.cue -- 210 package bar 211 ` 212 ctx := context.Background() 213 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 214 c := newTestClient(t) 215 zipData := createZip(t, mv, testMod) 216 meta := &Metadata{ 217 VCSType: "git", 218 VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 219 VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 220 } 221 err := c.PutModuleWithMetadata(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 222 qt.Assert(t, qt.IsNil(err)) 223 224 m, err := c.GetModule(ctx, mv) 225 qt.Assert(t, qt.IsNil(err)) 226 227 gotMeta, err := m.Metadata() 228 qt.Assert(t, qt.IsNil(err)) 229 qt.Assert(t, qt.DeepEquals(gotMeta, meta)) 230 } 231 232 func TestPutWithInvalidMetadata(t *testing.T) { 233 const testMod = ` 234 -- cue.mod/module.cue -- 235 module: "foo.com/bar@v0" 236 language: version: "v0.8.0" 237 238 -- x.cue -- 239 package bar 240 ` 241 ctx := context.Background() 242 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 243 c := newTestClient(t) 244 zipData := createZip(t, mv, testMod) 245 meta := &Metadata{ 246 // Missing VCSType field. 247 VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 248 VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 249 } 250 err := c.PutModuleWithMetadata(ctx, mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 251 qt.Assert(t, qt.ErrorMatches(err, `invalid metadata: empty metadata value for field "org.cuelang.vcs-type"`)) 252 } 253 254 func TestGetModuleWithManifest(t *testing.T) { 255 const testMod = ` 256 -- cue.mod/module.cue -- 257 module: "foo.com/bar@v0" 258 language: version: "v0.8.0" 259 deps: "example.com@v1": v: "v1.2.3" 260 deps: "other.com/something@v0": v: "v0.2.3" 261 262 -- x.cue -- 263 package bar 264 265 import ( 266 a "example.com" 267 "other.com/something" 268 ) 269 x: a.foo + something.bar 270 ` 271 ctx := context.Background() 272 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 273 // Note that we delete a tag below, so we want a mutable registry. 274 reg := ocimem.NewWithConfig(&ocimem.Config{ImmutableTags: false}) 275 276 c := NewClient(reg) 277 zipData := putModule(t, c, mv, testMod) 278 279 mr, err := reg.GetTag(ctx, "foo.com/bar", "v0.5.100") 280 qt.Assert(t, qt.IsNil(err)) 281 defer mr.Close() 282 mdata, err := io.ReadAll(mr) 283 qt.Assert(t, qt.IsNil(err)) 284 285 // Remove the tag so that we're sure it isn't 286 // used for the GetModuleWithManifest call. 287 err = reg.DeleteTag(ctx, "foo.com/bar", "v0.5.100") 288 qt.Assert(t, qt.IsNil(err)) 289 290 m, err := c.GetModuleWithManifest(mv, mdata, "application/json") 291 qt.Assert(t, qt.IsNil(err)) 292 293 r, err := m.GetZip(ctx) 294 qt.Assert(t, qt.IsNil(err)) 295 data, err := io.ReadAll(r) 296 qt.Assert(t, qt.IsNil(err)) 297 qt.Assert(t, qt.DeepEquals(data, zipData)) 298 } 299 300 func TestPutWithInvalidDependencyVersion(t *testing.T) { 301 const testMod = ` 302 -- cue.mod/module.cue -- 303 module: "foo.com/bar@v0" 304 language: version: "v0.8.0" 305 deps: "example.com@v1": v: "v1.2" 306 307 -- x.cue -- 308 x: 42 309 ` 310 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 311 c := newTestClient(t) 312 zipData := createZip(t, mv, testMod) 313 err := c.PutModule(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData))) 314 qt.Assert(t, qt.ErrorMatches(err, `module.cue file check failed: invalid module.cue file cue.mod/module.cue: cannot make version from module "example.com@v1", version "v1.2": version "v1.2" \(of module "example.com@v1"\) is not canonical`)) 315 } 316 317 var checkModuleTests = []struct { 318 testName string 319 mv module.Version 320 content string 321 wantError string 322 }{{ 323 testName: "Minimal", 324 mv: module.MustNewVersion("foo.com/bar", "v0.1.2"), 325 content: ` 326 -- cue.mod/module.cue -- 327 module: "foo.com/bar@v0" 328 language: version: "v0.8.0" 329 `, 330 }, { 331 testName: "MismatchedMajorVersion", 332 mv: module.MustNewVersion("foo.com/bar", "v0.1.2"), 333 content: ` 334 -- cue.mod/module.cue -- 335 module: "foo.com/bar@v1" 336 language: version: "v0.8.0" 337 `, 338 wantError: `module.cue file check failed: module path "foo.com/bar@v1" found in cue.mod/module.cue does not match module path being published "foo.com/bar@v0"`, 339 }, { 340 testName: "ModuleWithMinorVersion", 341 mv: module.MustNewVersion("foo.com/bar", "v1.2.3"), 342 content: ` 343 -- cue.mod/module.cue -- 344 module: "foo@v1.2.3" 345 language: version: "v0.8.0" 346 `, 347 wantError: `module.cue file check failed: module path foo@v1.2.3 in "cue.mod/module.cue" should contain the major version only`, 348 }, { 349 testName: "DependencyWithInvalidVersion", 350 mv: module.MustNewVersion("foo.com/bar", "v1.2.3"), 351 content: ` 352 -- cue.mod/module.cue -- 353 module: "foo@v1" 354 language: version: "v0.8.0" 355 deps: "foo.com/bar@v2": v: "invalid" 356 `, 357 wantError: `module.cue file check failed: invalid module.cue file cue.mod/module.cue: cannot make version from module "foo.com/bar@v2", version "invalid": version "invalid" \(of module "foo.com/bar@v2"\) is not well formed`, 358 }} 359 360 func TestCheckModule(t *testing.T) { 361 for _, test := range checkModuleTests { 362 t.Run(test.testName, func(t *testing.T) { 363 data := createZip(t, test.mv, test.content) 364 m, err := checkModule(test.mv, bytes.NewReader(data), int64(len(data))) 365 if test.wantError != "" { 366 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 367 return 368 } 369 qt.Assert(t, qt.IsNil(err)) 370 qt.Assert(t, qt.Not(qt.IsNil(m))) 371 qt.Assert(t, qt.DeepEquals(m.mv, test.mv)) 372 }) 373 } 374 } 375 376 func TestModuleVersionsOnNonExistentModule(t *testing.T) { 377 c := newTestClient(t) 378 ctx := context.Background() 379 tags, err := c.ModuleVersions(ctx, "not/there@v0") 380 qt.Assert(t, qt.IsNil(err)) 381 qt.Assert(t, qt.HasLen(tags, 0)) 382 383 // Bad names hit a slightly different code path, so make 384 // sure they work OK too. 385 tags, err = c.ModuleVersions(ctx, "bad--NAME-@v0") 386 qt.Assert(t, qt.IsNil(err)) 387 qt.Assert(t, qt.HasLen(tags, 0)) 388 } 389 390 func putModule(t *testing.T, c *Client, mv module.Version, txtarData string) []byte { 391 zipData := createZip(t, mv, txtarData) 392 err := c.PutModule(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData))) 393 qt.Assert(t, qt.IsNil(err)) 394 return zipData 395 } 396 397 func createZip(t *testing.T, mv module.Version, txtarData string) []byte { 398 ar := txtar.Parse([]byte(txtarData)) 399 var zipContent bytes.Buffer 400 err := modzip.Create(&zipContent, mv, ar.Files, txtarFileIO{}) 401 qt.Assert(t, qt.IsNil(err)) 402 return zipContent.Bytes() 403 } 404 405 type txtarFileIO struct{} 406 407 func (txtarFileIO) Path(f txtar.File) string { 408 return f.Name 409 } 410 411 func (txtarFileIO) Lstat(f txtar.File) (os.FileInfo, error) { 412 return txtarFileInfo{f}, nil 413 } 414 415 func (txtarFileIO) Open(f txtar.File) (io.ReadCloser, error) { 416 return io.NopCloser(bytes.NewReader(f.Data)), nil 417 } 418 419 func (txtarFileIO) Mode() os.FileMode { 420 return 0o444 421 } 422 423 type txtarFileInfo struct { 424 f txtar.File 425 } 426 427 func (fi txtarFileInfo) Name() string { 428 return path.Base(fi.f.Name) 429 } 430 431 func (fi txtarFileInfo) Size() int64 { 432 return int64(len(fi.f.Data)) 433 } 434 435 func (fi txtarFileInfo) Mode() os.FileMode { 436 return 0o644 437 } 438 439 func (fi txtarFileInfo) ModTime() time.Time { return time.Time{} } 440 func (fi txtarFileInfo) IsDir() bool { return false } 441 func (fi txtarFileInfo) Sys() interface{} { return nil }