github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/repository_test.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/docker/distribution"
    17  	"github.com/docker/distribution/context"
    18  	"github.com/docker/distribution/digest"
    19  	"github.com/docker/distribution/manifest"
    20  	"github.com/docker/distribution/manifest/schema1"
    21  	"github.com/docker/distribution/registry/api/errcode"
    22  	"github.com/docker/distribution/testutil"
    23  	"github.com/docker/distribution/uuid"
    24  	"github.com/docker/libtrust"
    25  )
    26  
    27  func testServer(rrm testutil.RequestResponseMap) (string, func()) {
    28  	h := testutil.NewHandler(rrm)
    29  	s := httptest.NewServer(h)
    30  	return s.URL, s.Close
    31  }
    32  
    33  func newRandomBlob(size int) (digest.Digest, []byte) {
    34  	b := make([]byte, size)
    35  	if n, err := rand.Read(b); err != nil {
    36  		panic(err)
    37  	} else if n != size {
    38  		panic("unable to read enough bytes")
    39  	}
    40  
    41  	return digest.FromBytes(b), b
    42  }
    43  
    44  func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
    45  	*m = append(*m, testutil.RequestResponseMapping{
    46  		Request: testutil.Request{
    47  			Method: "GET",
    48  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    49  		},
    50  		Response: testutil.Response{
    51  			StatusCode: http.StatusOK,
    52  			Body:       content,
    53  			Headers: http.Header(map[string][]string{
    54  				"Content-Length": {fmt.Sprint(len(content))},
    55  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    56  			}),
    57  		},
    58  	})
    59  
    60  	*m = append(*m, testutil.RequestResponseMapping{
    61  		Request: testutil.Request{
    62  			Method: "HEAD",
    63  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    64  		},
    65  		Response: testutil.Response{
    66  			StatusCode: http.StatusOK,
    67  			Headers: http.Header(map[string][]string{
    68  				"Content-Length": {fmt.Sprint(len(content))},
    69  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    70  			}),
    71  		},
    72  	})
    73  }
    74  
    75  func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
    76  	headers := map[string][]string{
    77  		"Content-Length": {strconv.Itoa(len(content))},
    78  		"Content-Type":   {"application/json; charset=utf-8"},
    79  	}
    80  	if link != "" {
    81  		headers["Link"] = append(headers["Link"], link)
    82  	}
    83  
    84  	*m = append(*m, testutil.RequestResponseMapping{
    85  		Request: testutil.Request{
    86  			Method: "GET",
    87  			Route:  route,
    88  		},
    89  		Response: testutil.Response{
    90  			StatusCode: http.StatusOK,
    91  			Body:       content,
    92  			Headers:    http.Header(headers),
    93  		},
    94  	})
    95  }
    96  
    97  func TestBlobDelete(t *testing.T) {
    98  	dgst, _ := newRandomBlob(1024)
    99  	var m testutil.RequestResponseMap
   100  	repo := "test.example.com/repo1"
   101  	m = append(m, testutil.RequestResponseMapping{
   102  		Request: testutil.Request{
   103  			Method: "DELETE",
   104  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
   105  		},
   106  		Response: testutil.Response{
   107  			StatusCode: http.StatusAccepted,
   108  			Headers: http.Header(map[string][]string{
   109  				"Content-Length": {"0"},
   110  			}),
   111  		},
   112  	})
   113  
   114  	e, c := testServer(m)
   115  	defer c()
   116  
   117  	ctx := context.Background()
   118  	r, err := NewRepository(ctx, repo, e, nil)
   119  	if err != nil {
   120  		t.Fatal(err)
   121  	}
   122  	l := r.Blobs(ctx)
   123  	err = l.Delete(ctx, dgst)
   124  	if err != nil {
   125  		t.Errorf("Error deleting blob: %s", err.Error())
   126  	}
   127  
   128  }
   129  
   130  func TestBlobFetch(t *testing.T) {
   131  	d1, b1 := newRandomBlob(1024)
   132  	var m testutil.RequestResponseMap
   133  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   134  
   135  	e, c := testServer(m)
   136  	defer c()
   137  
   138  	ctx := context.Background()
   139  	r, err := NewRepository(ctx, "test.example.com/repo1", e, nil)
   140  	if err != nil {
   141  		t.Fatal(err)
   142  	}
   143  	l := r.Blobs(ctx)
   144  
   145  	b, err := l.Get(ctx, d1)
   146  	if err != nil {
   147  		t.Fatal(err)
   148  	}
   149  	if bytes.Compare(b, b1) != 0 {
   150  		t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
   151  	}
   152  
   153  	// TODO(dmcgowan): Test for unknown blob case
   154  }
   155  
   156  func TestBlobExistsNoContentLength(t *testing.T) {
   157  	var m testutil.RequestResponseMap
   158  
   159  	repo := "biff"
   160  	dgst, content := newRandomBlob(1024)
   161  	m = append(m, testutil.RequestResponseMapping{
   162  		Request: testutil.Request{
   163  			Method: "GET",
   164  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
   165  		},
   166  		Response: testutil.Response{
   167  			StatusCode: http.StatusOK,
   168  			Body:       content,
   169  			Headers: http.Header(map[string][]string{
   170  				//			"Content-Length": {fmt.Sprint(len(content))},
   171  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   172  			}),
   173  		},
   174  	})
   175  
   176  	m = append(m, testutil.RequestResponseMapping{
   177  		Request: testutil.Request{
   178  			Method: "HEAD",
   179  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
   180  		},
   181  		Response: testutil.Response{
   182  			StatusCode: http.StatusOK,
   183  			Headers: http.Header(map[string][]string{
   184  				//			"Content-Length": {fmt.Sprint(len(content))},
   185  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   186  			}),
   187  		},
   188  	})
   189  	e, c := testServer(m)
   190  	defer c()
   191  
   192  	ctx := context.Background()
   193  	r, err := NewRepository(ctx, repo, e, nil)
   194  	if err != nil {
   195  		t.Fatal(err)
   196  	}
   197  	l := r.Blobs(ctx)
   198  
   199  	_, err = l.Stat(ctx, dgst)
   200  	if err == nil {
   201  		t.Fatal(err)
   202  	}
   203  	if !strings.Contains(err.Error(), "missing content-length heade") {
   204  		t.Fatalf("Expected missing content-length error message")
   205  	}
   206  
   207  }
   208  
   209  func TestBlobExists(t *testing.T) {
   210  	d1, b1 := newRandomBlob(1024)
   211  	var m testutil.RequestResponseMap
   212  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   213  
   214  	e, c := testServer(m)
   215  	defer c()
   216  
   217  	ctx := context.Background()
   218  	r, err := NewRepository(ctx, "test.example.com/repo1", e, nil)
   219  	if err != nil {
   220  		t.Fatal(err)
   221  	}
   222  	l := r.Blobs(ctx)
   223  
   224  	stat, err := l.Stat(ctx, d1)
   225  	if err != nil {
   226  		t.Fatal(err)
   227  	}
   228  
   229  	if stat.Digest != d1 {
   230  		t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1)
   231  	}
   232  
   233  	if stat.Size != int64(len(b1)) {
   234  		t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1))
   235  	}
   236  
   237  	// TODO(dmcgowan): Test error cases and ErrBlobUnknown case
   238  }
   239  
   240  func TestBlobUploadChunked(t *testing.T) {
   241  	dgst, b1 := newRandomBlob(1024)
   242  	var m testutil.RequestResponseMap
   243  	chunks := [][]byte{
   244  		b1[0:256],
   245  		b1[256:512],
   246  		b1[512:513],
   247  		b1[513:1024],
   248  	}
   249  	repo := "test.example.com/uploadrepo"
   250  	uuids := []string{uuid.Generate().String()}
   251  	m = append(m, testutil.RequestResponseMapping{
   252  		Request: testutil.Request{
   253  			Method: "POST",
   254  			Route:  "/v2/" + repo + "/blobs/uploads/",
   255  		},
   256  		Response: testutil.Response{
   257  			StatusCode: http.StatusAccepted,
   258  			Headers: http.Header(map[string][]string{
   259  				"Content-Length":     {"0"},
   260  				"Location":           {"/v2/" + repo + "/blobs/uploads/" + uuids[0]},
   261  				"Docker-Upload-UUID": {uuids[0]},
   262  				"Range":              {"0-0"},
   263  			}),
   264  		},
   265  	})
   266  	offset := 0
   267  	for i, chunk := range chunks {
   268  		uuids = append(uuids, uuid.Generate().String())
   269  		newOffset := offset + len(chunk)
   270  		m = append(m, testutil.RequestResponseMapping{
   271  			Request: testutil.Request{
   272  				Method: "PATCH",
   273  				Route:  "/v2/" + repo + "/blobs/uploads/" + uuids[i],
   274  				Body:   chunk,
   275  			},
   276  			Response: testutil.Response{
   277  				StatusCode: http.StatusAccepted,
   278  				Headers: http.Header(map[string][]string{
   279  					"Content-Length":     {"0"},
   280  					"Location":           {"/v2/" + repo + "/blobs/uploads/" + uuids[i+1]},
   281  					"Docker-Upload-UUID": {uuids[i+1]},
   282  					"Range":              {fmt.Sprintf("%d-%d", offset, newOffset-1)},
   283  				}),
   284  			},
   285  		})
   286  		offset = newOffset
   287  	}
   288  	m = append(m, testutil.RequestResponseMapping{
   289  		Request: testutil.Request{
   290  			Method: "PUT",
   291  			Route:  "/v2/" + repo + "/blobs/uploads/" + uuids[len(uuids)-1],
   292  			QueryParams: map[string][]string{
   293  				"digest": {dgst.String()},
   294  			},
   295  		},
   296  		Response: testutil.Response{
   297  			StatusCode: http.StatusCreated,
   298  			Headers: http.Header(map[string][]string{
   299  				"Content-Length":        {"0"},
   300  				"Docker-Content-Digest": {dgst.String()},
   301  				"Content-Range":         {fmt.Sprintf("0-%d", offset-1)},
   302  			}),
   303  		},
   304  	})
   305  	m = append(m, testutil.RequestResponseMapping{
   306  		Request: testutil.Request{
   307  			Method: "HEAD",
   308  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
   309  		},
   310  		Response: testutil.Response{
   311  			StatusCode: http.StatusOK,
   312  			Headers: http.Header(map[string][]string{
   313  				"Content-Length": {fmt.Sprint(offset)},
   314  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   315  			}),
   316  		},
   317  	})
   318  
   319  	e, c := testServer(m)
   320  	defer c()
   321  
   322  	ctx := context.Background()
   323  	r, err := NewRepository(ctx, repo, e, nil)
   324  	if err != nil {
   325  		t.Fatal(err)
   326  	}
   327  	l := r.Blobs(ctx)
   328  
   329  	upload, err := l.Create(ctx)
   330  	if err != nil {
   331  		t.Fatal(err)
   332  	}
   333  
   334  	if upload.ID() != uuids[0] {
   335  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0])
   336  	}
   337  
   338  	for _, chunk := range chunks {
   339  		n, err := upload.Write(chunk)
   340  		if err != nil {
   341  			t.Fatal(err)
   342  		}
   343  		if n != len(chunk) {
   344  			t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk))
   345  		}
   346  	}
   347  
   348  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   349  		Digest: dgst,
   350  		Size:   int64(len(b1)),
   351  	})
   352  	if err != nil {
   353  		t.Fatal(err)
   354  	}
   355  
   356  	if blob.Size != int64(len(b1)) {
   357  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   358  	}
   359  }
   360  
   361  func TestBlobUploadMonolithic(t *testing.T) {
   362  	dgst, b1 := newRandomBlob(1024)
   363  	var m testutil.RequestResponseMap
   364  	repo := "test.example.com/uploadrepo"
   365  	uploadID := uuid.Generate().String()
   366  	m = append(m, testutil.RequestResponseMapping{
   367  		Request: testutil.Request{
   368  			Method: "POST",
   369  			Route:  "/v2/" + repo + "/blobs/uploads/",
   370  		},
   371  		Response: testutil.Response{
   372  			StatusCode: http.StatusAccepted,
   373  			Headers: http.Header(map[string][]string{
   374  				"Content-Length":     {"0"},
   375  				"Location":           {"/v2/" + repo + "/blobs/uploads/" + uploadID},
   376  				"Docker-Upload-UUID": {uploadID},
   377  				"Range":              {"0-0"},
   378  			}),
   379  		},
   380  	})
   381  	m = append(m, testutil.RequestResponseMapping{
   382  		Request: testutil.Request{
   383  			Method: "PATCH",
   384  			Route:  "/v2/" + repo + "/blobs/uploads/" + uploadID,
   385  			Body:   b1,
   386  		},
   387  		Response: testutil.Response{
   388  			StatusCode: http.StatusAccepted,
   389  			Headers: http.Header(map[string][]string{
   390  				"Location":              {"/v2/" + repo + "/blobs/uploads/" + uploadID},
   391  				"Docker-Upload-UUID":    {uploadID},
   392  				"Content-Length":        {"0"},
   393  				"Docker-Content-Digest": {dgst.String()},
   394  				"Range":                 {fmt.Sprintf("0-%d", len(b1)-1)},
   395  			}),
   396  		},
   397  	})
   398  	m = append(m, testutil.RequestResponseMapping{
   399  		Request: testutil.Request{
   400  			Method: "PUT",
   401  			Route:  "/v2/" + repo + "/blobs/uploads/" + uploadID,
   402  			QueryParams: map[string][]string{
   403  				"digest": {dgst.String()},
   404  			},
   405  		},
   406  		Response: testutil.Response{
   407  			StatusCode: http.StatusCreated,
   408  			Headers: http.Header(map[string][]string{
   409  				"Content-Length":        {"0"},
   410  				"Docker-Content-Digest": {dgst.String()},
   411  				"Content-Range":         {fmt.Sprintf("0-%d", len(b1)-1)},
   412  			}),
   413  		},
   414  	})
   415  	m = append(m, testutil.RequestResponseMapping{
   416  		Request: testutil.Request{
   417  			Method: "HEAD",
   418  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
   419  		},
   420  		Response: testutil.Response{
   421  			StatusCode: http.StatusOK,
   422  			Headers: http.Header(map[string][]string{
   423  				"Content-Length": {fmt.Sprint(len(b1))},
   424  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   425  			}),
   426  		},
   427  	})
   428  
   429  	e, c := testServer(m)
   430  	defer c()
   431  
   432  	ctx := context.Background()
   433  	r, err := NewRepository(ctx, repo, e, nil)
   434  	if err != nil {
   435  		t.Fatal(err)
   436  	}
   437  	l := r.Blobs(ctx)
   438  
   439  	upload, err := l.Create(ctx)
   440  	if err != nil {
   441  		t.Fatal(err)
   442  	}
   443  
   444  	if upload.ID() != uploadID {
   445  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
   446  	}
   447  
   448  	n, err := upload.ReadFrom(bytes.NewReader(b1))
   449  	if err != nil {
   450  		t.Fatal(err)
   451  	}
   452  	if n != int64(len(b1)) {
   453  		t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
   454  	}
   455  
   456  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   457  		Digest: dgst,
   458  		Size:   int64(len(b1)),
   459  	})
   460  	if err != nil {
   461  		t.Fatal(err)
   462  	}
   463  
   464  	if blob.Size != int64(len(b1)) {
   465  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   466  	}
   467  }
   468  
   469  func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
   470  	blobs := make([]schema1.FSLayer, blobCount)
   471  	history := make([]schema1.History, blobCount)
   472  
   473  	for i := 0; i < blobCount; i++ {
   474  		dgst, blob := newRandomBlob((i % 5) * 16)
   475  
   476  		blobs[i] = schema1.FSLayer{BlobSum: dgst}
   477  		history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)}
   478  	}
   479  
   480  	m := schema1.Manifest{
   481  		Name:         name,
   482  		Tag:          tag,
   483  		Architecture: "x86",
   484  		FSLayers:     blobs,
   485  		History:      history,
   486  		Versioned: manifest.Versioned{
   487  			SchemaVersion: 1,
   488  		},
   489  	}
   490  
   491  	pk, err := libtrust.GenerateECP256PrivateKey()
   492  	if err != nil {
   493  		panic(err)
   494  	}
   495  
   496  	sm, err := schema1.Sign(&m, pk)
   497  	if err != nil {
   498  		panic(err)
   499  	}
   500  
   501  	return sm, digest.FromBytes(sm.Canonical), sm.Canonical
   502  }
   503  
   504  func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) {
   505  	actualDigest := digest.FromBytes(content)
   506  	getReqWithEtag := testutil.Request{
   507  		Method: "GET",
   508  		Route:  "/v2/" + repo + "/manifests/" + reference,
   509  		Headers: http.Header(map[string][]string{
   510  			"If-None-Match": {fmt.Sprintf(`"%s"`, dgst)},
   511  		}),
   512  	}
   513  
   514  	var getRespWithEtag testutil.Response
   515  	if actualDigest.String() == dgst {
   516  		getRespWithEtag = testutil.Response{
   517  			StatusCode: http.StatusNotModified,
   518  			Body:       []byte{},
   519  			Headers: http.Header(map[string][]string{
   520  				"Content-Length": {"0"},
   521  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   522  				"Content-Type":   {schema1.MediaTypeManifest},
   523  			}),
   524  		}
   525  	} else {
   526  		getRespWithEtag = testutil.Response{
   527  			StatusCode: http.StatusOK,
   528  			Body:       content,
   529  			Headers: http.Header(map[string][]string{
   530  				"Content-Length": {fmt.Sprint(len(content))},
   531  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   532  				"Content-Type":   {schema1.MediaTypeManifest},
   533  			}),
   534  		}
   535  
   536  	}
   537  	*m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag})
   538  }
   539  
   540  func addTestManifest(repo, reference string, content []byte, m *testutil.RequestResponseMap) {
   541  	*m = append(*m, testutil.RequestResponseMapping{
   542  		Request: testutil.Request{
   543  			Method: "GET",
   544  			Route:  "/v2/" + repo + "/manifests/" + reference,
   545  		},
   546  		Response: testutil.Response{
   547  			StatusCode: http.StatusOK,
   548  			Body:       content,
   549  			Headers: http.Header(map[string][]string{
   550  				"Content-Length": {fmt.Sprint(len(content))},
   551  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   552  				"Content-Type":   {schema1.MediaTypeManifest},
   553  			}),
   554  		},
   555  	})
   556  	*m = append(*m, testutil.RequestResponseMapping{
   557  		Request: testutil.Request{
   558  			Method: "HEAD",
   559  			Route:  "/v2/" + repo + "/manifests/" + reference,
   560  		},
   561  		Response: testutil.Response{
   562  			StatusCode: http.StatusOK,
   563  			Headers: http.Header(map[string][]string{
   564  				"Content-Length": {fmt.Sprint(len(content))},
   565  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   566  				"Content-Type":   {schema1.MediaTypeManifest},
   567  			}),
   568  		},
   569  	})
   570  
   571  }
   572  
   573  func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
   574  	if m1.Name != m2.Name {
   575  		return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name)
   576  	}
   577  	if m1.Tag != m2.Tag {
   578  		return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag)
   579  	}
   580  	if len(m1.FSLayers) != len(m2.FSLayers) {
   581  		return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers))
   582  	}
   583  	for i := range m1.FSLayers {
   584  		if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum {
   585  			return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum)
   586  		}
   587  	}
   588  	if len(m1.History) != len(m2.History) {
   589  		return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History))
   590  	}
   591  	for i := range m1.History {
   592  		if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility {
   593  			return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility)
   594  		}
   595  	}
   596  	return nil
   597  }
   598  
   599  func TestV1ManifestFetch(t *testing.T) {
   600  	ctx := context.Background()
   601  	repo := "test.example.com/repo"
   602  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   603  	var m testutil.RequestResponseMap
   604  	_, pl, err := m1.Payload()
   605  	if err != nil {
   606  		t.Fatal(err)
   607  	}
   608  	addTestManifest(repo, dgst.String(), pl, &m)
   609  	addTestManifest(repo, "latest", pl, &m)
   610  
   611  	e, c := testServer(m)
   612  	defer c()
   613  
   614  	r, err := NewRepository(context.Background(), repo, e, nil)
   615  	if err != nil {
   616  		t.Fatal(err)
   617  	}
   618  	ms, err := r.Manifests(ctx)
   619  	if err != nil {
   620  		t.Fatal(err)
   621  	}
   622  
   623  	ok, err := ms.Exists(ctx, dgst)
   624  	if err != nil {
   625  		t.Fatal(err)
   626  	}
   627  	if !ok {
   628  		t.Fatal("Manifest does not exist")
   629  	}
   630  
   631  	manifest, err := ms.Get(ctx, dgst)
   632  	if err != nil {
   633  		t.Fatal(err)
   634  	}
   635  	v1manifest, ok := manifest.(*schema1.SignedManifest)
   636  	if !ok {
   637  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   638  	}
   639  
   640  	if err := checkEqualManifest(v1manifest, m1); err != nil {
   641  		t.Fatal(err)
   642  	}
   643  
   644  	manifest, err = ms.Get(ctx, dgst, WithTag("latest"))
   645  	if err != nil {
   646  		t.Fatal(err)
   647  	}
   648  	v1manifest, ok = manifest.(*schema1.SignedManifest)
   649  	if !ok {
   650  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   651  	}
   652  
   653  	if err = checkEqualManifest(v1manifest, m1); err != nil {
   654  		t.Fatal(err)
   655  	}
   656  }
   657  
   658  func TestManifestFetchWithEtag(t *testing.T) {
   659  	repo := "test.example.com/repo/by/tag"
   660  	_, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
   661  	var m testutil.RequestResponseMap
   662  	addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
   663  
   664  	e, c := testServer(m)
   665  	defer c()
   666  
   667  	ctx := context.Background()
   668  	r, err := NewRepository(ctx, repo, e, nil)
   669  	if err != nil {
   670  		t.Fatal(err)
   671  	}
   672  
   673  	ms, err := r.Manifests(ctx)
   674  	if err != nil {
   675  		t.Fatal(err)
   676  	}
   677  
   678  	clientManifestService, ok := ms.(*manifests)
   679  	if !ok {
   680  		panic("wrong type for client manifest service")
   681  	}
   682  	_, err = clientManifestService.Get(ctx, d1, WithTag("latest"), AddEtagToTag("latest", d1.String()))
   683  	if err != distribution.ErrManifestNotModified {
   684  		t.Fatal(err)
   685  	}
   686  }
   687  
   688  func TestManifestDelete(t *testing.T) {
   689  	repo := "test.example.com/repo/delete"
   690  	_, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   691  	_, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   692  	var m testutil.RequestResponseMap
   693  	m = append(m, testutil.RequestResponseMapping{
   694  		Request: testutil.Request{
   695  			Method: "DELETE",
   696  			Route:  "/v2/" + repo + "/manifests/" + dgst1.String(),
   697  		},
   698  		Response: testutil.Response{
   699  			StatusCode: http.StatusAccepted,
   700  			Headers: http.Header(map[string][]string{
   701  				"Content-Length": {"0"},
   702  			}),
   703  		},
   704  	})
   705  
   706  	e, c := testServer(m)
   707  	defer c()
   708  
   709  	r, err := NewRepository(context.Background(), repo, e, nil)
   710  	if err != nil {
   711  		t.Fatal(err)
   712  	}
   713  	ctx := context.Background()
   714  	ms, err := r.Manifests(ctx)
   715  	if err != nil {
   716  		t.Fatal(err)
   717  	}
   718  
   719  	if err := ms.Delete(ctx, dgst1); err != nil {
   720  		t.Fatal(err)
   721  	}
   722  	if err := ms.Delete(ctx, dgst2); err == nil {
   723  		t.Fatal("Expected error deleting unknown manifest")
   724  	}
   725  	// TODO(dmcgowan): Check for specific unknown error
   726  }
   727  
   728  func TestManifestPut(t *testing.T) {
   729  	repo := "test.example.com/repo/delete"
   730  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
   731  
   732  	_, payload, err := m1.Payload()
   733  	if err != nil {
   734  		t.Fatal(err)
   735  	}
   736  	var m testutil.RequestResponseMap
   737  	m = append(m, testutil.RequestResponseMapping{
   738  		Request: testutil.Request{
   739  			Method: "PUT",
   740  			Route:  "/v2/" + repo + "/manifests/other",
   741  			Body:   payload,
   742  		},
   743  		Response: testutil.Response{
   744  			StatusCode: http.StatusAccepted,
   745  			Headers: http.Header(map[string][]string{
   746  				"Content-Length":        {"0"},
   747  				"Docker-Content-Digest": {dgst.String()},
   748  			}),
   749  		},
   750  	})
   751  
   752  	e, c := testServer(m)
   753  	defer c()
   754  
   755  	r, err := NewRepository(context.Background(), repo, e, nil)
   756  	if err != nil {
   757  		t.Fatal(err)
   758  	}
   759  	ctx := context.Background()
   760  	ms, err := r.Manifests(ctx)
   761  	if err != nil {
   762  		t.Fatal(err)
   763  	}
   764  
   765  	if _, err := ms.Put(ctx, m1, WithTag(m1.Tag)); err != nil {
   766  		t.Fatal(err)
   767  	}
   768  
   769  	// TODO(dmcgowan): Check for invalid input error
   770  }
   771  
   772  func TestManifestTags(t *testing.T) {
   773  	repo := "test.example.com/repo/tags/list"
   774  	tagsList := []byte(strings.TrimSpace(`
   775  {
   776  	"name": "test.example.com/repo/tags/list",
   777  	"tags": [
   778  		"tag1",
   779  		"tag2",
   780  		"funtag"
   781  	]
   782  }
   783  	`))
   784  	var m testutil.RequestResponseMap
   785  	for i := 0; i < 3; i++ {
   786  		m = append(m, testutil.RequestResponseMapping{
   787  			Request: testutil.Request{
   788  				Method: "GET",
   789  				Route:  "/v2/" + repo + "/tags/list",
   790  			},
   791  			Response: testutil.Response{
   792  				StatusCode: http.StatusOK,
   793  				Body:       tagsList,
   794  				Headers: http.Header(map[string][]string{
   795  					"Content-Length": {fmt.Sprint(len(tagsList))},
   796  					"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   797  				}),
   798  			},
   799  		})
   800  	}
   801  	e, c := testServer(m)
   802  	defer c()
   803  
   804  	r, err := NewRepository(context.Background(), repo, e, nil)
   805  	if err != nil {
   806  		t.Fatal(err)
   807  	}
   808  
   809  	ctx := context.Background()
   810  	tagService := r.Tags(ctx)
   811  
   812  	tags, err := tagService.All(ctx)
   813  	if err != nil {
   814  		t.Fatal(err)
   815  	}
   816  	if len(tags) != 3 {
   817  		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
   818  	}
   819  
   820  	expected := map[string]struct{}{
   821  		"tag1":   {},
   822  		"tag2":   {},
   823  		"funtag": {},
   824  	}
   825  	for _, t := range tags {
   826  		delete(expected, t)
   827  	}
   828  	if len(expected) != 0 {
   829  		t.Fatalf("unexpected tags returned: %v", expected)
   830  	}
   831  	// TODO(dmcgowan): Check for error cases
   832  }
   833  
   834  func TestManifestUnauthorized(t *testing.T) {
   835  	repo := "test.example.com/repo"
   836  	_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   837  	var m testutil.RequestResponseMap
   838  
   839  	m = append(m, testutil.RequestResponseMapping{
   840  		Request: testutil.Request{
   841  			Method: "GET",
   842  			Route:  "/v2/" + repo + "/manifests/" + dgst.String(),
   843  		},
   844  		Response: testutil.Response{
   845  			StatusCode: http.StatusUnauthorized,
   846  			Body:       []byte("<html>garbage</html>"),
   847  		},
   848  	})
   849  
   850  	e, c := testServer(m)
   851  	defer c()
   852  
   853  	r, err := NewRepository(context.Background(), repo, e, nil)
   854  	if err != nil {
   855  		t.Fatal(err)
   856  	}
   857  	ctx := context.Background()
   858  	ms, err := r.Manifests(ctx)
   859  	if err != nil {
   860  		t.Fatal(err)
   861  	}
   862  
   863  	_, err = ms.Get(ctx, dgst)
   864  	if err == nil {
   865  		t.Fatal("Expected error fetching manifest")
   866  	}
   867  	v2Err, ok := err.(errcode.Error)
   868  	if !ok {
   869  		t.Fatalf("Unexpected error type: %#v", err)
   870  	}
   871  	if v2Err.Code != errcode.ErrorCodeUnauthorized {
   872  		t.Fatalf("Unexpected error code: %s", v2Err.Code.String())
   873  	}
   874  	if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected {
   875  		t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
   876  	}
   877  }
   878  
   879  func TestCatalog(t *testing.T) {
   880  	var m testutil.RequestResponseMap
   881  	addTestCatalog(
   882  		"/v2/_catalog?n=5",
   883  		[]byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
   884  
   885  	e, c := testServer(m)
   886  	defer c()
   887  
   888  	entries := make([]string, 5)
   889  
   890  	r, err := NewRegistry(context.Background(), e, nil)
   891  	if err != nil {
   892  		t.Fatal(err)
   893  	}
   894  
   895  	ctx := context.Background()
   896  	numFilled, err := r.Repositories(ctx, entries, "")
   897  	if err != io.EOF {
   898  		t.Fatal(err)
   899  	}
   900  
   901  	if numFilled != 3 {
   902  		t.Fatalf("Got wrong number of repos")
   903  	}
   904  }
   905  
   906  func TestCatalogInParts(t *testing.T) {
   907  	var m testutil.RequestResponseMap
   908  	addTestCatalog(
   909  		"/v2/_catalog?n=2",
   910  		[]byte("{\"repositories\":[\"bar\", \"baz\"]}"),
   911  		"</v2/_catalog?last=baz&n=2>", &m)
   912  	addTestCatalog(
   913  		"/v2/_catalog?last=baz&n=2",
   914  		[]byte("{\"repositories\":[\"foo\"]}"),
   915  		"", &m)
   916  
   917  	e, c := testServer(m)
   918  	defer c()
   919  
   920  	entries := make([]string, 2)
   921  
   922  	r, err := NewRegistry(context.Background(), e, nil)
   923  	if err != nil {
   924  		t.Fatal(err)
   925  	}
   926  
   927  	ctx := context.Background()
   928  	numFilled, err := r.Repositories(ctx, entries, "")
   929  	if err != nil {
   930  		t.Fatal(err)
   931  	}
   932  
   933  	if numFilled != 2 {
   934  		t.Fatalf("Got wrong number of repos")
   935  	}
   936  
   937  	numFilled, err = r.Repositories(ctx, entries, "baz")
   938  	if err != io.EOF {
   939  		t.Fatal(err)
   940  	}
   941  
   942  	if numFilled != 1 {
   943  		t.Fatalf("Got wrong number of repos")
   944  	}
   945  }
   946  
   947  func TestSanitizeLocation(t *testing.T) {
   948  	for _, testcase := range []struct {
   949  		description string
   950  		location    string
   951  		source      string
   952  		expected    string
   953  		err         error
   954  	}{
   955  		{
   956  			description: "ensure relative location correctly resolved",
   957  			location:    "/v2/foo/baasdf",
   958  			source:      "http://blahalaja.com/v1",
   959  			expected:    "http://blahalaja.com/v2/foo/baasdf",
   960  		},
   961  		{
   962  			description: "ensure parameters are preserved",
   963  			location:    "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
   964  			source:      "http://blahalaja.com/v1",
   965  			expected:    "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
   966  		},
   967  		{
   968  			description: "ensure new hostname overidden",
   969  			location:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
   970  			source:      "http://blahalaja.com/v1",
   971  			expected:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
   972  		},
   973  	} {
   974  		fatalf := func(format string, args ...interface{}) {
   975  			t.Fatalf(testcase.description+": "+format, args...)
   976  		}
   977  
   978  		s, err := sanitizeLocation(testcase.location, testcase.source)
   979  		if err != testcase.err {
   980  			if testcase.err != nil {
   981  				fatalf("expected error: %v != %v", err, testcase)
   982  			} else {
   983  				fatalf("unexpected error sanitizing: %v", err)
   984  			}
   985  		}
   986  
   987  		if s != testcase.expected {
   988  			fatalf("bad sanitize: %q != %q", s, testcase.expected)
   989  		}
   990  	}
   991  }