@@ -16,13 +16,18 @@ package registry
1616
1717import (
1818 "context"
19+ stderrors "errors"
20+ "net"
21+ neturl "net/url"
1922 "testing"
2023
2124 "github.com/stretchr/testify/suite"
2225
2326 "github.com/goharbor/harbor/src/common"
2427 "github.com/goharbor/harbor/src/lib/config"
28+ "github.com/goharbor/harbor/src/lib/errors"
2529 _ "github.com/goharbor/harbor/src/pkg/config/inmemory"
30+ "github.com/goharbor/harbor/src/pkg/reg"
2631 "github.com/goharbor/harbor/src/pkg/reg/model"
2732 "github.com/goharbor/harbor/src/testing/mock"
2833 testingproject "github.com/goharbor/harbor/src/testing/pkg/project"
@@ -68,15 +73,18 @@ func (r *registryTestSuite) TestValidate() {
6873 err = r .ctl .validate (nil , registry )
6974 r .NotNil (err )
7075
71- // URL with FTP scheme
76+ // URL with FTP scheme: rejected before an adapter is ever built, because
77+ // net/http cannot dial it and the transport error used to surface as a 500
7278 registry = & model.Registry {
7379 Name : "endpoint01" ,
7480 URL : "ftp://example.com" ,
7581 }
76- mock .OnAnything (r .regMgr , "CreateAdapter" ).Return (r .adapter , nil )
77- mock .OnAnything (r .adapter , "HealthCheck" ).Return (model .Healthy , nil )
7882 err = r .ctl .validate (nil , registry )
79- r .Nil (err )
83+ r .NotNil (err )
84+ r .True (errors .IsErr (err , errors .BadRequestCode ), "want a bad request error, got %v" , err )
85+ r .regMgr .AssertNotCalled (r .T (), "CreateAdapter" , mock .Anything , mock .Anything )
86+
87+ r .SetupTest ()
8088
8189 // URL without scheme
8290 registry = & model.Registry {
@@ -151,6 +159,84 @@ func (r *registryTestSuite) TestValidate() {
151159 r .adapter .AssertExpectations (r .T ())
152160}
153161
162+ // An endpoint that cannot be reached is a bad request, not a 500. This drives
163+ // the real registry manager and the real Harbor adapter factory rather than
164+ // mocks: the probe that fails is the GET /api/version issued while the adapter
165+ // is being built, so it never reaches the HealthCheck call a mocked manager
166+ // would exercise.
167+ func (r * registryTestSuite ) TestValidateUnreachableEndpoint () {
168+ config .InitWithSettings (map [string ]any {
169+ common .CoreURL : "http://core:8080" ,
170+ })
171+
172+ // Hold a loopback listener open and hang up on every connection. Keeping
173+ // the port bound is what makes this deterministic: releasing it first
174+ // would let another process take it between the bind and the request.
175+ listener , err := net .Listen ("tcp" , "127.0.0.1:0" )
176+ r .Require ().NoError (err )
177+ defer listener .Close ()
178+ address := listener .Addr ().String ()
179+ go func () {
180+ for {
181+ conn , err := listener .Accept ()
182+ if err != nil {
183+ return
184+ }
185+ conn .Close ()
186+ }
187+ }()
188+
189+ ctl := & controller {
190+ regMgr : reg .Mgr ,
191+ repMgr : r .repMgr ,
192+ proMgr : r .proMgr ,
193+ }
194+
195+ err = ctl .validate (context .Background (), & model.Registry {
196+ Name : "endpoint01" ,
197+ Type : model .RegistryTypeHarbor ,
198+ URL : "http://" + address ,
199+ })
200+
201+ r .Require ().NotNil (err )
202+ r .True (errors .IsErr (err , errors .BadRequestCode ), "want a bad request error, got %v" , err )
203+ r .Contains (err .Error (), "failed to reach the registry endpoint" )
204+ }
205+
206+ // The other branch: an adapter that builds but whose health check cannot reach
207+ // the endpoint. Only reachable with a mocked adapter, since the adapters that
208+ // probe lazily are the ones that need credentials.
209+ func (r * registryTestSuite ) TestValidateHealthCheckTransportError () {
210+ transportErr := & neturl.Error {
211+ Op : "Get" ,
212+ URL : "http://example.com/v2/" ,
213+ Err : stderrors .New ("dial tcp: connection refused" ),
214+ }
215+ mock .OnAnything (r .regMgr , "CreateAdapter" ).Return (r .adapter , nil )
216+ mock .OnAnything (r .adapter , "HealthCheck" ).Return ("" , transportErr )
217+
218+ err := r .ctl .validate (context .Background (), & model.Registry {
219+ Name : "endpoint01" ,
220+ URL : "http://example.com" ,
221+ })
222+
223+ r .Require ().NotNil (err )
224+ r .True (errors .IsErr (err , errors .BadRequestCode ), "want a bad request error, got %v" , err )
225+ r .Contains (err .Error (), "failed to reach the registry endpoint" )
226+ r .regMgr .AssertExpectations (r .T ())
227+ r .adapter .AssertExpectations (r .T ())
228+ }
229+
230+ // Errors that are not transport failures keep their own code, so a genuine
231+ // internal fault is not reported to the caller as a bad request.
232+ func (r * registryTestSuite ) TestUnreachableEndpointErrorPassesOtherErrorsThrough () {
233+ internal := stderrors .New ("boom" )
234+ r .Equal (internal , unreachableEndpointError ("http://example.com" , internal ))
235+
236+ notFound := errors .New (nil ).WithCode (errors .NotFoundCode ).WithMessage ("gone" )
237+ r .Equal (notFound , unreachableEndpointError ("http://example.com" , notFound ))
238+ }
239+
154240func (r * registryTestSuite ) TestDelete () {
155241 // referenced by replication policy
156242 mock .OnAnything (r .repMgr , "Count" ).Return (int64 (1 ), nil )
0 commit comments