cuelang.org/go@v0.10.1/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 TestNotFound(t *testing.T) { 135 // Check that we get appropriate not-found behavior when the 136 // HTTP response isn't entirely according to spec. 137 // See https://cuelang.org/issue/2982 for an example. 138 var writeResponse func(w http.ResponseWriter) 139 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 140 writeResponse(w) 141 })) 142 defer srv.Close() 143 u, _ := url.Parse(srv.URL) 144 reg, err := ociclient.New(u.Host, &ociclient.Options{ 145 Insecure: true, 146 }) 147 qt.Assert(t, qt.IsNil(err)) 148 client := NewClient(reg) 149 checkNotFound := func(writeResp func(w http.ResponseWriter)) { 150 ctx := context.Background() 151 writeResponse = writeResp 152 mv := module.MustNewVersion("foo.com/bar@v1", "v1.2.3") 153 _, err := client.GetModule(ctx, mv) 154 qt.Assert(t, qt.ErrorIs(err, ErrNotFound)) 155 versions, err := client.ModuleVersions(ctx, "foo/bar") 156 qt.Assert(t, qt.IsNil(err)) 157 qt.Assert(t, qt.HasLen(versions, 0)) 158 } 159 160 checkNotFound(func(w http.ResponseWriter) { 161 // issue 2982 162 w.WriteHeader(http.StatusNotFound) 163 w.Write([]byte(`{"errors":[{"code":"NOT_FOUND","message":"repository playground/cue/github.com not found"}]}`)) 164 }) 165 checkNotFound(func(w http.ResponseWriter) { 166 w.WriteHeader(http.StatusForbidden) 167 w.Write([]byte(`some other message`)) 168 }) 169 } 170 171 func TestPutWithMetadata(t *testing.T) { 172 const testMod = ` 173 -- cue.mod/module.cue -- 174 module: "foo.com/bar@v0" 175 language: version: "v0.8.0" 176 177 -- x.cue -- 178 package bar 179 ` 180 ctx := context.Background() 181 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 182 c := newTestClient(t) 183 zipData := createZip(t, mv, testMod) 184 meta := &Metadata{ 185 VCSType: "git", 186 VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 187 VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 188 } 189 err := c.PutModuleWithMetadata(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 190 qt.Assert(t, qt.IsNil(err)) 191 192 m, err := c.GetModule(ctx, mv) 193 qt.Assert(t, qt.IsNil(err)) 194 195 gotMeta, err := m.Metadata() 196 qt.Assert(t, qt.IsNil(err)) 197 qt.Assert(t, qt.DeepEquals(gotMeta, meta)) 198 } 199 200 func TestPutWithInvalidMetadata(t *testing.T) { 201 const testMod = ` 202 -- cue.mod/module.cue -- 203 module: "foo.com/bar@v0" 204 language: version: "v0.8.0" 205 206 -- x.cue -- 207 package bar 208 ` 209 ctx := context.Background() 210 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 211 c := newTestClient(t) 212 zipData := createZip(t, mv, testMod) 213 meta := &Metadata{ 214 // Missing VCSType field. 215 VCSCommit: "2ff5afa7cda41bf030654ab03caeba3fadf241ae", 216 VCSCommitTime: time.Date(2024, 4, 23, 15, 16, 17, 0, time.UTC), 217 } 218 err := c.PutModuleWithMetadata(ctx, mv, bytes.NewReader(zipData), int64(len(zipData)), meta) 219 qt.Assert(t, qt.ErrorMatches(err, `invalid metadata: empty metadata value for field "org.cuelang.vcs-type"`)) 220 } 221 222 func TestGetModuleWithManifest(t *testing.T) { 223 const testMod = ` 224 -- cue.mod/module.cue -- 225 module: "foo.com/bar@v0" 226 language: version: "v0.8.0" 227 deps: "example.com@v1": v: "v1.2.3" 228 deps: "other.com/something@v0": v: "v0.2.3" 229 230 -- x.cue -- 231 package bar 232 233 import ( 234 a "example.com" 235 "other.com/something" 236 ) 237 x: a.foo + something.bar 238 ` 239 ctx := context.Background() 240 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 241 // Note that we delete a tag below, so we want a mutable registry. 242 reg := ocimem.NewWithConfig(&ocimem.Config{ImmutableTags: false}) 243 244 c := NewClient(reg) 245 zipData := putModule(t, c, mv, testMod) 246 247 mr, err := reg.GetTag(ctx, "foo.com/bar", "v0.5.100") 248 qt.Assert(t, qt.IsNil(err)) 249 defer mr.Close() 250 mdata, err := io.ReadAll(mr) 251 qt.Assert(t, qt.IsNil(err)) 252 253 // Remove the tag so that we're sure it isn't 254 // used for the GetModuleWithManifest call. 255 err = reg.DeleteTag(ctx, "foo.com/bar", "v0.5.100") 256 qt.Assert(t, qt.IsNil(err)) 257 258 m, err := c.GetModuleWithManifest(mv, mdata, "application/json") 259 qt.Assert(t, qt.IsNil(err)) 260 261 r, err := m.GetZip(ctx) 262 qt.Assert(t, qt.IsNil(err)) 263 data, err := io.ReadAll(r) 264 qt.Assert(t, qt.IsNil(err)) 265 qt.Assert(t, qt.DeepEquals(data, zipData)) 266 } 267 268 func TestPutWithInvalidDependencyVersion(t *testing.T) { 269 const testMod = ` 270 -- cue.mod/module.cue -- 271 module: "foo.com/bar@v0" 272 language: version: "v0.8.0" 273 deps: "example.com@v1": v: "v1.2" 274 275 -- x.cue -- 276 x: 42 277 ` 278 mv := module.MustParseVersion("foo.com/bar@v0.5.100") 279 c := newTestClient(t) 280 zipData := createZip(t, mv, testMod) 281 err := c.PutModule(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData))) 282 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`)) 283 } 284 285 var checkModuleTests = []struct { 286 testName string 287 mv module.Version 288 content string 289 wantError string 290 }{{ 291 testName: "Minimal", 292 mv: module.MustNewVersion("foo.com/bar", "v0.1.2"), 293 content: ` 294 -- cue.mod/module.cue -- 295 module: "foo.com/bar@v0" 296 language: version: "v0.8.0" 297 `, 298 }, { 299 testName: "MismatchedMajorVersion", 300 mv: module.MustNewVersion("foo.com/bar", "v0.1.2"), 301 content: ` 302 -- cue.mod/module.cue -- 303 module: "foo.com/bar@v1" 304 language: version: "v0.8.0" 305 `, 306 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"`, 307 }, { 308 testName: "ModuleWithMinorVersion", 309 mv: module.MustNewVersion("foo.com/bar", "v1.2.3"), 310 content: ` 311 -- cue.mod/module.cue -- 312 module: "foo@v1.2.3" 313 language: version: "v0.8.0" 314 `, 315 wantError: `module.cue file check failed: module path foo@v1.2.3 in "cue.mod/module.cue" should contain the major version only`, 316 }, { 317 testName: "DependencyWithInvalidVersion", 318 mv: module.MustNewVersion("foo.com/bar", "v1.2.3"), 319 content: ` 320 -- cue.mod/module.cue -- 321 module: "foo@v1" 322 language: version: "v0.8.0" 323 deps: "foo.com/bar@v2": v: "invalid" 324 `, 325 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`, 326 }} 327 328 func TestCheckModule(t *testing.T) { 329 for _, test := range checkModuleTests { 330 t.Run(test.testName, func(t *testing.T) { 331 data := createZip(t, test.mv, test.content) 332 m, err := checkModule(test.mv, bytes.NewReader(data), int64(len(data))) 333 if test.wantError != "" { 334 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 335 return 336 } 337 qt.Assert(t, qt.IsNil(err)) 338 qt.Assert(t, qt.Not(qt.IsNil(m))) 339 qt.Assert(t, qt.DeepEquals(m.mv, test.mv)) 340 }) 341 } 342 } 343 344 func TestModuleVersionsOnNonExistentModule(t *testing.T) { 345 c := newTestClient(t) 346 ctx := context.Background() 347 tags, err := c.ModuleVersions(ctx, "not/there@v0") 348 qt.Assert(t, qt.IsNil(err)) 349 qt.Assert(t, qt.HasLen(tags, 0)) 350 351 // Bad names hit a slightly different code path, so make 352 // sure they work OK too. 353 tags, err = c.ModuleVersions(ctx, "bad--NAME-@v0") 354 qt.Assert(t, qt.IsNil(err)) 355 qt.Assert(t, qt.HasLen(tags, 0)) 356 } 357 358 func putModule(t *testing.T, c *Client, mv module.Version, txtarData string) []byte { 359 zipData := createZip(t, mv, txtarData) 360 err := c.PutModule(context.Background(), mv, bytes.NewReader(zipData), int64(len(zipData))) 361 qt.Assert(t, qt.IsNil(err)) 362 return zipData 363 } 364 365 func createZip(t *testing.T, mv module.Version, txtarData string) []byte { 366 ar := txtar.Parse([]byte(txtarData)) 367 var zipContent bytes.Buffer 368 err := modzip.Create(&zipContent, mv, ar.Files, txtarFileIO{}) 369 qt.Assert(t, qt.IsNil(err)) 370 return zipContent.Bytes() 371 } 372 373 type txtarFileIO struct{} 374 375 func (txtarFileIO) Path(f txtar.File) string { 376 return f.Name 377 } 378 379 func (txtarFileIO) Lstat(f txtar.File) (os.FileInfo, error) { 380 return txtarFileInfo{f}, nil 381 } 382 383 func (txtarFileIO) Open(f txtar.File) (io.ReadCloser, error) { 384 return io.NopCloser(bytes.NewReader(f.Data)), nil 385 } 386 387 func (txtarFileIO) Mode() os.FileMode { 388 return 0o444 389 } 390 391 type txtarFileInfo struct { 392 f txtar.File 393 } 394 395 func (fi txtarFileInfo) Name() string { 396 return path.Base(fi.f.Name) 397 } 398 399 func (fi txtarFileInfo) Size() int64 { 400 return int64(len(fi.f.Data)) 401 } 402 403 func (fi txtarFileInfo) Mode() os.FileMode { 404 return 0o644 405 } 406 407 func (fi txtarFileInfo) ModTime() time.Time { return time.Time{} } 408 func (fi txtarFileInfo) IsDir() bool { return false } 409 func (fi txtarFileInfo) Sys() interface{} { return nil }