@@ -60,7 +60,10 @@ func TestNewClient(t *testing.T) {
6060
6161 for _ , tt := range tests {
6262 t .Run (tt .name , func (t * testing.T ) {
63- client , err := NewClient (tt .baseURL , tt .clientID , tt .clientSecret )
63+ // Use NewClientWithOptions with AllowInsecureHTTP for localhost test server
64+ client , err := NewClientWithOptions (tt .baseURL , tt .clientID , tt .clientSecret , & ClientOptions {
65+ AllowInsecureHTTP : true ,
66+ })
6467
6568 if tt .wantErr {
6669 assert .Error (t , err , "NewClient should return an error for invalid credentials" )
@@ -267,8 +270,118 @@ func TestNewClient_TokenResponseOverLimit(t *testing.T) {
267270 }))
268271 defer server .Close ()
269272
270- _ , err := NewClient (server .URL , "client-id" , "client-secret" )
273+ _ , err := NewClientWithOptions (server .URL , "client-id" , "client-secret" , & ClientOptions {
274+ AllowInsecureHTTP : true ,
275+ })
271276
272277 assert .Error (t , err , "NewClient should fail for oversized OAuth response" )
273278 assert .True (t , errors .Is (err , ErrResponseTooLarge ), "Error should be ErrResponseTooLarge, got: %v" , err )
274279}
280+
281+ // TestValidateBaseURL tests the validateBaseURL function that validates base URL parameters.
282+ func TestValidateBaseURL (t * testing.T ) {
283+ tests := []struct {
284+ name string
285+ url string
286+ allowHTTP bool
287+ wantErr error
288+ wantURL string
289+ }{
290+ // Valid HTTPS URLs
291+ {"valid https" , "https://api.example.com" , false , nil , "https://api.example.com" },
292+ {"https with port" , "https://api.example.com:443" , false , nil , "https://api.example.com:443" },
293+ {"https with path stripped" , "https://api.example.com/v1/" , false , nil , "https://api.example.com" },
294+ {"https trailing slash stripped" , "https://api.example.com/" , false , nil , "https://api.example.com" },
295+
296+ // Invalid: HTTP without allowHTTP
297+ {"http not allowed" , "http://api.example.com" , false , ErrHTTPSRequired , "" },
298+
299+ // Valid: HTTP with allowHTTP for loopback
300+ {"http localhost allowed" , "http://localhost:8080" , true , nil , "http://localhost:8080" },
301+ {"http 127.0.0.1 allowed" , "http://127.0.0.1:8080" , true , nil , "http://127.0.0.1:8080" },
302+ {"http 127.0.0.2 allowed" , "http://127.0.0.2:9999" , true , nil , "http://127.0.0.2:9999" },
303+ {"http ::1 allowed" , "http://[::1]:8080" , true , nil , "http://[::1]:8080" },
304+
305+ // Invalid: HTTP with allowHTTP for non-loopback
306+ {"http remote rejected even with allowHTTP" , "http://api.example.com" , true , ErrHTTPSRequired , "" },
307+
308+ // Invalid URLs
309+ {"empty url" , "" , false , ErrInvalidBaseURL , "" },
310+ {"missing scheme" , "api.example.com" , false , ErrInvalidBaseURL , "" },
311+ {"ftp scheme" , "ftp://api.example.com" , false , ErrInvalidBaseURL , "" },
312+ {"missing host" , "https://" , false , ErrInvalidBaseURL , "" },
313+
314+ // Security: credentials in URL rejected
315+ {"url with userinfo" , "https://user:pass@api.example.com" , false , ErrInvalidBaseURL , "" },
316+ }
317+
318+ for _ , tt := range tests {
319+ t .Run (tt .name , func (t * testing.T ) {
320+ result , err := validateBaseURL (tt .url , tt .allowHTTP )
321+ if tt .wantErr != nil {
322+ assert .Error (t , err )
323+ assert .True (t , errors .Is (err , tt .wantErr ), "expected error %v, got %v" , tt .wantErr , err )
324+ } else {
325+ assert .NoError (t , err )
326+ assert .Equal (t , tt .wantURL , result )
327+ }
328+ })
329+ }
330+ }
331+
332+ // TestIsLoopbackHost tests the isLoopbackHost function that checks for loopback addresses.
333+ func TestIsLoopbackHost (t * testing.T ) {
334+ tests := []struct {
335+ host string
336+ expected bool
337+ }{
338+ {"localhost" , true },
339+ {"localhost:8080" , true },
340+ {"LOCALHOST" , true },
341+ {"127.0.0.1" , true },
342+ {"127.0.0.1:8080" , true },
343+ {"127.0.0.2" , true },
344+ {"127.255.255.255" , true },
345+ {"[::1]" , true },
346+ {"[::1]:8080" , true },
347+ {"::1" , true },
348+ {"api.example.com" , false },
349+ {"192.168.1.1" , false },
350+ {"10.0.0.1" , false },
351+ }
352+
353+ for _ , tt := range tests {
354+ t .Run (tt .host , func (t * testing.T ) {
355+ result := isLoopbackHost (tt .host )
356+ assert .Equal (t , tt .expected , result )
357+ })
358+ }
359+ }
360+
361+ // TestNewClient_HTTPSRequired tests that NewClient rejects HTTP URLs by default.
362+ func TestNewClient_HTTPSRequired (t * testing.T ) {
363+ _ , err := NewClient ("http://api.example.com" , "id" , "secret" )
364+ assert .Error (t , err )
365+ assert .True (t , errors .Is (err , ErrHTTPSRequired ), "expected ErrHTTPSRequired, got: %v" , err )
366+ }
367+
368+ // TestNewClientWithOptions_AllowHTTPForLocalhost tests that NewClientWithOptions allows HTTP for localhost.
369+ func TestNewClientWithOptions_AllowHTTPForLocalhost (t * testing.T ) {
370+ server := setupMockServer ()
371+ defer server .Close ()
372+
373+ client , err := NewClientWithOptions (server .URL , "test-id" , "test-secret" , & ClientOptions {
374+ AllowInsecureHTTP : true ,
375+ })
376+ assert .NoError (t , err )
377+ assert .NotNil (t , client )
378+ }
379+
380+ // TestNewClientWithOptions_HTTPRemoteRejected tests that HTTP to non-localhost is always rejected.
381+ func TestNewClientWithOptions_HTTPRemoteRejected (t * testing.T ) {
382+ _ , err := NewClientWithOptions ("http://api.example.com" , "test-id" , "test-secret" , & ClientOptions {
383+ AllowInsecureHTTP : true ,
384+ })
385+ assert .Error (t , err )
386+ assert .True (t , errors .Is (err , ErrHTTPSRequired ), "expected ErrHTTPSRequired, got: %v" , err )
387+ }
0 commit comments