1package cli
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "io"
8 "net/http"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14
15 "github.com/bnema/gordon/internal/adapters/dto"
16 climocks "github.com/bnema/gordon/internal/adapters/in/cli/mocks"
17 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
18)
19
20// remoteWithRegistry returns a remote handle whose /auth/verify reports registryDomain.
21func remoteWithRegistry(t *testing.T, registryDomain string) *controlPlaneHandle {
22 t.Helper()
23 plane := climocks.NewMockControlPlane(t)
24 plane.EXPECT().VerifyAuth(mock.Anything).Return(&dto.AuthVerifyResponse{Valid: true, RegistryDomain: registryDomain}, nil).Once()
25 return &controlPlaneHandle{plane: plane, isRemote: true}
26}
27
28func TestResolveImageTarget_BareNameUsesTargetRegistry(t *testing.T) {
29 tests := []struct {
30 name string
31 arg string
32 wantName string
33 wantSource string
34 }{
35 {"bare name", "myapp", "myapp", "myapp"},
36 {"bare name with tag", "myapp:v1.2.3", "myapp", "myapp:v1.2.3"},
37 {"namespaced name", "team/myapp:v1", "team/myapp", "team/myapp:v1"},
38 {"bare digest", "myapp@sha256:deadbeef", "myapp", "myapp@sha256:deadbeef"},
39 }
40 for _, tt := range tests {
41 t.Run(tt.name, func(t *testing.T) {
42 var out bytes.Buffer
43 registry, name, source, err := resolveImageTarget(context.Background(), &out, remoteWithRegistry(t, "reg.example.com"), tt.arg, "Dockerfile")
44 require.NoError(t, err)
45 assert.Equal(t, "reg.example.com", registry)
46 assert.Equal(t, tt.wantName, name)
47 assert.Equal(t, tt.wantSource, source)
48 assert.Contains(t, out.String(), "reg.example.com")
49 })
50 }
51}
52
53func TestResolveImageTarget_QualifiedRefMatchingTarget(t *testing.T) {
54 registry, name, source, err := resolveImageTarget(context.Background(), io.Discard, remoteWithRegistry(t, "reg.example.com."), "Reg.Example.com/myapp:v1", "Dockerfile")
55 require.NoError(t, err)
56 assert.Equal(t, "Reg.Example.com", registry)
57 assert.Equal(t, "myapp", name)
58 assert.Equal(t, "Reg.Example.com/myapp:v1", source)
59}
60
61func TestResolveImageTarget_RejectsMismatchedRegistry(t *testing.T) {
62 for _, ref := range []string{"other.example.org/myapp:v1", "reg.example.com:443/myapp:v1"} {
63 t.Run(ref, func(t *testing.T) {
64 _, _, _, err := resolveImageTarget(context.Background(), io.Discard, remoteWithRegistry(t, "reg.example.com"), ref, "Dockerfile")
65 require.Error(t, err)
66 assert.Contains(t, err.Error(), "reg.example.com/myapp")
67 })
68 }
69}
70
71func TestTargetRegistryDomain_RemoteFallsBackToStatusForOlderServers(t *testing.T) {
72 plane := climocks.NewMockControlPlane(t)
73 plane.EXPECT().VerifyAuth(mock.Anything).Return(&dto.AuthVerifyResponse{Valid: true}, nil).Once()
74 plane.EXPECT().GetStatus(mock.Anything).Return(&remote.Status{RegistryDomain: "reg.example.com"}, nil).Once()
75
76 got, err := targetRegistryDomain(context.Background(), &controlPlaneHandle{plane: plane, isRemote: true})
77 require.NoError(t, err)
78 assert.Equal(t, "reg.example.com", got)
79}
80
81func TestTargetRegistryDomain_LocalSocketUsesStatus(t *testing.T) {
82 plane := climocks.NewMockControlPlane(t)
83 plane.EXPECT().GetStatus(mock.Anything).Return(&remote.Status{RegistryDomain: "reg.example.com"}, nil).Once()
84
85 got, err := targetRegistryDomain(context.Background(), &controlPlaneHandle{plane: plane})
86 require.NoError(t, err)
87 assert.Equal(t, "reg.example.com", got)
88}
89
90func TestTargetRegistryDomain_Errors(t *testing.T) {
91 tests := []struct {
92 name string
93 setup func(*climocks.MockControlPlane)
94 wantErr string
95 }{
96 {
97 name: "verify fails",
98 setup: func(p *climocks.MockControlPlane) {
99 p.EXPECT().VerifyAuth(mock.Anything).Return(nil, errors.New("boom")).Once()
100 },
101 wantErr: "boom",
102 },
103 {
104 name: "token without admin scope",
105 setup: func(p *climocks.MockControlPlane) {
106 p.EXPECT().VerifyAuth(mock.Anything).Return(nil, &remote.HTTPError{StatusCode: http.StatusForbidden, Status: "403 Forbidden"}).Once()
107 },
108 wantErr: "admin scope",
109 },
110 {
111 name: "token invalid",
112 setup: func(p *climocks.MockControlPlane) {
113 p.EXPECT().VerifyAuth(mock.Anything).Return(&dto.AuthVerifyResponse{Valid: false}, nil).Once()
114 },
115 wantErr: "token is not valid",
116 },
117 {
118 name: "older server denies status",
119 setup: func(p *climocks.MockControlPlane) {
120 p.EXPECT().VerifyAuth(mock.Anything).Return(&dto.AuthVerifyResponse{Valid: true}, nil).Once()
121 p.EXPECT().GetStatus(mock.Anything).Return(nil, &remote.HTTPError{StatusCode: http.StatusForbidden, Status: "403 Forbidden"}).Once()
122 },
123 wantErr: "admin:status:read",
124 },
125 {
126 name: "no registry domain configured",
127 setup: func(p *climocks.MockControlPlane) {
128 p.EXPECT().VerifyAuth(mock.Anything).Return(&dto.AuthVerifyResponse{Valid: true}, nil).Once()
129 p.EXPECT().GetStatus(mock.Anything).Return(&remote.Status{}, nil).Once()
130 },
131 wantErr: "registry_domain",
132 },
133 }
134 for _, tt := range tests {
135 t.Run(tt.name, func(t *testing.T) {
136 plane := climocks.NewMockControlPlane(t)
137 tt.setup(plane)
138 _, err := targetRegistryDomain(context.Background(), &controlPlaneHandle{plane: plane, isRemote: true})
139 require.ErrorContains(t, err, tt.wantErr)
140 })
141 }
142}