detached 485dfa5

test: add integration tests for native push against real Gordon registry stack

commit
485dfa58c0d6948a75802e91bd109fd1afb4033b
author
brice · · 2026-03-14 14:06 UTC
parent
515ee12
signature
unsigned
1 file · +203 -0
A pkg/registrypush/integration_test.go +203 -0
@@ -0,0 +1,203 @@
1+package registrypush_test
2+
3+import (
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+
25+func 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+
41+func 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+
57+func 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+
74+func 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+
89+func 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+
118+type registryHarness struct {
119+ server *httptest.Server
120+ ref string
121+}
122+
123+func 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 &registryHarness{
142+ server: server,
143+ ref: host + "/testapp:v1.0.0",
144+ }
145+}
146+
147+func (h *registryHarness) Close() {
148+ h.server.Close()
149+}
150+
151+func (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+
163+func (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+
175+func (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+
199+type noopEventPublisher struct{}
200+
201+func (noopEventPublisher) Publish(domain.EventType, any) error {
202+ return nil
203+}
@@ -0,0 +1,203 @@
1+package registrypush_test
2+
3+import (
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+
25+func 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+
41+func 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+
57+func 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+
74+func 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+
89+func 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+
118+type registryHarness struct {
119+ server *httptest.Server
120+ ref string
121+}
122+
123+func 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 &registryHarness{
142+ server: server,
143+ ref: host + "/testapp:v1.0.0",
144+ }
145+}
146+
147+func (h *registryHarness) Close() {
148+ h.server.Close()
149+}
150+
151+func (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+
163+func (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+
175+func (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+
199+type noopEventPublisher struct{}
200+
201+func (noopEventPublisher) Publish(domain.EventType, any) error {
202+ return nil
203+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close