...

Source file src/github.com/docker/distribution/registry/client/repository_test.go

Documentation: github.com/docker/distribution/registry/client

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"reflect"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/distribution/reference"
    20  	"github.com/docker/distribution"
    21  	"github.com/docker/distribution/context"
    22  	"github.com/docker/distribution/manifest"
    23  	"github.com/docker/distribution/manifest/schema1"
    24  	"github.com/docker/distribution/registry/api/errcode"
    25  	v2 "github.com/docker/distribution/registry/api/v2"
    26  	"github.com/docker/distribution/testutil"
    27  	"github.com/docker/distribution/uuid"
    28  	"github.com/docker/libtrust"
    29  	"github.com/opencontainers/go-digest"
    30  )
    31  
    32  func testServer(rrm testutil.RequestResponseMap) (string, func()) {
    33  	h := testutil.NewHandler(rrm)
    34  	s := httptest.NewServer(h)
    35  	return s.URL, s.Close
    36  }
    37  
    38  func newRandomBlob(size int) (digest.Digest, []byte) {
    39  	b := make([]byte, size)
    40  	if n, err := rand.Read(b); err != nil {
    41  		panic(err)
    42  	} else if n != size {
    43  		panic("unable to read enough bytes")
    44  	}
    45  
    46  	return digest.FromBytes(b), b
    47  }
    48  
    49  func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
    50  	*m = append(*m, testutil.RequestResponseMapping{
    51  		Request: testutil.Request{
    52  			Method: "GET",
    53  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    54  		},
    55  		Response: testutil.Response{
    56  			StatusCode: http.StatusOK,
    57  			Body:       content,
    58  			Headers: http.Header(map[string][]string{
    59  				"Content-Length": {fmt.Sprint(len(content))},
    60  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    61  			}),
    62  		},
    63  	})
    64  
    65  	*m = append(*m, testutil.RequestResponseMapping{
    66  		Request: testutil.Request{
    67  			Method: "HEAD",
    68  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    69  		},
    70  		Response: testutil.Response{
    71  			StatusCode: http.StatusOK,
    72  			Headers: http.Header(map[string][]string{
    73  				"Content-Length": {fmt.Sprint(len(content))},
    74  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    75  			}),
    76  		},
    77  	})
    78  }
    79  
    80  func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
    81  	headers := map[string][]string{
    82  		"Content-Length": {strconv.Itoa(len(content))},
    83  		"Content-Type":   {"application/json; charset=utf-8"},
    84  	}
    85  	if link != "" {
    86  		headers["Link"] = append(headers["Link"], link)
    87  	}
    88  
    89  	*m = append(*m, testutil.RequestResponseMapping{
    90  		Request: testutil.Request{
    91  			Method: "GET",
    92  			Route:  route,
    93  		},
    94  		Response: testutil.Response{
    95  			StatusCode: http.StatusOK,
    96  			Body:       content,
    97  			Headers:    http.Header(headers),
    98  		},
    99  	})
   100  }
   101  
   102  func TestBlobDelete(t *testing.T) {
   103  	dgst, _ := newRandomBlob(1024)
   104  	var m testutil.RequestResponseMap
   105  	repo, _ := reference.WithName("test.example.com/repo1")
   106  	m = append(m, testutil.RequestResponseMapping{
   107  		Request: testutil.Request{
   108  			Method: "DELETE",
   109  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   110  		},
   111  		Response: testutil.Response{
   112  			StatusCode: http.StatusAccepted,
   113  			Headers: http.Header(map[string][]string{
   114  				"Content-Length": {"0"},
   115  			}),
   116  		},
   117  	})
   118  
   119  	e, c := testServer(m)
   120  	defer c()
   121  
   122  	ctx := context.Background()
   123  	r, err := NewRepository(repo, e, nil)
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  	l := r.Blobs(ctx)
   128  	err = l.Delete(ctx, dgst)
   129  	if err != nil {
   130  		t.Errorf("Error deleting blob: %s", err.Error())
   131  	}
   132  
   133  }
   134  
   135  func TestBlobFetch(t *testing.T) {
   136  	d1, b1 := newRandomBlob(1024)
   137  	var m testutil.RequestResponseMap
   138  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   139  
   140  	e, c := testServer(m)
   141  	defer c()
   142  
   143  	ctx := context.Background()
   144  	repo, _ := reference.WithName("test.example.com/repo1")
   145  	r, err := NewRepository(repo, e, nil)
   146  	if err != nil {
   147  		t.Fatal(err)
   148  	}
   149  	l := r.Blobs(ctx)
   150  
   151  	b, err := l.Get(ctx, d1)
   152  	if err != nil {
   153  		t.Fatal(err)
   154  	}
   155  	if !bytes.Equal(b, b1) {
   156  		t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
   157  	}
   158  
   159  	// TODO(dmcgowan): Test for unknown blob case
   160  }
   161  
   162  func TestBlobExistsNoContentLength(t *testing.T) {
   163  	var m testutil.RequestResponseMap
   164  
   165  	repo, _ := reference.WithName("biff")
   166  	dgst, content := newRandomBlob(1024)
   167  	m = append(m, testutil.RequestResponseMapping{
   168  		Request: testutil.Request{
   169  			Method: "GET",
   170  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   171  		},
   172  		Response: testutil.Response{
   173  			StatusCode: http.StatusOK,
   174  			Body:       content,
   175  			Headers: http.Header(map[string][]string{
   176  				//			"Content-Length": {fmt.Sprint(len(content))},
   177  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   178  			}),
   179  		},
   180  	})
   181  
   182  	m = append(m, testutil.RequestResponseMapping{
   183  		Request: testutil.Request{
   184  			Method: "HEAD",
   185  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   186  		},
   187  		Response: testutil.Response{
   188  			StatusCode: http.StatusOK,
   189  			Headers: http.Header(map[string][]string{
   190  				//			"Content-Length": {fmt.Sprint(len(content))},
   191  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   192  			}),
   193  		},
   194  	})
   195  	e, c := testServer(m)
   196  	defer c()
   197  
   198  	ctx := context.Background()
   199  	r, err := NewRepository(repo, e, nil)
   200  	if err != nil {
   201  		t.Fatal(err)
   202  	}
   203  	l := r.Blobs(ctx)
   204  
   205  	_, err = l.Stat(ctx, dgst)
   206  	if err == nil {
   207  		t.Fatal(err)
   208  	}
   209  	if !strings.Contains(err.Error(), "missing content-length heade") {
   210  		t.Fatalf("Expected missing content-length error message")
   211  	}
   212  
   213  }
   214  
   215  func TestBlobExists(t *testing.T) {
   216  	d1, b1 := newRandomBlob(1024)
   217  	var m testutil.RequestResponseMap
   218  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   219  
   220  	e, c := testServer(m)
   221  	defer c()
   222  
   223  	ctx := context.Background()
   224  	repo, _ := reference.WithName("test.example.com/repo1")
   225  	r, err := NewRepository(repo, e, nil)
   226  	if err != nil {
   227  		t.Fatal(err)
   228  	}
   229  	l := r.Blobs(ctx)
   230  
   231  	stat, err := l.Stat(ctx, d1)
   232  	if err != nil {
   233  		t.Fatal(err)
   234  	}
   235  
   236  	if stat.Digest != d1 {
   237  		t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1)
   238  	}
   239  
   240  	if stat.Size != int64(len(b1)) {
   241  		t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1))
   242  	}
   243  
   244  	// TODO(dmcgowan): Test error cases and ErrBlobUnknown case
   245  }
   246  
   247  func TestBlobUploadChunked(t *testing.T) {
   248  	dgst, b1 := newRandomBlob(1024)
   249  	var m testutil.RequestResponseMap
   250  	chunks := [][]byte{
   251  		b1[0:256],
   252  		b1[256:512],
   253  		b1[512:513],
   254  		b1[513:1024],
   255  	}
   256  	repo, _ := reference.WithName("test.example.com/uploadrepo")
   257  	uuids := []string{uuid.Generate().String()}
   258  	m = append(m, testutil.RequestResponseMapping{
   259  		Request: testutil.Request{
   260  			Method: "POST",
   261  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/",
   262  		},
   263  		Response: testutil.Response{
   264  			StatusCode: http.StatusAccepted,
   265  			Headers: http.Header(map[string][]string{
   266  				"Content-Length":     {"0"},
   267  				"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[0]},
   268  				"Docker-Upload-UUID": {uuids[0]},
   269  				"Range":              {"0-0"},
   270  			}),
   271  		},
   272  	})
   273  	offset := 0
   274  	for i, chunk := range chunks {
   275  		uuids = append(uuids, uuid.Generate().String())
   276  		newOffset := offset + len(chunk)
   277  		m = append(m, testutil.RequestResponseMapping{
   278  			Request: testutil.Request{
   279  				Method: "PATCH",
   280  				Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i],
   281  				Body:   chunk,
   282  			},
   283  			Response: testutil.Response{
   284  				StatusCode: http.StatusAccepted,
   285  				Headers: http.Header(map[string][]string{
   286  					"Content-Length":     {"0"},
   287  					"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i+1]},
   288  					"Docker-Upload-UUID": {uuids[i+1]},
   289  					"Range":              {fmt.Sprintf("%d-%d", offset, newOffset-1)},
   290  				}),
   291  			},
   292  		})
   293  		offset = newOffset
   294  	}
   295  	m = append(m, testutil.RequestResponseMapping{
   296  		Request: testutil.Request{
   297  			Method: "PUT",
   298  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[len(uuids)-1],
   299  			QueryParams: map[string][]string{
   300  				"digest": {dgst.String()},
   301  			},
   302  		},
   303  		Response: testutil.Response{
   304  			StatusCode: http.StatusCreated,
   305  			Headers: http.Header(map[string][]string{
   306  				"Content-Length":        {"0"},
   307  				"Docker-Content-Digest": {dgst.String()},
   308  				"Content-Range":         {fmt.Sprintf("0-%d", offset-1)},
   309  			}),
   310  		},
   311  	})
   312  	m = append(m, testutil.RequestResponseMapping{
   313  		Request: testutil.Request{
   314  			Method: "HEAD",
   315  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   316  		},
   317  		Response: testutil.Response{
   318  			StatusCode: http.StatusOK,
   319  			Headers: http.Header(map[string][]string{
   320  				"Content-Length": {fmt.Sprint(offset)},
   321  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   322  			}),
   323  		},
   324  	})
   325  
   326  	e, c := testServer(m)
   327  	defer c()
   328  
   329  	ctx := context.Background()
   330  	r, err := NewRepository(repo, e, nil)
   331  	if err != nil {
   332  		t.Fatal(err)
   333  	}
   334  	l := r.Blobs(ctx)
   335  
   336  	upload, err := l.Create(ctx)
   337  	if err != nil {
   338  		t.Fatal(err)
   339  	}
   340  
   341  	if upload.ID() != uuids[0] {
   342  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0])
   343  	}
   344  
   345  	for _, chunk := range chunks {
   346  		n, err := upload.Write(chunk)
   347  		if err != nil {
   348  			t.Fatal(err)
   349  		}
   350  		if n != len(chunk) {
   351  			t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk))
   352  		}
   353  	}
   354  
   355  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   356  		Digest: dgst,
   357  		Size:   int64(len(b1)),
   358  	})
   359  	if err != nil {
   360  		t.Fatal(err)
   361  	}
   362  
   363  	if blob.Size != int64(len(b1)) {
   364  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   365  	}
   366  }
   367  
   368  func TestBlobUploadMonolithic(t *testing.T) {
   369  	dgst, b1 := newRandomBlob(1024)
   370  	var m testutil.RequestResponseMap
   371  	repo, _ := reference.WithName("test.example.com/uploadrepo")
   372  	uploadID := uuid.Generate().String()
   373  	m = append(m, testutil.RequestResponseMapping{
   374  		Request: testutil.Request{
   375  			Method: "POST",
   376  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/",
   377  		},
   378  		Response: testutil.Response{
   379  			StatusCode: http.StatusAccepted,
   380  			Headers: http.Header(map[string][]string{
   381  				"Content-Length":     {"0"},
   382  				"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
   383  				"Docker-Upload-UUID": {uploadID},
   384  				"Range":              {"0-0"},
   385  			}),
   386  		},
   387  	})
   388  	m = append(m, testutil.RequestResponseMapping{
   389  		Request: testutil.Request{
   390  			Method: "PATCH",
   391  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
   392  			Body:   b1,
   393  		},
   394  		Response: testutil.Response{
   395  			StatusCode: http.StatusAccepted,
   396  			Headers: http.Header(map[string][]string{
   397  				"Location":              {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
   398  				"Docker-Upload-UUID":    {uploadID},
   399  				"Content-Length":        {"0"},
   400  				"Docker-Content-Digest": {dgst.String()},
   401  				"Range":                 {fmt.Sprintf("0-%d", len(b1)-1)},
   402  			}),
   403  		},
   404  	})
   405  	m = append(m, testutil.RequestResponseMapping{
   406  		Request: testutil.Request{
   407  			Method: "PUT",
   408  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
   409  			QueryParams: map[string][]string{
   410  				"digest": {dgst.String()},
   411  			},
   412  		},
   413  		Response: testutil.Response{
   414  			StatusCode: http.StatusCreated,
   415  			Headers: http.Header(map[string][]string{
   416  				"Content-Length":        {"0"},
   417  				"Docker-Content-Digest": {dgst.String()},
   418  				"Content-Range":         {fmt.Sprintf("0-%d", len(b1)-1)},
   419  			}),
   420  		},
   421  	})
   422  	m = append(m, testutil.RequestResponseMapping{
   423  		Request: testutil.Request{
   424  			Method: "HEAD",
   425  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   426  		},
   427  		Response: testutil.Response{
   428  			StatusCode: http.StatusOK,
   429  			Headers: http.Header(map[string][]string{
   430  				"Content-Length": {fmt.Sprint(len(b1))},
   431  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   432  			}),
   433  		},
   434  	})
   435  
   436  	e, c := testServer(m)
   437  	defer c()
   438  
   439  	ctx := context.Background()
   440  	r, err := NewRepository(repo, e, nil)
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	l := r.Blobs(ctx)
   445  
   446  	upload, err := l.Create(ctx)
   447  	if err != nil {
   448  		t.Fatal(err)
   449  	}
   450  
   451  	if upload.ID() != uploadID {
   452  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
   453  	}
   454  
   455  	n, err := upload.ReadFrom(bytes.NewReader(b1))
   456  	if err != nil {
   457  		t.Fatal(err)
   458  	}
   459  	if n != int64(len(b1)) {
   460  		t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
   461  	}
   462  
   463  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   464  		Digest: dgst,
   465  		Size:   int64(len(b1)),
   466  	})
   467  	if err != nil {
   468  		t.Fatal(err)
   469  	}
   470  
   471  	if blob.Size != int64(len(b1)) {
   472  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   473  	}
   474  }
   475  
   476  func TestBlobMount(t *testing.T) {
   477  	dgst, content := newRandomBlob(1024)
   478  	var m testutil.RequestResponseMap
   479  	repo, _ := reference.WithName("test.example.com/uploadrepo")
   480  
   481  	sourceRepo, _ := reference.WithName("test.example.com/sourcerepo")
   482  	canonicalRef, _ := reference.WithDigest(sourceRepo, dgst)
   483  
   484  	m = append(m, testutil.RequestResponseMapping{
   485  		Request: testutil.Request{
   486  			Method:      "POST",
   487  			Route:       "/v2/" + repo.Name() + "/blobs/uploads/",
   488  			QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}},
   489  		},
   490  		Response: testutil.Response{
   491  			StatusCode: http.StatusCreated,
   492  			Headers: http.Header(map[string][]string{
   493  				"Content-Length":        {"0"},
   494  				"Location":              {"/v2/" + repo.Name() + "/blobs/" + dgst.String()},
   495  				"Docker-Content-Digest": {dgst.String()},
   496  			}),
   497  		},
   498  	})
   499  	m = append(m, testutil.RequestResponseMapping{
   500  		Request: testutil.Request{
   501  			Method: "HEAD",
   502  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   503  		},
   504  		Response: testutil.Response{
   505  			StatusCode: http.StatusOK,
   506  			Headers: http.Header(map[string][]string{
   507  				"Content-Length": {fmt.Sprint(len(content))},
   508  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   509  			}),
   510  		},
   511  	})
   512  
   513  	e, c := testServer(m)
   514  	defer c()
   515  
   516  	ctx := context.Background()
   517  	r, err := NewRepository(repo, e, nil)
   518  	if err != nil {
   519  		t.Fatal(err)
   520  	}
   521  
   522  	l := r.Blobs(ctx)
   523  
   524  	bw, err := l.Create(ctx, WithMountFrom(canonicalRef))
   525  	if bw != nil {
   526  		t.Fatalf("Expected blob writer to be nil, was %v", bw)
   527  	}
   528  
   529  	if ebm, ok := err.(distribution.ErrBlobMounted); ok {
   530  		if ebm.From.Digest() != dgst {
   531  			t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst)
   532  		}
   533  		if ebm.From.Name() != sourceRepo.Name() {
   534  			t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo)
   535  		}
   536  	} else {
   537  		t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err)
   538  	}
   539  }
   540  
   541  func newRandomSchemaV1Manifest(name reference.Named, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
   542  	blobs := make([]schema1.FSLayer, blobCount)
   543  	history := make([]schema1.History, blobCount)
   544  
   545  	for i := 0; i < blobCount; i++ {
   546  		dgst, blob := newRandomBlob((i % 5) * 16)
   547  
   548  		blobs[i] = schema1.FSLayer{BlobSum: dgst}
   549  		history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)}
   550  	}
   551  
   552  	m := schema1.Manifest{
   553  		Name:         name.String(),
   554  		Tag:          tag,
   555  		Architecture: "x86",
   556  		FSLayers:     blobs,
   557  		History:      history,
   558  		Versioned: manifest.Versioned{
   559  			SchemaVersion: 1,
   560  		},
   561  	}
   562  
   563  	pk, err := libtrust.GenerateECP256PrivateKey()
   564  	if err != nil {
   565  		panic(err)
   566  	}
   567  
   568  	sm, err := schema1.Sign(&m, pk)
   569  	if err != nil {
   570  		panic(err)
   571  	}
   572  
   573  	return sm, digest.FromBytes(sm.Canonical), sm.Canonical
   574  }
   575  
   576  func addTestManifestWithEtag(repo reference.Named, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) {
   577  	actualDigest := digest.FromBytes(content)
   578  	getReqWithEtag := testutil.Request{
   579  		Method: "GET",
   580  		Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   581  		Headers: http.Header(map[string][]string{
   582  			"If-None-Match": {fmt.Sprintf(`"%s"`, dgst)},
   583  		}),
   584  	}
   585  
   586  	var getRespWithEtag testutil.Response
   587  	if actualDigest.String() == dgst {
   588  		getRespWithEtag = testutil.Response{
   589  			StatusCode: http.StatusNotModified,
   590  			Body:       []byte{},
   591  			Headers: http.Header(map[string][]string{
   592  				"Content-Length": {"0"},
   593  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   594  				"Content-Type":   {schema1.MediaTypeSignedManifest},
   595  			}),
   596  		}
   597  	} else {
   598  		getRespWithEtag = testutil.Response{
   599  			StatusCode: http.StatusOK,
   600  			Body:       content,
   601  			Headers: http.Header(map[string][]string{
   602  				"Content-Length": {fmt.Sprint(len(content))},
   603  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   604  				"Content-Type":   {schema1.MediaTypeSignedManifest},
   605  			}),
   606  		}
   607  
   608  	}
   609  	*m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag})
   610  }
   611  
   612  func contentDigestString(mediatype string, content []byte) string {
   613  	if mediatype == schema1.MediaTypeSignedManifest {
   614  		m, _, _ := distribution.UnmarshalManifest(mediatype, content)
   615  		content = m.(*schema1.SignedManifest).Canonical
   616  	}
   617  	return digest.Canonical.FromBytes(content).String()
   618  }
   619  
   620  func addTestManifest(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) {
   621  	*m = append(*m, testutil.RequestResponseMapping{
   622  		Request: testutil.Request{
   623  			Method: "GET",
   624  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   625  		},
   626  		Response: testutil.Response{
   627  			StatusCode: http.StatusOK,
   628  			Body:       content,
   629  			Headers: http.Header(map[string][]string{
   630  				"Content-Length":        {fmt.Sprint(len(content))},
   631  				"Last-Modified":         {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   632  				"Content-Type":          {mediatype},
   633  				"Docker-Content-Digest": {contentDigestString(mediatype, content)},
   634  			}),
   635  		},
   636  	})
   637  	*m = append(*m, testutil.RequestResponseMapping{
   638  		Request: testutil.Request{
   639  			Method: "HEAD",
   640  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   641  		},
   642  		Response: testutil.Response{
   643  			StatusCode: http.StatusOK,
   644  			Headers: http.Header(map[string][]string{
   645  				"Content-Length":        {fmt.Sprint(len(content))},
   646  				"Last-Modified":         {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   647  				"Content-Type":          {mediatype},
   648  				"Docker-Content-Digest": {digest.Canonical.FromBytes(content).String()},
   649  			}),
   650  		},
   651  	})
   652  }
   653  
   654  func addTestManifestWithoutDigestHeader(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) {
   655  	*m = append(*m, testutil.RequestResponseMapping{
   656  		Request: testutil.Request{
   657  			Method: "GET",
   658  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   659  		},
   660  		Response: testutil.Response{
   661  			StatusCode: http.StatusOK,
   662  			Body:       content,
   663  			Headers: http.Header(map[string][]string{
   664  				"Content-Length": {fmt.Sprint(len(content))},
   665  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   666  				"Content-Type":   {mediatype},
   667  			}),
   668  		},
   669  	})
   670  	*m = append(*m, testutil.RequestResponseMapping{
   671  		Request: testutil.Request{
   672  			Method: "HEAD",
   673  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   674  		},
   675  		Response: testutil.Response{
   676  			StatusCode: http.StatusOK,
   677  			Headers: http.Header(map[string][]string{
   678  				"Content-Length": {fmt.Sprint(len(content))},
   679  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   680  				"Content-Type":   {mediatype},
   681  			}),
   682  		},
   683  	})
   684  }
   685  
   686  func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
   687  	if m1.Name != m2.Name {
   688  		return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name)
   689  	}
   690  	if m1.Tag != m2.Tag {
   691  		return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag)
   692  	}
   693  	if len(m1.FSLayers) != len(m2.FSLayers) {
   694  		return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers))
   695  	}
   696  	for i := range m1.FSLayers {
   697  		if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum {
   698  			return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum)
   699  		}
   700  	}
   701  	if len(m1.History) != len(m2.History) {
   702  		return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History))
   703  	}
   704  	for i := range m1.History {
   705  		if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility {
   706  			return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility)
   707  		}
   708  	}
   709  	return nil
   710  }
   711  
   712  func TestV1ManifestFetch(t *testing.T) {
   713  	ctx := context.Background()
   714  	repo, _ := reference.WithName("test.example.com/repo")
   715  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   716  	var m testutil.RequestResponseMap
   717  	_, pl, err := m1.Payload()
   718  	if err != nil {
   719  		t.Fatal(err)
   720  	}
   721  	addTestManifest(repo, dgst.String(), schema1.MediaTypeSignedManifest, pl, &m)
   722  	addTestManifest(repo, "latest", schema1.MediaTypeSignedManifest, pl, &m)
   723  	addTestManifest(repo, "badcontenttype", "text/html", pl, &m)
   724  
   725  	e, c := testServer(m)
   726  	defer c()
   727  
   728  	r, err := NewRepository(repo, e, nil)
   729  	if err != nil {
   730  		t.Fatal(err)
   731  	}
   732  	ms, err := r.Manifests(ctx)
   733  	if err != nil {
   734  		t.Fatal(err)
   735  	}
   736  
   737  	ok, err := ms.Exists(ctx, dgst)
   738  	if err != nil {
   739  		t.Fatal(err)
   740  	}
   741  	if !ok {
   742  		t.Fatal("Manifest does not exist")
   743  	}
   744  
   745  	manifest, err := ms.Get(ctx, dgst)
   746  	if err != nil {
   747  		t.Fatal(err)
   748  	}
   749  	v1manifest, ok := manifest.(*schema1.SignedManifest)
   750  	if !ok {
   751  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   752  	}
   753  
   754  	if err := checkEqualManifest(v1manifest, m1); err != nil {
   755  		t.Fatal(err)
   756  	}
   757  
   758  	var contentDigest digest.Digest
   759  	manifest, err = ms.Get(ctx, dgst, distribution.WithTag("latest"), ReturnContentDigest(&contentDigest))
   760  	if err != nil {
   761  		t.Fatal(err)
   762  	}
   763  	v1manifest, ok = manifest.(*schema1.SignedManifest)
   764  	if !ok {
   765  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   766  	}
   767  
   768  	if err = checkEqualManifest(v1manifest, m1); err != nil {
   769  		t.Fatal(err)
   770  	}
   771  
   772  	if contentDigest != dgst {
   773  		t.Fatalf("Unexpected returned content digest %v, expected %v", contentDigest, dgst)
   774  	}
   775  
   776  	manifest, err = ms.Get(ctx, dgst, distribution.WithTag("badcontenttype"))
   777  	if err != nil {
   778  		t.Fatal(err)
   779  	}
   780  	v1manifest, ok = manifest.(*schema1.SignedManifest)
   781  	if !ok {
   782  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   783  	}
   784  
   785  	if err = checkEqualManifest(v1manifest, m1); err != nil {
   786  		t.Fatal(err)
   787  	}
   788  }
   789  
   790  func TestManifestFetchWithEtag(t *testing.T) {
   791  	repo, _ := reference.WithName("test.example.com/repo/by/tag")
   792  	_, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
   793  	var m testutil.RequestResponseMap
   794  	addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
   795  
   796  	e, c := testServer(m)
   797  	defer c()
   798  
   799  	ctx := context.Background()
   800  	r, err := NewRepository(repo, e, nil)
   801  	if err != nil {
   802  		t.Fatal(err)
   803  	}
   804  
   805  	ms, err := r.Manifests(ctx)
   806  	if err != nil {
   807  		t.Fatal(err)
   808  	}
   809  
   810  	clientManifestService, ok := ms.(*manifests)
   811  	if !ok {
   812  		panic("wrong type for client manifest service")
   813  	}
   814  	_, err = clientManifestService.Get(ctx, d1, distribution.WithTag("latest"), AddEtagToTag("latest", d1.String()))
   815  	if err != distribution.ErrManifestNotModified {
   816  		t.Fatal(err)
   817  	}
   818  }
   819  
   820  func TestManifestFetchWithAccept(t *testing.T) {
   821  	ctx := context.Background()
   822  	repo, _ := reference.WithName("test.example.com/repo")
   823  	_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   824  	headers := make(chan []string, 1)
   825  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   826  		headers <- req.Header["Accept"]
   827  	}))
   828  	defer close(headers)
   829  	defer s.Close()
   830  
   831  	r, err := NewRepository(repo, s.URL, nil)
   832  	if err != nil {
   833  		t.Fatal(err)
   834  	}
   835  	ms, err := r.Manifests(ctx)
   836  	if err != nil {
   837  		t.Fatal(err)
   838  	}
   839  
   840  	testCases := []struct {
   841  		// the media types we send
   842  		mediaTypes []string
   843  		// the expected Accept headers the server should receive
   844  		expect []string
   845  		// whether to sort the request and response values for comparison
   846  		sort bool
   847  	}{
   848  		{
   849  			mediaTypes: []string{},
   850  			expect:     distribution.ManifestMediaTypes(),
   851  			sort:       true,
   852  		},
   853  		{
   854  			mediaTypes: []string{"test1", "test2"},
   855  			expect:     []string{"test1", "test2"},
   856  		},
   857  		{
   858  			mediaTypes: []string{"test1"},
   859  			expect:     []string{"test1"},
   860  		},
   861  		{
   862  			mediaTypes: []string{""},
   863  			expect:     []string{""},
   864  		},
   865  	}
   866  	for _, testCase := range testCases {
   867  		ms.Get(ctx, dgst, distribution.WithManifestMediaTypes(testCase.mediaTypes))
   868  		actual := <-headers
   869  		if testCase.sort {
   870  			sort.Strings(actual)
   871  			sort.Strings(testCase.expect)
   872  		}
   873  		if !reflect.DeepEqual(actual, testCase.expect) {
   874  			t.Fatalf("unexpected Accept header values: %v", actual)
   875  		}
   876  	}
   877  }
   878  
   879  func TestManifestDelete(t *testing.T) {
   880  	repo, _ := reference.WithName("test.example.com/repo/delete")
   881  	_, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   882  	_, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   883  	var m testutil.RequestResponseMap
   884  	m = append(m, testutil.RequestResponseMapping{
   885  		Request: testutil.Request{
   886  			Method: "DELETE",
   887  			Route:  "/v2/" + repo.Name() + "/manifests/" + dgst1.String(),
   888  		},
   889  		Response: testutil.Response{
   890  			StatusCode: http.StatusAccepted,
   891  			Headers: http.Header(map[string][]string{
   892  				"Content-Length": {"0"},
   893  			}),
   894  		},
   895  	})
   896  
   897  	e, c := testServer(m)
   898  	defer c()
   899  
   900  	r, err := NewRepository(repo, e, nil)
   901  	if err != nil {
   902  		t.Fatal(err)
   903  	}
   904  	ctx := context.Background()
   905  	ms, err := r.Manifests(ctx)
   906  	if err != nil {
   907  		t.Fatal(err)
   908  	}
   909  
   910  	if err := ms.Delete(ctx, dgst1); err != nil {
   911  		t.Fatal(err)
   912  	}
   913  	if err := ms.Delete(ctx, dgst2); err == nil {
   914  		t.Fatal("Expected error deleting unknown manifest")
   915  	}
   916  	// TODO(dmcgowan): Check for specific unknown error
   917  }
   918  
   919  func TestManifestPut(t *testing.T) {
   920  	repo, _ := reference.WithName("test.example.com/repo/delete")
   921  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
   922  
   923  	_, payload, err := m1.Payload()
   924  	if err != nil {
   925  		t.Fatal(err)
   926  	}
   927  
   928  	var m testutil.RequestResponseMap
   929  	m = append(m, testutil.RequestResponseMapping{
   930  		Request: testutil.Request{
   931  			Method: "PUT",
   932  			Route:  "/v2/" + repo.Name() + "/manifests/other",
   933  			Body:   payload,
   934  		},
   935  		Response: testutil.Response{
   936  			StatusCode: http.StatusAccepted,
   937  			Headers: http.Header(map[string][]string{
   938  				"Content-Length":        {"0"},
   939  				"Docker-Content-Digest": {dgst.String()},
   940  			}),
   941  		},
   942  	})
   943  
   944  	putDgst := digest.FromBytes(m1.Canonical)
   945  	m = append(m, testutil.RequestResponseMapping{
   946  		Request: testutil.Request{
   947  			Method: "PUT",
   948  			Route:  "/v2/" + repo.Name() + "/manifests/" + putDgst.String(),
   949  			Body:   payload,
   950  		},
   951  		Response: testutil.Response{
   952  			StatusCode: http.StatusAccepted,
   953  			Headers: http.Header(map[string][]string{
   954  				"Content-Length":        {"0"},
   955  				"Docker-Content-Digest": {putDgst.String()},
   956  			}),
   957  		},
   958  	})
   959  
   960  	e, c := testServer(m)
   961  	defer c()
   962  
   963  	r, err := NewRepository(repo, e, nil)
   964  	if err != nil {
   965  		t.Fatal(err)
   966  	}
   967  	ctx := context.Background()
   968  	ms, err := r.Manifests(ctx)
   969  	if err != nil {
   970  		t.Fatal(err)
   971  	}
   972  
   973  	if _, err := ms.Put(ctx, m1, distribution.WithTag(m1.Tag)); err != nil {
   974  		t.Fatal(err)
   975  	}
   976  
   977  	if _, err := ms.Put(ctx, m1); err != nil {
   978  		t.Fatal(err)
   979  	}
   980  
   981  	// TODO(dmcgowan): Check for invalid input error
   982  }
   983  
   984  func TestManifestTags(t *testing.T) {
   985  	repo, _ := reference.WithName("test.example.com/repo/tags/list")
   986  	tagsList := []byte(strings.TrimSpace(`
   987  {
   988  	"name": "test.example.com/repo/tags/list",
   989  	"tags": [
   990  		"tag1",
   991  		"tag2",
   992  		"funtag"
   993  	]
   994  }
   995  	`))
   996  	var m testutil.RequestResponseMap
   997  	for i := 0; i < 3; i++ {
   998  		m = append(m, testutil.RequestResponseMapping{
   999  			Request: testutil.Request{
  1000  				Method: "GET",
  1001  				Route:  "/v2/" + repo.Name() + "/tags/list",
  1002  			},
  1003  			Response: testutil.Response{
  1004  				StatusCode: http.StatusOK,
  1005  				Body:       tagsList,
  1006  				Headers: http.Header(map[string][]string{
  1007  					"Content-Length": {fmt.Sprint(len(tagsList))},
  1008  					"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
  1009  				}),
  1010  			},
  1011  		})
  1012  	}
  1013  	e, c := testServer(m)
  1014  	defer c()
  1015  
  1016  	r, err := NewRepository(repo, e, nil)
  1017  	if err != nil {
  1018  		t.Fatal(err)
  1019  	}
  1020  
  1021  	ctx := context.Background()
  1022  	tagService := r.Tags(ctx)
  1023  
  1024  	tags, err := tagService.All(ctx)
  1025  	if err != nil {
  1026  		t.Fatal(err)
  1027  	}
  1028  	if len(tags) != 3 {
  1029  		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
  1030  	}
  1031  
  1032  	expected := map[string]struct{}{
  1033  		"tag1":   {},
  1034  		"tag2":   {},
  1035  		"funtag": {},
  1036  	}
  1037  	for _, t := range tags {
  1038  		delete(expected, t)
  1039  	}
  1040  	if len(expected) != 0 {
  1041  		t.Fatalf("unexpected tags returned: %v", expected)
  1042  	}
  1043  	// TODO(dmcgowan): Check for error cases
  1044  }
  1045  
  1046  func TestObtainsErrorForMissingTag(t *testing.T) {
  1047  	repo, _ := reference.WithName("test.example.com/repo")
  1048  
  1049  	var m testutil.RequestResponseMap
  1050  	var errors errcode.Errors
  1051  	errors = append(errors, v2.ErrorCodeManifestUnknown.WithDetail("unknown manifest"))
  1052  	errBytes, err := json.Marshal(errors)
  1053  	if err != nil {
  1054  		t.Fatal(err)
  1055  	}
  1056  	m = append(m, testutil.RequestResponseMapping{
  1057  		Request: testutil.Request{
  1058  			Method: "GET",
  1059  			Route:  "/v2/" + repo.Name() + "/manifests/1.0.0",
  1060  		},
  1061  		Response: testutil.Response{
  1062  			StatusCode: http.StatusNotFound,
  1063  			Body:       errBytes,
  1064  			Headers: http.Header(map[string][]string{
  1065  				"Content-Type": {"application/json; charset=utf-8"},
  1066  			}),
  1067  		},
  1068  	})
  1069  	e, c := testServer(m)
  1070  	defer c()
  1071  
  1072  	ctx := context.Background()
  1073  	r, err := NewRepository(repo, e, nil)
  1074  	if err != nil {
  1075  		t.Fatal(err)
  1076  	}
  1077  
  1078  	tagService := r.Tags(ctx)
  1079  
  1080  	_, err = tagService.Get(ctx, "1.0.0")
  1081  	if err == nil {
  1082  		t.Fatalf("Expected an error")
  1083  	}
  1084  	if !strings.Contains(err.Error(), "manifest unknown") {
  1085  		t.Fatalf("Expected unknown manifest error message")
  1086  	}
  1087  }
  1088  
  1089  func TestObtainsManifestForTagWithoutHeaders(t *testing.T) {
  1090  	repo, _ := reference.WithName("test.example.com/repo")
  1091  
  1092  	var m testutil.RequestResponseMap
  1093  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
  1094  	_, pl, err := m1.Payload()
  1095  	if err != nil {
  1096  		t.Fatal(err)
  1097  	}
  1098  	addTestManifestWithoutDigestHeader(repo, "1.0.0", schema1.MediaTypeSignedManifest, pl, &m)
  1099  
  1100  	e, c := testServer(m)
  1101  	defer c()
  1102  
  1103  	ctx := context.Background()
  1104  	r, err := NewRepository(repo, e, nil)
  1105  	if err != nil {
  1106  		t.Fatal(err)
  1107  	}
  1108  
  1109  	tagService := r.Tags(ctx)
  1110  
  1111  	desc, err := tagService.Get(ctx, "1.0.0")
  1112  	if err != nil {
  1113  		t.Fatalf("Expected no error")
  1114  	}
  1115  	if desc.Digest != dgst {
  1116  		t.Fatalf("Unexpected digest")
  1117  	}
  1118  }
  1119  func TestManifestTagsPaginated(t *testing.T) {
  1120  	s := httptest.NewServer(http.NotFoundHandler())
  1121  	defer s.Close()
  1122  
  1123  	repo, _ := reference.WithName("test.example.com/repo/tags/list")
  1124  	tagsList := []string{"tag1", "tag2", "funtag"}
  1125  	var m testutil.RequestResponseMap
  1126  	for i := 0; i < 3; i++ {
  1127  		body, err := json.Marshal(map[string]interface{}{
  1128  			"name": "test.example.com/repo/tags/list",
  1129  			"tags": []string{tagsList[i]},
  1130  		})
  1131  		if err != nil {
  1132  			t.Fatal(err)
  1133  		}
  1134  		queryParams := make(map[string][]string)
  1135  		if i > 0 {
  1136  			queryParams["n"] = []string{"1"}
  1137  			queryParams["last"] = []string{tagsList[i-1]}
  1138  		}
  1139  
  1140  		// Test both relative and absolute links.
  1141  		relativeLink := "/v2/" + repo.Name() + "/tags/list?n=1&last=" + tagsList[i]
  1142  		var link string
  1143  		switch i {
  1144  		case 0:
  1145  			link = relativeLink
  1146  		case len(tagsList) - 1:
  1147  			link = ""
  1148  		default:
  1149  			link = s.URL + relativeLink
  1150  		}
  1151  
  1152  		headers := http.Header(map[string][]string{
  1153  			"Content-Length": {fmt.Sprint(len(body))},
  1154  			"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
  1155  		})
  1156  		if link != "" {
  1157  			headers.Set("Link", fmt.Sprintf(`<%s>; rel="next"`, link))
  1158  		}
  1159  
  1160  		m = append(m, testutil.RequestResponseMapping{
  1161  			Request: testutil.Request{
  1162  				Method:      "GET",
  1163  				Route:       "/v2/" + repo.Name() + "/tags/list",
  1164  				QueryParams: queryParams,
  1165  			},
  1166  			Response: testutil.Response{
  1167  				StatusCode: http.StatusOK,
  1168  				Body:       body,
  1169  				Headers:    headers,
  1170  			},
  1171  		})
  1172  	}
  1173  
  1174  	s.Config.Handler = testutil.NewHandler(m)
  1175  
  1176  	r, err := NewRepository(repo, s.URL, nil)
  1177  	if err != nil {
  1178  		t.Fatal(err)
  1179  	}
  1180  
  1181  	ctx := context.Background()
  1182  	tagService := r.Tags(ctx)
  1183  
  1184  	tags, err := tagService.All(ctx)
  1185  	if err != nil {
  1186  		t.Fatal(tags, err)
  1187  	}
  1188  	if len(tags) != 3 {
  1189  		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
  1190  	}
  1191  
  1192  	expected := map[string]struct{}{
  1193  		"tag1":   {},
  1194  		"tag2":   {},
  1195  		"funtag": {},
  1196  	}
  1197  	for _, t := range tags {
  1198  		delete(expected, t)
  1199  	}
  1200  	if len(expected) != 0 {
  1201  		t.Fatalf("unexpected tags returned: %v", expected)
  1202  	}
  1203  }
  1204  
  1205  func TestManifestUnauthorized(t *testing.T) {
  1206  	repo, _ := reference.WithName("test.example.com/repo")
  1207  	_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
  1208  	var m testutil.RequestResponseMap
  1209  
  1210  	m = append(m, testutil.RequestResponseMapping{
  1211  		Request: testutil.Request{
  1212  			Method: "GET",
  1213  			Route:  "/v2/" + repo.Name() + "/manifests/" + dgst.String(),
  1214  		},
  1215  		Response: testutil.Response{
  1216  			StatusCode: http.StatusUnauthorized,
  1217  			Headers:    http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
  1218  			Body:       []byte("<html>garbage</html>"),
  1219  		},
  1220  	})
  1221  
  1222  	e, c := testServer(m)
  1223  	defer c()
  1224  
  1225  	r, err := NewRepository(repo, e, nil)
  1226  	if err != nil {
  1227  		t.Fatal(err)
  1228  	}
  1229  	ctx := context.Background()
  1230  	ms, err := r.Manifests(ctx)
  1231  	if err != nil {
  1232  		t.Fatal(err)
  1233  	}
  1234  
  1235  	_, err = ms.Get(ctx, dgst)
  1236  	if err == nil {
  1237  		t.Fatal("Expected error fetching manifest")
  1238  	}
  1239  	v2Err, ok := err.(errcode.Error)
  1240  	if !ok {
  1241  		t.Fatalf("Unexpected error type: %#v", err)
  1242  	}
  1243  	if v2Err.Code != errcode.ErrorCodeUnauthorized {
  1244  		t.Fatalf("Unexpected error code: %s", v2Err.Code.String())
  1245  	}
  1246  	if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected {
  1247  		t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
  1248  	}
  1249  }
  1250  
  1251  func TestCatalog(t *testing.T) {
  1252  	var m testutil.RequestResponseMap
  1253  	addTestCatalog(
  1254  		"/v2/_catalog?n=5",
  1255  		[]byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
  1256  
  1257  	e, c := testServer(m)
  1258  	defer c()
  1259  
  1260  	entries := make([]string, 5)
  1261  
  1262  	r, err := NewRegistry(e, nil)
  1263  	if err != nil {
  1264  		t.Fatal(err)
  1265  	}
  1266  
  1267  	ctx := context.Background()
  1268  	numFilled, err := r.Repositories(ctx, entries, "")
  1269  	if err != io.EOF {
  1270  		t.Fatal(err)
  1271  	}
  1272  
  1273  	if numFilled != 3 {
  1274  		t.Fatalf("Got wrong number of repos")
  1275  	}
  1276  }
  1277  
  1278  func TestCatalogInParts(t *testing.T) {
  1279  	var m testutil.RequestResponseMap
  1280  	addTestCatalog(
  1281  		"/v2/_catalog?n=2",
  1282  		[]byte("{\"repositories\":[\"bar\", \"baz\"]}"),
  1283  		"</v2/_catalog?last=baz&n=2>", &m)
  1284  	addTestCatalog(
  1285  		"/v2/_catalog?last=baz&n=2",
  1286  		[]byte("{\"repositories\":[\"foo\"]}"),
  1287  		"", &m)
  1288  
  1289  	e, c := testServer(m)
  1290  	defer c()
  1291  
  1292  	entries := make([]string, 2)
  1293  
  1294  	r, err := NewRegistry(e, nil)
  1295  	if err != nil {
  1296  		t.Fatal(err)
  1297  	}
  1298  
  1299  	ctx := context.Background()
  1300  	numFilled, err := r.Repositories(ctx, entries, "")
  1301  	if err != nil {
  1302  		t.Fatal(err)
  1303  	}
  1304  
  1305  	if numFilled != 2 {
  1306  		t.Fatalf("Got wrong number of repos")
  1307  	}
  1308  
  1309  	numFilled, err = r.Repositories(ctx, entries, "baz")
  1310  	if err != io.EOF {
  1311  		t.Fatal(err)
  1312  	}
  1313  
  1314  	if numFilled != 1 {
  1315  		t.Fatalf("Got wrong number of repos")
  1316  	}
  1317  }
  1318  
  1319  func TestSanitizeLocation(t *testing.T) {
  1320  	for _, testcase := range []struct {
  1321  		description string
  1322  		location    string
  1323  		source      string
  1324  		expected    string
  1325  		err         error
  1326  	}{
  1327  		{
  1328  			description: "ensure relative location correctly resolved",
  1329  			location:    "/v2/foo/baasdf",
  1330  			source:      "http://blahalaja.com/v1",
  1331  			expected:    "http://blahalaja.com/v2/foo/baasdf",
  1332  		},
  1333  		{
  1334  			description: "ensure parameters are preserved",
  1335  			location:    "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
  1336  			source:      "http://blahalaja.com/v1",
  1337  			expected:    "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
  1338  		},
  1339  		{
  1340  			description: "ensure new hostname overridden",
  1341  			location:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
  1342  			source:      "http://blahalaja.com/v1",
  1343  			expected:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
  1344  		},
  1345  	} {
  1346  		fatalf := func(format string, args ...interface{}) {
  1347  			t.Fatalf(testcase.description+": "+format, args...)
  1348  		}
  1349  
  1350  		s, err := sanitizeLocation(testcase.location, testcase.source)
  1351  		if err != testcase.err {
  1352  			if testcase.err != nil {
  1353  				fatalf("expected error: %v != %v", err, testcase)
  1354  			} else {
  1355  				fatalf("unexpected error sanitizing: %v", err)
  1356  			}
  1357  		}
  1358  
  1359  		if s != testcase.expected {
  1360  			fatalf("bad sanitize: %q != %q", s, testcase.expected)
  1361  		}
  1362  	}
  1363  }
  1364  

View as plain text