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 }