1package registrypush_test
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11
12 "github.com/bnema/zerowrap"
13 v1 "github.com/google/go-containerregistry/pkg/v1"
14 "github.com/google/go-containerregistry/pkg/v1/random"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17
18 registryhttp "github.com/bnema/gordon/internal/adapters/in/http/registry"
19 "github.com/bnema/gordon/internal/adapters/out/filesystem"
20 "github.com/bnema/gordon/internal/domain"
21 registrysvc "github.com/bnema/gordon/internal/usecase/registry"
22 "github.com/bnema/gordon/pkg/registrypush"
23)
24
25func TestIntegration_PushSingleLayerImage(t *testing.T) {
26 t.Parallel()
27
28 ctx := context.Background()
29 img, err := random.Image(512, 1)
30 require.NoError(t, err)
31
32 harness := newRegistryHarness(t, registryhttp.DefaultMaxBlobChunkSize)
33 defer harness.Close()
34
35 err = harness.Push(ctx, img, registrypush.WithChunkSize(256))
36 require.NoError(t, err)
37
38 harness.RequireManifestAndBlobs(ctx, t, img)
39}
40
41func TestIntegration_PushMultiLayerChunked(t *testing.T) {
42 t.Parallel()
43
44 ctx := context.Background()
45 img, err := random.Image(4096, 3)
46 require.NoError(t, err)
47
48 harness := newRegistryHarness(t, 1025)
49 defer harness.Close()
50
51 err = harness.Push(ctx, img, registrypush.WithChunkSize(1024))
52 require.NoError(t, err)
53
54 harness.RequireManifestAndBlobs(ctx, t, img)
55}
56
57func TestIntegration_PushSkipsExistingBlobs(t *testing.T) {
58 t.Parallel()
59
60 ctx := context.Background()
61 img, err := random.Image(1024, 2)
62 require.NoError(t, err)
63
64 harness := newRegistryHarness(t, registryhttp.DefaultMaxBlobChunkSize)
65 defer harness.Close()
66
67 err = harness.Push(ctx, img, registrypush.WithChunkSize(256))
68 require.NoError(t, err)
69
70 err = harness.Push(ctx, img, registrypush.WithChunkSize(256))
71 require.NoError(t, err)
72}
73
74func TestIntegration_ServerRejectsOversizedChunk(t *testing.T) {
75 t.Parallel()
76
77 ctx := context.Background()
78 img, err := random.Image(4096, 1)
79 require.NoError(t, err)
80
81 harness := newRegistryHarness(t, 512)
82 defer harness.Close()
83
84 err = harness.Push(ctx, img, registrypush.WithChunkSize(4096))
85 require.Error(t, err)
86 assert.Contains(t, err.Error(), "413")
87}
88
89func TestIntegration_ManifestRoundtrip(t *testing.T) {
90 t.Parallel()
91
92 ctx := context.Background()
93 img, err := random.Image(1024, 2)
94 require.NoError(t, err)
95
96 harness := newRegistryHarness(t, registryhttp.DefaultMaxBlobChunkSize)
97 defer harness.Close()
98
99 err = harness.Push(ctx, img, registrypush.WithChunkSize(512))
100 require.NoError(t, err)
101
102 expectedManifest, err := img.RawManifest()
103 require.NoError(t, err)
104
105 expectedMediaType, err := img.MediaType()
106 require.NoError(t, err)
107
108 resp := harness.GetManifest(t)
109 body, readErr := io.ReadAll(resp.Body)
110 require.NoError(t, readErr)
111 require.NoError(t, resp.Body.Close())
112
113 assert.Equal(t, http.StatusOK, resp.StatusCode)
114 assert.Equal(t, expectedManifest, body)
115 assert.Equal(t, string(expectedMediaType), resp.Header.Get("Content-Type"))
116}
117
118type registryHarness struct {
119 server *httptest.Server
120 ref string
121}
122
123func newRegistryHarness(t *testing.T, maxBlobChunkSize int64) *registryHarness {
124 t.Helper()
125
126 log := zerowrap.Default()
127 rootDir := t.TempDir()
128
129 blobStore, err := filesystem.NewBlobStorage(rootDir, log)
130 require.NoError(t, err)
131
132 manifestStore, err := filesystem.NewManifestStorage(rootDir, log)
133 require.NoError(t, err)
134
135 svc := registrysvc.NewService(blobStore, manifestStore, noopEventPublisher{})
136 handler := registryhttp.NewHandler(svc, log, maxBlobChunkSize)
137 server := httptest.NewServer(handler)
138
139 host := strings.TrimPrefix(server.URL, "http://")
140
141 return ®istryHarness{
142 server: server,
143 ref: host + "/testapp:v1.0.0",
144 }
145}
146
147func (h *registryHarness) Close() {
148 h.server.Close()
149}
150
151func (h *registryHarness) Push(ctx context.Context, img v1.Image, opts ...registrypush.Option) error {
152 allOpts := append([]registrypush.Option{
153 registrypush.WithTransport(h.server.Client().Transport),
154 registrypush.WithImageSource(func(context.Context, string) (v1.Image, error) {
155 return img, nil
156 }),
157 }, opts...)
158
159 pusher := registrypush.New(allOpts...)
160 return pusher.Push(ctx, h.ref)
161}
162
163func (h *registryHarness) GetManifest(t *testing.T) *http.Response {
164 t.Helper()
165
166 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/testapp/manifests/v1.0.0", h.server.URL), nil)
167 require.NoError(t, err)
168
169 resp, err := h.server.Client().Do(req)
170 require.NoError(t, err)
171
172 return resp
173}
174
175func (h *registryHarness) RequireManifestAndBlobs(ctx context.Context, t *testing.T, img v1.Image) {
176 t.Helper()
177
178 manifestResp := h.GetManifest(t)
179 require.Equal(t, http.StatusOK, manifestResp.StatusCode)
180 require.NoError(t, manifestResp.Body.Close())
181
182 layers, err := img.Layers()
183 require.NoError(t, err)
184
185 for _, layer := range layers {
186 digest, digestErr := layer.Digest()
187 require.NoError(t, digestErr)
188
189 req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/v2/testapp/blobs/%s", h.server.URL, digest.String()), nil)
190 require.NoError(t, reqErr)
191
192 resp, respErr := h.server.Client().Do(req)
193 require.NoError(t, respErr)
194 assert.Equal(t, http.StatusOK, resp.StatusCode)
195 require.NoError(t, resp.Body.Close())
196 }
197}
198
199type noopEventPublisher struct{}
200
201func (noopEventPublisher) Publish(domain.EventType, any) error {
202 return nil
203}