diff --git a/cmd/login.go b/cmd/login.go index 2180504e..fa31524f 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -120,7 +120,11 @@ microcks login http://localhost:8080 --sso --sso-launch-browser=false os.Exit(1) } //Perform login and retrive tokens - authToken, refreshToken = passwordLogin(keycloakUrl, clientID, clientSecret, username, password) + var loginErr error + authToken, refreshToken, loginErr = passwordLogin(keycloakUrl, clientID, clientSecret, username, password) + if loginErr != nil { + log.Fatal(loginErr) + } authCfg.ClientId = clientID authCfg.ClientSecret = clientSecret } else { @@ -308,17 +312,16 @@ func ssoAuthFlow(url string, ssoLaunchBrowser bool) { } } -func passwordLogin(keycloakURL, clientId, clientSecret, Username, Password string) (string, string) { +func passwordLogin(keycloakURL, clientId, clientSecret, Username, Password string) (string, string, error) { kc := connectors.NewKeycloakClient(keycloakURL, clientId, clientSecret) username, password := promptCredentials(Username, Password) authToken, refreshToken, err := kc.ConnectAndGetTokenAndRefreshToken(username, password) - if err != nil { - panic(err) + return "", "", err } - return authToken, refreshToken + return authToken, refreshToken, nil } func promptCredentials(username, password string) (string, string) { diff --git a/pkg/connectors/keycloak_client_test.go b/pkg/connectors/keycloak_client_test.go new file mode 100644 index 00000000..c263633d --- /dev/null +++ b/pkg/connectors/keycloak_client_test.go @@ -0,0 +1,88 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package connectors + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestKeycloakClientNegativeScenarios(t *testing.T) { + tests := []struct { + name string + statusCode int + responseBody string + checkErrSubstr string + }{ + { + name: "Non-200 status code", + statusCode: http.StatusInternalServerError, + responseBody: "Internal Server Error Details", + checkErrSubstr: "returned HTTP 500", + }, + { + name: "Non-JSON response", + statusCode: http.StatusOK, + responseBody: "not-a-json-string", + checkErrSubstr: "parsing Keycloak", + }, + { + name: "JSON response with missing required fields", + statusCode: http.StatusOK, + responseBody: `{"some_key": "some_value"}`, + checkErrSubstr: "missing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + _, _ = w.Write([]byte(tt.responseBody)) + })) + defer server.Close() + + // Add trailing slash to server.URL so ResolveReference resolves paths properly + client := NewKeycloakClient(server.URL+"/", "client-id", "client-secret") + + // Test ConnectAndGetToken + _, err := client.ConnectAndGetToken() + if err == nil { + t.Error("expected ConnectAndGetToken to return error, got nil") + } else if !strings.Contains(err.Error(), tt.checkErrSubstr) { + t.Errorf("ConnectAndGetToken: expected error containing %q, got: %v", tt.checkErrSubstr, err) + } + + // Test ConnectAndGetTokenAndRefreshToken + _, _, err = client.ConnectAndGetTokenAndRefreshToken("user", "pass") + if err == nil { + t.Error("expected ConnectAndGetTokenAndRefreshToken to return error, got nil") + } else if !strings.Contains(err.Error(), tt.checkErrSubstr) { + t.Errorf("ConnectAndGetTokenAndRefreshToken: expected error containing %q, got: %v", tt.checkErrSubstr, err) + } + + // Test GetOIDCConfig + _, err = client.GetOIDCConfig() + if err == nil { + t.Error("expected GetOIDCConfig to return error, got nil") + } else if !strings.Contains(err.Error(), tt.checkErrSubstr) { + t.Errorf("GetOIDCConfig: expected error containing %q, got: %v", tt.checkErrSubstr, err) + } + }) + } +} diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index e4fb2c66..8b13a3a5 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -253,14 +253,22 @@ func (c *microcksClient) GetKeycloakURL() (string, error) { return "", errors.Wrap(errors.KindAPI, fmt.Errorf("parsing Keycloak config response: %w", err)) } - // Return 'null' if Keycloak is disabled. - if enabled, _ := configResp["enabled"].(bool); !enabled { + // Retrieve auth server url and realm name. + enabledVal, exists := configResp["enabled"] + if !exists { + return "", errors.Wrapf(errors.KindAPI, "invalid keycloak config response (missing 'enabled' field)") + } + enabled, ok := enabledVal.(bool) + if !ok { + return "", errors.Wrapf(errors.KindAPI, "invalid keycloak config response ('enabled' field is not a boolean)") + } + if !enabled { return "null", nil } - authServerURL, _ := configResp["auth-server-url"].(string) - realmName, _ := configResp["realm"].(string) - if authServerURL == "" || realmName == "" { + authServerURL, ok1 := configResp["auth-server-url"].(string) + realmName, ok2 := configResp["realm"].(string) + if !ok1 || !ok2 || authServerURL == "" || realmName == "" { return "", errors.Wrapf(errors.KindAPI, "Keycloak config response missing auth-server-url or realm") } return authServerURL + "/realms/" + realmName + "/", nil @@ -443,6 +451,10 @@ func (c *microcksClient) GetTestResult(testResultID string) (*TestResultSummary, return nil, errors.Wrap(errors.KindConnection, fmt.Errorf("reading test result response: %w", err)) } + if resp.StatusCode != http.StatusOK { + return nil, errors.Wrapf(errors.KindAPI, "Microcks returned HTTP %d for test result: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + result := TestResultSummary{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to parse test result response: %w", err) diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index 2fe423e7..f71d1973 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -102,3 +102,115 @@ func TestDownloadArtifactReturnsResponseBody(t *testing.T) { t.Fatalf("expected response body %q, got %q", expectedBody, msg) } } + +func TestUnexpectedServerResponseHandling(t *testing.T) { + // Create a test server that returns HTML / Bad Gateway + const htmlError = `502 Bad Gateway` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write([]byte(htmlError)) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + + // Test GetKeycloakURL + _, err := client.GetKeycloakURL() + if err == nil { + t.Error("expected GetKeycloakURL to return error on Bad Gateway response, got nil") + } else if !strings.Contains(err.Error(), "502") { + t.Errorf("expected error message to contain HTTP status code '502', got: %v", err) + } + + // Test GetTestResult + _, err = client.GetTestResult("some-id") + if err == nil { + t.Error("expected GetTestResult to return error on Bad Gateway response, got nil") + } else if !strings.Contains(err.Error(), "502") { + t.Errorf("expected error message to contain HTTP status code '502', got: %v", err) + } + + // Test DownloadArtifact + _, err = client.DownloadArtifact("https://example.com/openapi.yaml", true, "") + if err == nil { + t.Error("expected DownloadArtifact to return error on Bad Gateway response, got nil") + } +} + +func TestGetKeycloakURLConfigValidation(t *testing.T) { + tests := []struct { + name string + responseBody string + expectedURL string + expectError bool + wantErrSubstr string + }{ + { + name: "Valid Keycloak enabled config", + responseBody: `{"enabled": true, "auth-server-url": "http://localhost:8080/auth", "realm": "microcks"}`, + expectedURL: "http://localhost:8080/auth/realms/microcks/", + expectError: false, + }, + { + name: "Valid Keycloak disabled config", + responseBody: `{"enabled": false}`, + expectedURL: "null", + expectError: false, + }, + { + name: "Missing enabled field", + responseBody: `{"auth-server-url": "http://localhost:8080/auth", "realm": "microcks"}`, + expectError: true, + wantErrSubstr: "missing 'enabled' field", + }, + { + name: "Invalid type for enabled field", + responseBody: `{"enabled": "true", "auth-server-url": "http://localhost:8080/auth", "realm": "microcks"}`, + expectError: true, + wantErrSubstr: "'enabled' field is not a boolean", + }, + { + name: "Enabled but missing auth-server-url", + responseBody: `{"enabled": true, "realm": "microcks"}`, + expectError: true, + wantErrSubstr: "missing auth-server-url or realm", + }, + { + name: "Enabled but missing realm", + responseBody: `{"enabled": true, "auth-server-url": "http://localhost:8080/auth"}`, + expectError: true, + wantErrSubstr: "missing auth-server-url or realm", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/keycloak/config" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(tt.responseBody)) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + url, err := client.GetKeycloakURL() + + if tt.expectError { + if err == nil { + t.Error("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.wantErrSubstr) { + t.Errorf("expected error containing %q, got: %v", tt.wantErrSubstr, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if url != tt.expectedURL { + t.Errorf("expected URL %q, got %q", tt.expectedURL, url) + } + } + }) + } +}