diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluate.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluate.java
new file mode 100644
index 0000000000..a72e410d94
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluate.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.console.page.clients.clientscopes;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.console.page.clients.Client;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientScopesEvaluate extends Client {
+
+ @Page
+ private ClientScopesEvaluateForm form;
+
+ @Override
+ public String getUriFragment() {
+ return super.getUriFragment() + "/client-scopes/evaluate-scopes";
+ }
+
+ public ClientScopesEvaluateForm form() {
+ return form;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluateForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluateForm.java
new file mode 100644
index 0000000000..2a426520eb
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesEvaluateForm.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.console.page.clients.clientscopes;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.keycloak.testsuite.console.page.fragment.DataTable;
+import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientScopesEvaluateForm extends Form {
+
+ @FindBy(id = "scopeParam")
+ private WebElement scopeParamInput;
+
+ @FindBy(id = "available")
+ protected Select availableClientScopesSelect;
+
+ @FindBy(id = "assigned")
+ protected Select assignedClientScopesSelect;
+
+ @FindBy(id = "effective")
+ protected Select effectiveClientScopesSelect;
+
+ @FindBy(css = "button[ng-click*='addAppliedClientScope']")
+ protected WebElement addAppliedClientScopesButton;
+
+ @FindBy(css = "button[ng-click*='deleteAppliedClientScope']")
+ protected WebElement deleteAppliedClientScopesButton;
+
+ @FindBy(css = "button[data-ng-click*='sendEvaluationRequest']")
+ protected WebElement evaluateButton;
+
+ // Bottom part of the page (stuff shown after "Evaluate" button clicked)
+ @FindBy(css = "li[data-ng-click*='showTab(1)']")
+ protected WebElement showProtocolMappersLink;
+
+ @FindBy(css = "li[data-ng-click*='showTab(2)']")
+ protected WebElement showRolesLink;
+
+ @FindBy(css = "li[data-ng-click*='showTab(3)']")
+ protected WebElement showTokenLink;
+
+ @FindBy(css = "table[data-ng-show*='protocolMappersShown']")
+ protected DataTable protocolMappersTable;
+
+ @FindBy(id = "available-realm-roles")
+ protected Select notGrantedRealmRolesSelect;
+
+ @FindBy(id = "realm-composite")
+ protected Select grantedRealmRolesSelect;
+
+ @FindBy(tagName = "textarea")
+ private WebElement accessTokenTextArea;
+
+
+ public Set getAvailableClientScopes() {
+ return ClientScopesSetupForm.getSelectValues(availableClientScopesSelect);
+ }
+
+ public Set getAssignedClientScopes() {
+ return ClientScopesSetupForm.getSelectValues(assignedClientScopesSelect);
+ }
+
+ public Set getEffectiveClientScopes() {
+ return ClientScopesSetupForm.getSelectValues(effectiveClientScopesSelect);
+ }
+
+ public void setAssignedClientScopes(Collection scopes) {
+ ClientScopesSetupForm.removeRedundantScopes(assignedClientScopesSelect, deleteAppliedClientScopesButton, scopes);
+ ClientScopesSetupForm.addMissingScopes(availableClientScopesSelect, addAppliedClientScopesButton, scopes);
+ }
+
+
+ public void selectUser(String username) {
+ // TODO: Should be probably better way how to work with the "ui-select2" component
+ driver.findElement(By.id("select2-chosen-1")).click();
+ driver.findElement(By.className("select2-input")).sendKeys(username);
+ driver.findElement(By.className("select2-result-label")).click();
+ }
+
+
+ public void evaluate() {
+ evaluateButton.click();
+ WaitUtils.waitForPageToLoad();
+ }
+
+
+ public void showProtocolMappers() {
+ showProtocolMappersLink.click();
+ WaitUtils.waitForPageToLoad();
+ }
+
+ public void showRoles() {
+ showRolesLink.click();
+ WaitUtils.waitForPageToLoad();
+ }
+
+ public void showToken() {
+ showTokenLink.click();
+ WaitUtils.waitForPageToLoad();
+ }
+
+
+ // Bottom part of the page (stuff shown after "Evaluate" button clicked)
+ public Set getEffectiveProtocolMapperNames() {
+ List rows = protocolMappersTable.rows();
+
+ Set names = rows.stream().map((WebElement row) -> {
+
+ return row.findElement(By.xpath("td[1]")).getText();
+
+ }).collect(Collectors.toSet());
+
+ return names;
+ }
+
+
+ public Set getGrantedRealmRoles() {
+ return ClientScopesSetupForm.getSelectValues(grantedRealmRolesSelect);
+ }
+
+ public Set getNotGrantedRealmRoles() {
+ return ClientScopesSetupForm.getSelectValues(notGrantedRealmRolesSelect);
+ }
+
+ public String getAccessToken() {
+ return accessTokenTextArea.getText();
+ }
+
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetup.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetup.java
new file mode 100644
index 0000000000..6f4ecfea5f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetup.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.console.page.clients.clientscopes;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.testsuite.console.page.clients.Client;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientScopesSetup extends Client {
+
+ @Page
+ private ClientScopesSetupForm form;
+
+ @Override
+ public String getUriFragment() {
+ return super.getUriFragment() + "/client-scopes/setup-scopes";
+ }
+
+ public ClientScopesSetupForm form() {
+ return form;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetupForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetupForm.java
new file mode 100644
index 0000000000..6ce0a46daf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/clientscopes/ClientScopesSetupForm.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.console.page.clients.clientscopes;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
+
+/**
+ * @author Marek Posolda
+ */
+public class ClientScopesSetupForm extends Form {
+
+ @FindBy(id = "available")
+ protected Select availableDefaultClientScopesSelect;
+
+ @FindBy(id = "assigned")
+ protected Select defaultClientScopesSelect;
+
+ @FindBy(id = "available-opt")
+ protected Select availableOptionalClientScopesSelect;
+
+ @FindBy(id = "assigned-opt")
+ protected Select optionalClientScopesSelect;
+
+
+ @FindBy(css = "button[ng-click*='addDefaultClientScope']")
+ protected WebElement addSelectedDefaultClientScopesButton;
+
+ @FindBy(css = "button[ng-click*='addOptionalClientScope']")
+ protected WebElement addSelectedOptionalClientScopesButton;
+
+ @FindBy(css = "button[ng-click*='deleteDefaultClientScope']")
+ protected WebElement removeSelectedDefaultClientScopesButton;
+
+ @FindBy(css = "button[ng-click*='deleteOptionalClientScope']")
+ protected WebElement removeSelectedOptionalClientScopesButton;
+
+
+ public Set getAvailableDefaultClientScopes() {
+ return getSelectValues(availableDefaultClientScopesSelect);
+ }
+
+ public Set getDefaultClientScopes() {
+ return getSelectValues(defaultClientScopesSelect);
+ }
+
+ public Set getAvailableOptionalClientScopes() {
+ return getSelectValues(availableOptionalClientScopesSelect);
+ }
+
+ public Set getOptionalClientScopes() {
+ return getSelectValues(optionalClientScopesSelect);
+ }
+
+
+ public void setDefaultClientScopes(Collection scopes) {
+ removeRedundantScopes(defaultClientScopesSelect, removeSelectedDefaultClientScopesButton, scopes);
+ addMissingScopes(availableDefaultClientScopesSelect, addSelectedDefaultClientScopesButton, scopes);
+ }
+
+ public void setOptionalClientScopes(Collection scopes) {
+ removeRedundantScopes(optionalClientScopesSelect, removeSelectedOptionalClientScopesButton, scopes);
+ addMissingScopes(availableOptionalClientScopesSelect, addSelectedOptionalClientScopesButton, scopes);
+ }
+
+
+ // Static helper methods
+
+ static Set getSelectValues(Select select) {
+ Set roles = new HashSet<>();
+ for (WebElement option : select.getOptions()) {
+ roles.add(option.getText());
+ }
+ return roles;
+ }
+
+
+ static void removeRedundantScopes(Select select, WebElement button, Collection scopes) {
+ boolean someRemoved = false;
+
+ select.deselectAll();
+ for (String scope : getSelectValues(select)) {
+ if (scopes == null // if scopes not provided, remove all
+ || !scopes.contains(scope)) { // if scopes provided, remove only the redundant
+ select.selectByVisibleText(scope);
+ someRemoved = true;
+ }
+ }
+
+ if (someRemoved) {
+ waitUntilElement(button).is().enabled();
+ button.click();
+ }
+ }
+
+
+ static void addMissingScopes(Select select, WebElement button, Collection scopes) {
+ select.deselectAll();
+ if (scopes != null) { // if scopes not provided, don't add any
+ boolean someAdded = false;
+
+ for (String scope : getSelectValues(select)) {
+ if (scopes.contains(scope)) { // if scopes provided, add only the missing
+ select.selectByVisibleText(scope);
+ someAdded = true;
+ }
+ }
+
+ if (someAdded) {
+ waitUntilElement(button).is().enabled();
+ button.click();
+ }
+ }
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientClientScopesTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientClientScopesTest.java
new file mode 100644
index 0000000000..e7cb74fe62
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientClientScopesTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.testsuite.console.clients;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.console.page.clients.clientscopes.ClientScopesEvaluate;
+import org.keycloak.testsuite.console.page.clients.clientscopes.ClientScopesEvaluateForm;
+import org.keycloak.testsuite.console.page.clients.clientscopes.ClientScopesSetup;
+import org.keycloak.testsuite.console.page.clients.clientscopes.ClientScopesSetupForm;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.TokenUtil;
+
+import static org.junit.Assert.assertNotNull;
+import static org.keycloak.testsuite.auth.page.login.Login.OIDC;
+
+/**
+ * Test for the "Client Scopes" tab of client (Binding between "Client" and "Client Scopes")
+ *
+ * @author Marek Posolda
+ */
+public class ClientClientScopesTest extends AbstractClientTest {
+
+ private ClientRepresentation newClient;
+ private ClientRepresentation found;
+
+
+ @Page
+ private ClientScopesSetup clientScopesSetupPage;
+
+ @Page
+ private ClientScopesEvaluate clientScopesEvaluatePage;
+
+
+ @Before
+ public void before() {
+ newClient = createClientRep(TEST_CLIENT_ID, OIDC);
+ newClient.setFullScopeAllowed(false);
+
+ testRealmResource().clients().create(newClient).close();
+
+ found = findClientByClientId(TEST_CLIENT_ID);
+ assertNotNull("Client " + TEST_CLIENT_ID + " was not found.", found);
+ clientScopesSetupPage.setId(found.getId());
+ clientScopesSetupPage.navigateTo();
+ }
+
+
+ @Test
+ public void testSetupClientScopes() {
+ ClientScopesSetupForm setupForm = clientScopesSetupPage.form();
+
+ // Test the initial state
+ Assert.assertNames(setupForm.getAvailableDefaultClientScopes());
+ Assert.assertNames(setupForm.getDefaultClientScopes(), "email", "profile");
+ Assert.assertNames(setupForm.getAvailableOptionalClientScopes());
+ Assert.assertNames(setupForm.getOptionalClientScopes(), "address", "phone", "offline_access");
+
+ // Remove 'profile' as default client scope and assert
+ setupForm.setDefaultClientScopes(Collections.singletonList("email"));
+ Assert.assertNames(setupForm.getAvailableDefaultClientScopes(), "profile");
+ Assert.assertNames(setupForm.getDefaultClientScopes(), "email");
+ Assert.assertNames(setupForm.getAvailableOptionalClientScopes(), "profile");
+ Assert.assertNames(setupForm.getOptionalClientScopes(), "address", "phone", "offline_access");
+
+ // Add 'profile' as optional client scope and assert
+ setupForm.setOptionalClientScopes(Arrays.asList("profile", "address", "phone", "offline_access"));
+ Assert.assertNames(setupForm.getAvailableDefaultClientScopes());
+ Assert.assertNames(setupForm.getDefaultClientScopes(), "email");
+ Assert.assertNames(setupForm.getAvailableOptionalClientScopes());
+ Assert.assertNames(setupForm.getOptionalClientScopes(), "profile", "address", "phone", "offline_access");
+
+ // Retrieve client through adminClient
+ found = findClientByClientId(TEST_CLIENT_ID);
+ Assert.assertNames(found.getDefaultClientScopes(), "email", "role_list"); // SAML client scope 'role_list' is included too in the rep
+ Assert.assertNames(found.getOptionalClientScopes(), "profile", "address", "phone", "offline_access");
+
+
+ // Revert and check things successfully reverted
+ setupForm.setOptionalClientScopes(Arrays.asList("address", "phone", "offline_access"));
+ Assert.assertNames(setupForm.getAvailableDefaultClientScopes(), "profile");
+ setupForm.setDefaultClientScopes(Arrays.asList("profile", "email"));
+
+ Assert.assertNames(setupForm.getAvailableDefaultClientScopes());
+ Assert.assertNames(setupForm.getDefaultClientScopes(), "email", "profile");
+ Assert.assertNames(setupForm.getAvailableOptionalClientScopes());
+ Assert.assertNames(setupForm.getOptionalClientScopes(), "address", "phone", "offline_access");
+ }
+
+
+ @Test
+ public void testEvaluateClientScopes() throws IOException {
+ clientScopesEvaluatePage.setId(found.getId());
+ clientScopesEvaluatePage.navigateTo();
+
+ ClientScopesEvaluateForm evaluateForm = clientScopesEvaluatePage.form();
+
+ // Check the defaults
+ Assert.assertNames(evaluateForm.getAvailableClientScopes(), "address", "phone", "offline_access");
+ Assert.assertNames(evaluateForm.getAssignedClientScopes());
+ Assert.assertNames(evaluateForm.getEffectiveClientScopes(), "profile", "email");
+
+ // Add some optional scopes to the evaluation
+ evaluateForm.setAssignedClientScopes(Arrays.asList("address", "phone"));
+ Assert.assertNames(evaluateForm.getAvailableClientScopes(), "offline_access");
+ Assert.assertNames(evaluateForm.getAssignedClientScopes(), "address", "phone");
+ Assert.assertNames(evaluateForm.getEffectiveClientScopes(), "address", "phone", "profile", "email");
+
+ // Remove optional 'phone' scope from the evaluation
+ evaluateForm.setAssignedClientScopes(Arrays.asList("address", "offline_access"));
+ Assert.assertNames(evaluateForm.getAvailableClientScopes(), "phone");
+ Assert.assertNames(evaluateForm.getAssignedClientScopes(), "address", "offline_access");
+ Assert.assertNames(evaluateForm.getEffectiveClientScopes(), "address", "offline_access", "profile", "email");
+
+ // Select some user
+ evaluateForm.selectUser("test");
+
+ // Submit
+ evaluateForm.evaluate();
+
+ // Test protocolMappers of 'address' , 'profile' and 'email' scopes are included
+ Set protocolMappers = evaluateForm.getEffectiveProtocolMapperNames();
+ Assert.assertTrue(protocolMappers.contains("address"));
+ Assert.assertTrue(protocolMappers.contains("email"));
+ Assert.assertTrue(protocolMappers.contains("email verified"));
+ Assert.assertTrue(protocolMappers.contains("username"));
+ Assert.assertTrue(protocolMappers.contains("full name"));
+ Assert.assertFalse(protocolMappers.contains("phone"));
+
+ // Test roles
+ evaluateForm.showRoles();
+ Assert.assertNames(evaluateForm.getGrantedRealmRoles(), "offline_access");
+ Assert.assertNames(evaluateForm.getNotGrantedRealmRoles(), "uma_authorization");
+
+ // Test access token
+ evaluateForm.showToken();
+ String accessTokenStr = evaluateForm.getAccessToken();
+
+ AccessToken token = JsonSerialization.readValue(accessTokenStr, AccessToken.class);
+ String scopeParam = token.getScope();
+ Assert.assertTrue(TokenUtil.isOIDCRequest(scopeParam));
+ Assert.assertTrue(TokenUtil.hasScope(scopeParam, "address"));
+ Assert.assertTrue(TokenUtil.hasScope(scopeParam, "profile"));
+ Assert.assertTrue(TokenUtil.hasScope(scopeParam, "email"));
+ Assert.assertFalse(TokenUtil.hasScope(scopeParam, "phone"));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientScopeTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientScopeTest.java
index 1c9d03eddf..51e0ccf097 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientScopeTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/clients/ClientScopeTest.java
@@ -38,6 +38,7 @@ import static org.junit.Assert.*;
import static org.keycloak.testsuite.auth.page.login.Login.OIDC;
/**
+ * Test for the "Scope" tab of client (Client role mappings)
*
* @author Vlastislav Ramik
*/