diff --git a/internal/keycloak/groups_test.go b/internal/keycloak/groups_test.go index 48ce1ed..0d59010 100644 --- a/internal/keycloak/groups_test.go +++ b/internal/keycloak/groups_test.go @@ -1,18 +1,64 @@ package keycloak_test import ( - "encoding/json" + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" "os" - "reflect" "testing" + "github.com/alecthomas/assert/v2" "github.com/uselagoon/lagoon-opensearch-sync/internal/keycloak" ) -func TestGroupsUnmarshal(t *testing.T) { +// newTestGroupsServer sets up a mock keycloak which responds with +// appropriate group JSON data to exercise Groups. +func newTestGroupsServer(tt *testing.T, testDataPath string) *httptest.Server { + // load the discovery JSON first, because the mux closure needs to + // reference its buffer + discoveryBuf, err := os.ReadFile("testdata/realm.oidc.discovery.json") + if err != nil { + tt.Fatal(err) + return nil + } + // configure router with the URLs that OIDC discovery and JWKS require + mux := http.NewServeMux() + mux.HandleFunc("/auth/realms/lagoon/.well-known/openid-configuration", + func(w http.ResponseWriter, r *http.Request) { + d := bytes.NewBuffer(discoveryBuf) + _, err = io.Copy(w, d) + if err != nil { + tt.Fatal(err) + } + }) + // configure the "all groups" path + mux.HandleFunc("/auth/admin/realms/lagoon/groups", + func(w http.ResponseWriter, r *http.Request) { + f, err := os.Open(testDataPath) + if err != nil { + tt.Fatal(err) + return + } + _, err = io.Copy(w, f) + if err != nil { + tt.Fatal(err) + } + }) + ts := httptest.NewServer(mux) + // now replace the example URL in the discovery JSON with the actual + // httptest server URL + discoveryBuf = bytes.ReplaceAll(discoveryBuf, + []byte("https://keycloak.example.com"), []byte(ts.URL)) + return ts +} + +func TestGroups(t *testing.T) { var testCases = map[string]struct { - input string - expect []keycloak.Group + input string + expect []keycloak.Group + expectError bool }{ "unmarshal groups": { input: "testdata/groups.json", @@ -110,20 +156,36 @@ func TestGroupsUnmarshal(t *testing.T) { }, }, }, + // https://github.com/uselagoon/lagoon-opensearch-sync/issues/150 + "empty groups error response": { + input: "testdata/groups.empty.json", + expectError: true, + }, } for name, tc := range testCases { t.Run(name, func(tt *testing.T) { - jb, err := os.ReadFile(tc.input) + ts := newTestGroupsServer(tt, tc.input) + defer ts.Close() + ctx := context.Background() + k, err := keycloak.NewClientCredentialsClient( + ctx, + ts.URL, + "test-client-id", + "test-client-secret", + ) if err != nil { tt.Fatal(err) } - var groups []keycloak.Group - if err = json.Unmarshal(jb, &groups); err != nil { - tt.Fatal(err) - } - if !reflect.DeepEqual(tc.expect, groups) { - tt.Fatalf("expected %v, got %v", tc.expect, groups) + // override internal client credentials HTTP client for testing + k.UseDefaultHTTPClient() + // execute test + groups, err := k.Groups(ctx) + if tc.expectError { + assert.Error(tt, err, name) + } else { + assert.NoError(tt, err, name) } + assert.Equal(tt, tc.expect, groups, name) }) } } diff --git a/internal/keycloak/helper_test.go b/internal/keycloak/helper_test.go new file mode 100644 index 0000000..00515b8 --- /dev/null +++ b/internal/keycloak/helper_test.go @@ -0,0 +1,9 @@ +package keycloak + +import "net/http" + +// UseDefaultHTTPClient uses the default http client to avoid token refresh in +// tests. +func (c *Client) UseDefaultHTTPClient() { + c.httpClient = http.DefaultClient +} diff --git a/internal/keycloak/testdata/groups.empty.json b/internal/keycloak/testdata/groups.empty.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/internal/keycloak/testdata/groups.empty.json @@ -0,0 +1 @@ +[] diff --git a/internal/keycloak/testdata/realm.oidc.discovery.json b/internal/keycloak/testdata/realm.oidc.discovery.json new file mode 100644 index 0000000..0dc2307 --- /dev/null +++ b/internal/keycloak/testdata/realm.oidc.discovery.json @@ -0,0 +1,272 @@ +{ + "authorization_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "authorization_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "authorization_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/auth", + "authorization_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "backchannel_authentication_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/ext/ciba/auth", + "backchannel_authentication_request_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "ES256", + "RS256", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "backchannel_logout_session_supported": true, + "backchannel_logout_supported": true, + "backchannel_token_delivery_modes_supported": [ + "poll", + "ping" + ], + "check_session_iframe": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/login-status-iframe.html", + "claim_types_supported": [ + "normal" + ], + "claims_parameter_supported": true, + "claims_supported": [ + "aud", + "sub", + "iss", + "auth_time", + "name", + "given_name", + "family_name", + "preferred_username", + "email", + "acr" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ], + "device_authorization_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/auth/device", + "end_session_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/logout", + "frontchannel_logout_session_supported": true, + "frontchannel_logout_supported": true, + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "password", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:openid:params:grant-type:ciba" + ], + "id_token_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "id_token_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "id_token_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "introspection_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/token/introspect", + "introspection_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "issuer": "https://keycloak.example.com/auth/realms/lagoon", + "jwks_uri": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/certs", + "mtls_endpoint_aliases": { + "backchannel_authentication_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/ext/ciba/auth", + "device_authorization_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/auth/device", + "introspection_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/token/introspect", + "pushed_authorization_request_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/ext/par/request", + "registration_endpoint": "https://keycloak.example.com/auth/realms/lagoon/clients-registrations/openid-connect", + "revocation_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/revoke", + "token_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/token", + "userinfo_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/userinfo" + }, + "pushed_authorization_request_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/ext/par/request", + "registration_endpoint": "https://keycloak.example.com/auth/realms/lagoon/clients-registrations/openid-connect", + "request_object_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "request_object_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "request_object_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "request_parameter_supported": true, + "request_uri_parameter_supported": true, + "require_pushed_authorization_requests": false, + "require_request_uri_registration": true, + "response_modes_supported": [ + "query", + "fragment", + "form_post", + "query.jwt", + "fragment.jwt", + "form_post.jwt", + "jwt" + ], + "response_types_supported": [ + "code", + "none", + "id_token", + "token", + "id_token token", + "code id_token", + "code token", + "code id_token token" + ], + "revocation_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/revoke", + "revocation_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "revocation_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "scopes_supported": [ + "openid", + "email", + "roles", + "address", + "offline_access", + "web-origins", + "profile", + "microprofile-jwt", + "phone" + ], + "subject_types_supported": [ + "public", + "pairwise" + ], + "tls_client_certificate_bound_access_tokens": true, + "token_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/token", + "token_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "userinfo_endpoint": "https://keycloak.example.com/auth/realms/lagoon/protocol/openid-connect/userinfo", + "userinfo_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ] +}