1package imageref
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/gordon/internal/domain"
12)
13
14type stubManifests struct {
15 raw map[string][]byte
16 err error
17}
18
19func (f *stubManifests) GetManifest(name, reference string) ([]byte, string, error) {
20 if f.err != nil {
21 return nil, "", f.err
22 }
23 raw, ok := f.raw[name+":"+reference]
24 if !ok {
25 return nil, "", errors.New("missing")
26 }
27 return raw, "application/vnd.oci.image.manifest.v1+json", nil
28}
29
30func TestResolver_PinnedPassesThrough(t *testing.T) {
31 r := NewResolver("reg.example.com", &stubManifests{}, nil)
32 valid := "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
33 got, err := r.ResolveDigest(context.Background(), "img@"+valid)
34 require.NoError(t, err)
35 assert.Equal(t, valid, got)
36
37 _, err = r.ResolveDigest(context.Background(), "img@sha256:short")
38 require.ErrorIs(t, err, domain.ErrAppImageUnresolvable)
39}
40
41func TestResolver_LocalManifestHashes(t *testing.T) {
42 r := NewResolver("reg.example.com", &stubManifests{raw: map[string][]byte{"blog/web:1.0": []byte("{}")}}, nil)
43 got, err := r.ResolveDigest(context.Background(), "reg.example.com/blog/web:1.0")
44 require.NoError(t, err)
45 assert.True(t, len(got) == 7+64)
46
47 _, err = r.ResolveDigest(context.Background(), "reg.example.com/blog/missing:1.0")
48 require.ErrorIs(t, err, domain.ErrAppImageUnresolvable)
49}
50
51func TestResolver_ExternalNeedsRemote(t *testing.T) {
52 r := NewResolver("reg.example.com", &stubManifests{}, nil)
53 _, err := r.ResolveDigest(context.Background(), "docker.io/library/nginx:1.0")
54 require.ErrorIs(t, err, domain.ErrAppImageUnresolvable)
55}
56
57func TestResolver_RejectsMalformedRemoteDigest(t *testing.T) {
58 r := NewResolver("reg.example.com", &stubManifests{}, &recordingRemote{digest: "sha256:not-a-digest"})
59 _, err := r.ResolveDigest(context.Background(), "docker.io/library/nginx:1.0")
60 require.ErrorIs(t, err, domain.ErrAppImageUnresolvable)
61}
62
63// recordingRemote records whether the connector was consulted.
64type recordingRemote struct {
65 called bool
66 digest string
67}
68
69func (r *recordingRemote) ResolveDigest(context.Context, string) (string, error) {
70 r.called = true
71 if r.digest != "" {
72 return r.digest, nil
73 }
74 return "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil
75}
76
77// TestResolver_RejectsDisallowedRefsWithoutConnecting proves policy is
78// enforced before the remote connector is consulted, including for
79// already-pinned digest references.
80func TestResolver_RejectsDisallowedRefsWithoutConnecting(t *testing.T) {
81 const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
82 remote := &recordingRemote{}
83 r := NewResolver("reg.example.com", &stubManifests{}, remote).WithPolicy(domain.ImageSourcePolicy{
84 InstallationRegistry: "reg.example.com",
85 AllowedRegistries: []string{"registry.example.com"},
86 })
87
88 _, err := r.ResolveDigest(context.Background(), "127.0.0.1:12345/private@"+digest)
89 require.ErrorIs(t, err, domain.ErrAppImageNotAllowed)
90
91 _, err = r.ResolveDigest(context.Background(), "other.example.com/team/app@"+digest)
92 require.ErrorIs(t, err, domain.ErrAppImageNotAllowed)
93
94 _, err = r.ResolveDigest(context.Background(), "other.example.com/team/app:1.0")
95 require.ErrorIs(t, err, domain.ErrAppImageNotAllowed)
96
97 assert.False(t, remote.called, "policy rejection must happen before connecting")
98}
99
100func TestResolver_AllowsInstallationAndAllowlistedRefs(t *testing.T) {
101 const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
102 manifests := &stubManifests{raw: map[string][]byte{"blog/web:1.0": []byte("{}")}}
103 remote := &recordingRemote{}
104 r := NewResolver("reg.example.com", manifests, remote).WithPolicy(domain.ImageSourcePolicy{
105 InstallationRegistry: "reg.example.com",
106 AllowedRegistries: []string{"registry.example.com"},
107 })
108
109 got, err := r.ResolveDigest(context.Background(), "reg.example.com/blog/web:1.0")
110 require.NoError(t, err)
111 assert.True(t, len(got) == 7+64)
112
113 got, err = r.ResolveDigest(context.Background(), "registry.example.com/team/app@"+digest)
114 require.NoError(t, err)
115 assert.Equal(t, digest, got)
116 assert.False(t, remote.called, "digest refs resolve without contacting the connector")
117}
118
119// TestResolver_RequireDigestRejectsTags proves the require-digest policy is
120// enforced on the manifest reference before it is resolved.
121func TestResolver_RequireDigestRejectsTags(t *testing.T) {
122 const digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
123 r := NewResolver("reg.example.com", &stubManifests{}, &recordingRemote{}).WithPolicy(domain.ImageSourcePolicy{
124 AllowedRegistries: []string{"registry.example.com"},
125 RequireDigest: true,
126 InstallationRegistry: "reg.example.com",
127 })
128
129 _, err := r.ResolveDigest(context.Background(), "registry.example.com/team/app:1.0")
130 require.ErrorIs(t, err, domain.ErrAppImageNotAllowed)
131
132 got, err := r.ResolveDigest(context.Background(), "registry.example.com/team/app@"+digest)
133 require.NoError(t, err)
134 assert.Equal(t, digest, got)
135}