registry_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. package ollama
  2. import (
  3. "bytes"
  4. "cmp"
  5. "context"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/fs"
  11. "math/rand/v2"
  12. "net/http"
  13. "net/http/httptest"
  14. "os"
  15. "path"
  16. "reflect"
  17. "slices"
  18. "strings"
  19. "testing"
  20. "time"
  21. "github.com/ollama/ollama/server/internal/cache/blob"
  22. "github.com/ollama/ollama/server/internal/chunks"
  23. "github.com/ollama/ollama/server/internal/testutil"
  24. )
  25. func TestManifestMarshalJSON(t *testing.T) {
  26. // All manifests should contain an "empty" config object.
  27. var m Manifest
  28. data, err := json.Marshal(m)
  29. if err != nil {
  30. t.Fatal(err)
  31. }
  32. if !bytes.Contains(data, []byte(`"config":{"digest":"sha256:`)) {
  33. t.Error("expected manifest to contain empty config")
  34. t.Fatalf("got:\n%s", string(data))
  35. }
  36. }
  37. var errRoundTrip = errors.New("forced roundtrip error")
  38. type recordRoundTripper http.HandlerFunc
  39. func (rr recordRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
  40. w := httptest.NewRecorder()
  41. rr(w, req)
  42. if w.Code == 499 {
  43. return nil, errRoundTrip
  44. }
  45. resp := w.Result()
  46. // For some reason, Response.Request is not set by httptest.NewRecorder, so we
  47. // set it manually.
  48. resp.Request = req
  49. return w.Result(), nil
  50. }
  51. // newClient constructs a cache with predefined manifests for testing. The manifests are:
  52. //
  53. // empty: no data
  54. // zero: no layers
  55. // single: one layer with the contents "exists"
  56. // multiple: two layers with the contents "exists" and "here"
  57. // notfound: a layer that does not exist in the cache
  58. // null: one null layer (e.g. [null])
  59. // sizemismatch: one valid layer, and one with a size mismatch (file size is less than the reported size)
  60. // invalid: a layer with invalid JSON data
  61. //
  62. // Tests that want to ensure the client does not communicate with the upstream
  63. // registry should pass a nil handler, which will cause a panic if
  64. // communication is attempted.
  65. //
  66. // To simulate a network error, pass a handler that returns a 499 status code.
  67. func newClient(t *testing.T, h http.HandlerFunc) (*Registry, *blob.DiskCache) {
  68. t.Helper()
  69. c, err := blob.Open(t.TempDir())
  70. if err != nil {
  71. t.Fatal(err)
  72. }
  73. mklayer := func(data string) *Layer {
  74. return &Layer{
  75. Digest: importBytes(t, c, data),
  76. Size: int64(len(data)),
  77. }
  78. }
  79. r := &Registry{
  80. Cache: c,
  81. HTTPClient: &http.Client{
  82. Transport: recordRoundTripper(h),
  83. },
  84. }
  85. link := func(name string, manifest string) {
  86. n, err := r.parseName(name)
  87. if err != nil {
  88. panic(err)
  89. }
  90. d, err := c.Import(bytes.NewReader([]byte(manifest)), int64(len(manifest)))
  91. if err != nil {
  92. panic(err)
  93. }
  94. if err := c.Link(n.String(), d); err != nil {
  95. panic(err)
  96. }
  97. }
  98. commit := func(name string, layers ...*Layer) {
  99. t.Helper()
  100. data, err := json.Marshal(&Manifest{Layers: layers})
  101. if err != nil {
  102. t.Fatal(err)
  103. }
  104. link(name, string(data))
  105. }
  106. link("empty", "")
  107. commit("zero")
  108. commit("single", mklayer("exists"))
  109. commit("multiple", mklayer("exists"), mklayer("present"))
  110. commit("notfound", &Layer{Digest: blob.DigestFromBytes("notfound"), Size: int64(len("notfound"))})
  111. commit("null", nil)
  112. commit("sizemismatch", mklayer("exists"), &Layer{Digest: blob.DigestFromBytes("present"), Size: 499})
  113. link("invalid", "!!!!!")
  114. return r, c
  115. }
  116. func okHandler(w http.ResponseWriter, r *http.Request) {
  117. w.WriteHeader(http.StatusOK)
  118. }
  119. func checkErrCode(t *testing.T, err error, status int, code string) {
  120. t.Helper()
  121. var e *Error
  122. if !errors.As(err, &e) || e.Status != status || e.Code != code {
  123. t.Errorf("err = %v; want %v %v", err, status, code)
  124. }
  125. }
  126. func importBytes(t *testing.T, c *blob.DiskCache, data string) blob.Digest {
  127. d, err := c.Import(strings.NewReader(data), int64(len(data)))
  128. if err != nil {
  129. t.Fatal(err)
  130. }
  131. return d
  132. }
  133. func withTraceUnexpected(ctx context.Context) (context.Context, *Trace) {
  134. t := &Trace{Update: func(*Layer, int64, error) { panic("unexpected") }}
  135. return WithTrace(ctx, t), t
  136. }
  137. func TestPushZero(t *testing.T) {
  138. rc, _ := newClient(t, okHandler)
  139. err := rc.Push(t.Context(), "empty", nil)
  140. if !errors.Is(err, ErrManifestInvalid) {
  141. t.Errorf("err = %v; want %v", err, ErrManifestInvalid)
  142. }
  143. }
  144. func TestPushSingle(t *testing.T) {
  145. rc, _ := newClient(t, okHandler)
  146. err := rc.Push(t.Context(), "single", nil)
  147. testutil.Check(t, err)
  148. }
  149. func TestPushMultiple(t *testing.T) {
  150. rc, _ := newClient(t, okHandler)
  151. err := rc.Push(t.Context(), "multiple", nil)
  152. testutil.Check(t, err)
  153. }
  154. func TestPushNotFound(t *testing.T) {
  155. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  156. t.Errorf("unexpected request: %v", r)
  157. })
  158. err := rc.Push(t.Context(), "notfound", nil)
  159. if !errors.Is(err, fs.ErrNotExist) {
  160. t.Errorf("err = %v; want %v", err, fs.ErrNotExist)
  161. }
  162. }
  163. func TestPushNullLayer(t *testing.T) {
  164. rc, _ := newClient(t, nil)
  165. err := rc.Push(t.Context(), "null", nil)
  166. if err == nil || !strings.Contains(err.Error(), "invalid manifest") {
  167. t.Errorf("err = %v; want invalid manifest", err)
  168. }
  169. }
  170. func TestPushSizeMismatch(t *testing.T) {
  171. rc, _ := newClient(t, nil)
  172. ctx, _ := withTraceUnexpected(t.Context())
  173. got := rc.Push(ctx, "sizemismatch", nil)
  174. if got == nil || !strings.Contains(got.Error(), "size mismatch") {
  175. t.Errorf("err = %v; want size mismatch", got)
  176. }
  177. }
  178. func TestPushInvalid(t *testing.T) {
  179. rc, _ := newClient(t, nil)
  180. err := rc.Push(t.Context(), "invalid", nil)
  181. if err == nil || !strings.Contains(err.Error(), "invalid manifest") {
  182. t.Errorf("err = %v; want invalid manifest", err)
  183. }
  184. }
  185. func TestPushExistsAtRemote(t *testing.T) {
  186. var pushed bool
  187. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  188. if strings.Contains(r.URL.Path, "/uploads/") {
  189. if !pushed {
  190. // First push. Return an uploadURL.
  191. pushed = true
  192. w.Header().Set("Location", "http://blob.store/blobs/123")
  193. return
  194. }
  195. w.WriteHeader(http.StatusAccepted)
  196. return
  197. }
  198. io.Copy(io.Discard, r.Body)
  199. w.WriteHeader(http.StatusOK)
  200. })
  201. rc.MaxStreams = 1 // prevent concurrent uploads
  202. var errs []error
  203. ctx := WithTrace(t.Context(), &Trace{
  204. Update: func(_ *Layer, n int64, err error) {
  205. // uploading one at a time so no need to lock
  206. errs = append(errs, err)
  207. },
  208. })
  209. check := testutil.Checker(t)
  210. err := rc.Push(ctx, "single", nil)
  211. check(err)
  212. if !errors.Is(errors.Join(errs...), nil) {
  213. t.Errorf("errs = %v; want %v", errs, []error{ErrCached})
  214. }
  215. err = rc.Push(ctx, "single", nil)
  216. check(err)
  217. }
  218. func TestPushRemoteError(t *testing.T) {
  219. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  220. if strings.Contains(r.URL.Path, "/blobs/") {
  221. w.WriteHeader(500)
  222. io.WriteString(w, `{"errors":[{"code":"blob_error"}]}`)
  223. return
  224. }
  225. })
  226. got := rc.Push(t.Context(), "single", nil)
  227. checkErrCode(t, got, 500, "blob_error")
  228. }
  229. func TestPushLocationError(t *testing.T) {
  230. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  231. w.Header().Set("Location", ":///x")
  232. w.WriteHeader(http.StatusAccepted)
  233. })
  234. got := rc.Push(t.Context(), "single", nil)
  235. wantContains := "invalid upload URL"
  236. if got == nil || !strings.Contains(got.Error(), wantContains) {
  237. t.Errorf("err = %v; want to contain %v", got, wantContains)
  238. }
  239. }
  240. func TestPushUploadRoundtripError(t *testing.T) {
  241. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  242. if r.Host == "blob.store" {
  243. w.WriteHeader(499) // force RoundTrip error on upload
  244. return
  245. }
  246. w.Header().Set("Location", "http://blob.store/blobs/123")
  247. })
  248. got := rc.Push(t.Context(), "single", nil)
  249. if !errors.Is(got, errRoundTrip) {
  250. t.Errorf("got = %v; want %v", got, errRoundTrip)
  251. }
  252. }
  253. func TestPushUploadFileOpenError(t *testing.T) {
  254. rc, c := newClient(t, okHandler)
  255. ctx := WithTrace(t.Context(), &Trace{
  256. Update: func(l *Layer, _ int64, err error) {
  257. // Remove the file just before it is opened for upload,
  258. // but after the initial Stat that happens before the
  259. // upload starts
  260. os.Remove(c.GetFile(l.Digest))
  261. },
  262. })
  263. got := rc.Push(ctx, "single", nil)
  264. if !errors.Is(got, fs.ErrNotExist) {
  265. t.Errorf("got = %v; want fs.ErrNotExist", got)
  266. }
  267. }
  268. func TestPushCommitRoundtripError(t *testing.T) {
  269. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  270. if strings.Contains(r.URL.Path, "/blobs/") {
  271. panic("unexpected")
  272. }
  273. w.WriteHeader(499) // force RoundTrip error
  274. })
  275. err := rc.Push(t.Context(), "zero", nil)
  276. if !errors.Is(err, errRoundTrip) {
  277. t.Errorf("err = %v; want %v", err, errRoundTrip)
  278. }
  279. }
  280. func checkNotExist(t *testing.T, err error) {
  281. t.Helper()
  282. if !errors.Is(err, fs.ErrNotExist) {
  283. t.Fatalf("err = %v; want fs.ErrNotExist", err)
  284. }
  285. }
  286. func TestRegistryPullInvalidName(t *testing.T) {
  287. rc, _ := newClient(t, nil)
  288. err := rc.Pull(t.Context(), "://")
  289. if !errors.Is(err, ErrNameInvalid) {
  290. t.Errorf("err = %v; want %v", err, ErrNameInvalid)
  291. }
  292. }
  293. func TestRegistryPullInvalidManifest(t *testing.T) {
  294. cases := []string{
  295. "",
  296. "null",
  297. "!!!",
  298. `{"layers":[]}`,
  299. }
  300. for _, resp := range cases {
  301. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  302. io.WriteString(w, resp)
  303. })
  304. err := rc.Pull(t.Context(), "x")
  305. if !errors.Is(err, ErrManifestInvalid) {
  306. t.Errorf("err = %v; want invalid manifest", err)
  307. }
  308. }
  309. }
  310. func TestRegistryPullNotCached(t *testing.T) {
  311. check := testutil.Checker(t)
  312. var c *blob.DiskCache
  313. var rc *Registry
  314. d := blob.DigestFromBytes("some data")
  315. rc, c = newClient(t, func(w http.ResponseWriter, r *http.Request) {
  316. if strings.Contains(r.URL.Path, "/blobs/") {
  317. io.WriteString(w, "some data")
  318. return
  319. }
  320. fmt.Fprintf(w, `{"layers":[{"digest":%q,"size":9}]}`, d)
  321. })
  322. // Confirm that the layer does not exist locally
  323. _, err := rc.ResolveLocal("model")
  324. checkNotExist(t, err)
  325. _, err = c.Get(d)
  326. checkNotExist(t, err)
  327. err = rc.Pull(t.Context(), "model")
  328. check(err)
  329. mw, err := rc.Resolve(t.Context(), "model")
  330. check(err)
  331. mg, err := rc.ResolveLocal("model")
  332. check(err)
  333. if !reflect.DeepEqual(mw, mg) {
  334. t.Errorf("mw = %v; mg = %v", mw, mg)
  335. }
  336. // Confirm successful download
  337. info, err := c.Get(d)
  338. check(err)
  339. if info.Digest != d {
  340. t.Errorf("info.Digest = %v; want %v", info.Digest, d)
  341. }
  342. if info.Size != 9 {
  343. t.Errorf("info.Size = %v; want %v", info.Size, 9)
  344. }
  345. data, err := os.ReadFile(c.GetFile(d))
  346. check(err)
  347. if string(data) != "some data" {
  348. t.Errorf("data = %q; want %q", data, "exists")
  349. }
  350. }
  351. func TestRegistryPullCached(t *testing.T) {
  352. cached := blob.DigestFromBytes("exists")
  353. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  354. if strings.Contains(r.URL.Path, "/blobs/") {
  355. w.WriteHeader(499) // should not be called
  356. return
  357. }
  358. if strings.Contains(r.URL.Path, "/manifests/") {
  359. fmt.Fprintf(w, `{"layers":[{"digest":%q,"size":6}]}`, cached)
  360. }
  361. })
  362. var errs []error
  363. var reads []int64
  364. ctx := WithTrace(t.Context(), &Trace{
  365. Update: func(d *Layer, n int64, err error) {
  366. t.Logf("update %v %d %v", d, n, err)
  367. reads = append(reads, n)
  368. errs = append(errs, err)
  369. },
  370. })
  371. ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
  372. defer cancel()
  373. err := rc.Pull(ctx, "single")
  374. testutil.Check(t, err)
  375. want := []int64{6}
  376. if !errors.Is(errors.Join(errs...), ErrCached) {
  377. t.Errorf("errs = %v; want %v", errs, ErrCached)
  378. }
  379. if !slices.Equal(reads, want) {
  380. t.Errorf("pairs = %v; want %v", reads, want)
  381. }
  382. }
  383. func TestRegistryPullManifestNotFound(t *testing.T) {
  384. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  385. w.WriteHeader(http.StatusNotFound)
  386. })
  387. err := rc.Pull(t.Context(), "notfound")
  388. checkErrCode(t, err, 404, "")
  389. }
  390. func TestRegistryPullResolveRemoteError(t *testing.T) {
  391. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  392. w.WriteHeader(http.StatusInternalServerError)
  393. io.WriteString(w, `{"errors":[{"code":"an_error"}]}`)
  394. })
  395. err := rc.Pull(t.Context(), "single")
  396. checkErrCode(t, err, 500, "an_error")
  397. }
  398. func TestRegistryPullResolveRoundtripError(t *testing.T) {
  399. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  400. if strings.Contains(r.URL.Path, "/manifests/") {
  401. w.WriteHeader(499) // force RoundTrip error
  402. return
  403. }
  404. })
  405. err := rc.Pull(t.Context(), "single")
  406. if !errors.Is(err, errRoundTrip) {
  407. t.Errorf("err = %v; want %v", err, errRoundTrip)
  408. }
  409. }
  410. // TestRegistryPullMixedCachedNotCached tests that cached layers do not
  411. // interfere with pulling layers that are not cached
  412. func TestRegistryPullMixedCachedNotCached(t *testing.T) {
  413. x := blob.DigestFromBytes("xxxxxx")
  414. e := blob.DigestFromBytes("exists")
  415. y := blob.DigestFromBytes("yyyyyy")
  416. for i := range 10 {
  417. t.Logf("iteration %d", i)
  418. digests := []blob.Digest{x, e, y}
  419. rand.Shuffle(len(digests), func(i, j int) {
  420. digests[i], digests[j] = digests[j], digests[i]
  421. })
  422. manifest := fmt.Sprintf(`{
  423. "layers": [
  424. {"digest":"%s","size":6},
  425. {"digest":"%s","size":6},
  426. {"digest":"%s","size":6}
  427. ]
  428. }`, digests[0], digests[1], digests[2])
  429. rc, c := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  430. switch path.Base(r.URL.Path) {
  431. case "latest":
  432. io.WriteString(w, manifest)
  433. case x.String():
  434. io.WriteString(w, "xxxxxx")
  435. case e.String():
  436. io.WriteString(w, "exists")
  437. case y.String():
  438. io.WriteString(w, "yyyyyy")
  439. default:
  440. panic(fmt.Sprintf("unexpected request: %v", r))
  441. }
  442. })
  443. ctx := WithTrace(t.Context(), &Trace{
  444. Update: func(l *Layer, n int64, err error) {
  445. t.Logf("update %v %d %v", l, n, err)
  446. },
  447. })
  448. // Check that we pull all layers that we can.
  449. err := rc.Pull(ctx, "mixed")
  450. if err != nil {
  451. t.Fatal(err)
  452. }
  453. for _, d := range digests {
  454. info, err := c.Get(d)
  455. if err != nil {
  456. t.Fatalf("Get(%v): %v", d, err)
  457. }
  458. if info.Size != 6 {
  459. t.Errorf("info.Size = %v; want %v", info.Size, 6)
  460. }
  461. }
  462. }
  463. }
  464. func TestRegistryPullChunking(t *testing.T) {
  465. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  466. t.Log("request:", r.URL.Host, r.Method, r.URL.Path, r.Header.Get("Range"))
  467. if r.URL.Host != "blob.store" {
  468. // The production registry redirects to the blob store.
  469. http.Redirect(w, r, "http://blob.store"+r.URL.Path, http.StatusFound)
  470. return
  471. }
  472. if strings.Contains(r.URL.Path, "/blobs/") {
  473. rng := r.Header.Get("Range")
  474. if rng == "" {
  475. http.Error(w, "missing range", http.StatusBadRequest)
  476. return
  477. }
  478. _, c, err := chunks.ParseRange(r.Header.Get("Range"))
  479. if err != nil {
  480. panic(err)
  481. }
  482. io.WriteString(w, "remote"[c.Start:c.End+1])
  483. return
  484. }
  485. fmt.Fprintf(w, `{"layers":[{"digest":%q,"size":6}]}`, blob.DigestFromBytes("remote"))
  486. })
  487. // Force chunking by setting the threshold to less than the size of the
  488. // layer.
  489. rc.ChunkingThreshold = 3
  490. rc.MaxChunkSize = 3
  491. var reads []int64
  492. ctx := WithTrace(t.Context(), &Trace{
  493. Update: func(d *Layer, n int64, err error) {
  494. if err != nil {
  495. t.Errorf("update %v %d %v", d, n, err)
  496. }
  497. reads = append(reads, n)
  498. },
  499. })
  500. err := rc.Pull(ctx, "remote")
  501. testutil.Check(t, err)
  502. want := []int64{0, 3, 6}
  503. if !slices.Equal(reads, want) {
  504. t.Errorf("reads = %v; want %v", reads, want)
  505. }
  506. }
  507. func TestRegistryResolveByDigest(t *testing.T) {
  508. check := testutil.Checker(t)
  509. exists := blob.DigestFromBytes("exists")
  510. rc, _ := newClient(t, func(w http.ResponseWriter, r *http.Request) {
  511. if r.URL.Path != "/v2/alice/palace/blobs/"+exists.String() {
  512. w.WriteHeader(499) // should not hit manifest endpoint
  513. }
  514. fmt.Fprintf(w, `{"layers":[{"digest":%q,"size":5}]}`, exists)
  515. })
  516. _, err := rc.Resolve(t.Context(), "alice/palace@"+exists.String())
  517. check(err)
  518. }
  519. func TestInsecureSkipVerify(t *testing.T) {
  520. exists := blob.DigestFromBytes("exists")
  521. s := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  522. fmt.Fprintf(w, `{"layers":[{"digest":%q,"size":5}]}`, exists)
  523. }))
  524. defer s.Close()
  525. const name = "library/insecure"
  526. var rc Registry
  527. url := fmt.Sprintf("https://%s/%s", s.Listener.Addr(), name)
  528. _, err := rc.Resolve(t.Context(), url)
  529. if err == nil || !strings.Contains(err.Error(), "failed to verify") {
  530. t.Errorf("err = %v; want cert verifiction failure", err)
  531. }
  532. url = fmt.Sprintf("https+insecure://%s/%s", s.Listener.Addr(), name)
  533. _, err = rc.Resolve(t.Context(), url)
  534. testutil.Check(t, err)
  535. }
  536. func TestCanRetry(t *testing.T) {
  537. cases := []struct {
  538. err error
  539. want bool
  540. }{
  541. {nil, false},
  542. {errors.New("x"), false},
  543. {ErrCached, false},
  544. {ErrManifestInvalid, false},
  545. {ErrNameInvalid, false},
  546. {&Error{Status: 100}, false},
  547. {&Error{Status: 500}, true},
  548. }
  549. for _, tt := range cases {
  550. if got := canRetry(tt.err); got != tt.want {
  551. t.Errorf("CanRetry(%v) = %v; want %v", tt.err, got, tt.want)
  552. }
  553. }
  554. }
  555. func TestErrorUnmarshal(t *testing.T) {
  556. cases := []struct {
  557. name string
  558. data string
  559. want *Error
  560. wantErr bool
  561. }{
  562. {
  563. name: "errors empty",
  564. data: `{"errors":[]}`,
  565. wantErr: true,
  566. },
  567. {
  568. name: "errors empty",
  569. data: `{"errors":[]}`,
  570. wantErr: true,
  571. },
  572. {
  573. name: "errors single",
  574. data: `{"errors":[{"code":"blob_unknown"}]}`,
  575. want: &Error{Code: "blob_unknown", Message: ""},
  576. },
  577. {
  578. name: "errors multiple",
  579. data: `{"errors":[{"code":"blob_unknown"},{"code":"blob_error"}]}`,
  580. want: &Error{Code: "blob_unknown", Message: ""},
  581. },
  582. {
  583. name: "error empty",
  584. data: `{"error":""}`,
  585. wantErr: true,
  586. },
  587. {
  588. name: "error very empty",
  589. data: `{}`,
  590. wantErr: true,
  591. },
  592. {
  593. name: "error message",
  594. data: `{"error":"message", "code":"code"}`,
  595. want: &Error{Code: "code", Message: "message"},
  596. },
  597. {
  598. name: "invalid value",
  599. data: `{"error": 1}`,
  600. wantErr: true,
  601. },
  602. }
  603. for _, tt := range cases {
  604. t.Run(tt.name, func(t *testing.T) {
  605. var got Error
  606. err := json.Unmarshal([]byte(tt.data), &got)
  607. if err != nil {
  608. if tt.wantErr {
  609. return
  610. }
  611. t.Errorf("Unmarshal() error = %v", err)
  612. // fallthrough and check got
  613. }
  614. if tt.want == nil {
  615. tt.want = &Error{}
  616. }
  617. if !reflect.DeepEqual(got, *tt.want) {
  618. t.Errorf("got = %v; want %v", got, *tt.want)
  619. }
  620. })
  621. }
  622. }
  623. // TestParseNameErrors tests that parseName returns errors messages with enough
  624. // detail for users to debug naming issues they may encounter. Previous to this
  625. // test, the error messages were not very helpful and each problem was reported
  626. // as the same message.
  627. //
  628. // It is only for testing error messages, not that all invalids and valids are
  629. // covered. Those are in other tests for names.Name and blob.Digest.
  630. func TestParseNameExtendedErrors(t *testing.T) {
  631. cases := []struct {
  632. name string
  633. err error
  634. want string
  635. }{}
  636. var r Registry
  637. for _, tt := range cases {
  638. _, _, _, err := r.parseNameExtended(tt.name)
  639. if !errors.Is(err, tt.err) {
  640. t.Errorf("[%s]: err = %v; want %v", tt.name, err, tt.err)
  641. }
  642. if err != nil && !strings.Contains(err.Error(), tt.want) {
  643. t.Errorf("[%s]: err =\n\t%v\nwant\n\t%v", tt.name, err, tt.want)
  644. }
  645. }
  646. }
  647. func TestParseNameExtended(t *testing.T) {
  648. cases := []struct {
  649. in string
  650. scheme string
  651. name string
  652. digest string
  653. err string
  654. }{
  655. {in: "http://m", scheme: "http", name: "m"},
  656. {in: "https+insecure://m", scheme: "https+insecure", name: "m"},
  657. {in: "http+insecure://m", err: "unsupported scheme"},
  658. {in: "http://m@sha256:1111111111111111111111111111111111111111111111111111111111111111", scheme: "http", name: "m", digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111"},
  659. {in: "", err: "invalid or missing name"},
  660. {in: "m", scheme: "https", name: "m"},
  661. {in: "://", err: "invalid or missing name"},
  662. {in: "@sha256:deadbeef", err: "invalid digest"},
  663. {in: "@sha256:deadbeef@sha256:deadbeef", err: "invalid digest"},
  664. }
  665. for _, tt := range cases {
  666. t.Run(tt.in, func(t *testing.T) {
  667. var r Registry
  668. scheme, n, digest, err := r.parseNameExtended(tt.in)
  669. if err != nil {
  670. if tt.err == "" {
  671. t.Errorf("err = %v; want nil", err)
  672. } else if !strings.Contains(err.Error(), tt.err) {
  673. t.Errorf("err = %v; want %q", err, tt.err)
  674. }
  675. } else if tt.err != "" {
  676. t.Errorf("err = nil; want %q", tt.err)
  677. }
  678. if err == nil && !n.IsFullyQualified() {
  679. t.Errorf("name = %q; want fully qualified", n)
  680. }
  681. if scheme != tt.scheme {
  682. t.Errorf("scheme = %q; want %q", scheme, tt.scheme)
  683. }
  684. // smoke-test name is superset of tt.name
  685. if !strings.Contains(n.String(), tt.name) {
  686. t.Errorf("name = %q; want %q", n, tt.name)
  687. }
  688. tt.digest = cmp.Or(tt.digest, (&blob.Digest{}).String())
  689. if digest.String() != tt.digest {
  690. t.Errorf("digest = %q; want %q", digest, tt.digest)
  691. }
  692. })
  693. }
  694. }
  695. func TestUnlink(t *testing.T) {
  696. t.Run("found by name", func(t *testing.T) {
  697. rc, _ := newClient(t, nil)
  698. // confirm linked
  699. _, err := rc.ResolveLocal("single")
  700. if err != nil {
  701. t.Errorf("unexpected error: %v", err)
  702. }
  703. // unlink
  704. _, err = rc.Unlink("single")
  705. testutil.Check(t, err)
  706. // confirm unlinked
  707. _, err = rc.ResolveLocal("single")
  708. if !errors.Is(err, fs.ErrNotExist) {
  709. t.Errorf("err = %v; want fs.ErrNotExist", err)
  710. }
  711. })
  712. t.Run("not found by name", func(t *testing.T) {
  713. rc, _ := newClient(t, nil)
  714. ok, err := rc.Unlink("manifestNotFound")
  715. if err != nil {
  716. t.Fatal(err)
  717. }
  718. if ok {
  719. t.Error("expected not found")
  720. }
  721. })
  722. }