Added a new API: install plugin

Change-Id: I3f8cb4935487cee9b2fa83a0c4d6fa15a53d3b29
diff --git a/full/Changes b/full/Changes
index 9bf30f2..18bb5ef 100644
--- a/full/Changes
+++ b/full/Changes
@@ -6,6 +6,7 @@
  - Handled user-defined refresh token expiry (added in client info and 
    list API response)
  - Added installed_plugins table
+ - Added a new API: install plugin 
 
 
 # version 0.67.1
diff --git a/full/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java b/full/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java
new file mode 100644
index 0000000..3cef4c6
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/dto/InstalledPluginDto.java
@@ -0,0 +1,31 @@
+package de.ids_mannheim.korap.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_EMPTY)
+public class InstalledPluginDto {
+    private String client_id; // oauth2 client id
+    private String name;
+    private String description;
+    private String url;
+    @JsonProperty("installed_date")
+    private String installedDate;
+    
+    public InstalledPluginDto (InstalledPlugin plugin) {
+        OAuth2Client client = plugin.getClient();
+        setClient_id(client.getId());
+        setInstalledDate(plugin.getInstalledDate().toString());
+        setName(client.getName());
+        setDescription(client.getDescription());
+        setUrl(client.getUrl());
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java b/full/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java
new file mode 100644
index 0000000..1aef8b6
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/InstalledPlugin.java
@@ -0,0 +1,36 @@
+package de.ids_mannheim.korap.entity;
+
+import java.time.ZonedDateTime;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@Entity
+@Table(name = "installed_plugin")
+public class InstalledPlugin {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+    @Column(name = "installed_by")
+    private String installedBy;
+    @Column(name = "installed_date")
+    private ZonedDateTime installedDate;
+    
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "client_id")
+    private OAuth2Client client;
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java
new file mode 100644
index 0000000..8cd5368
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/InstalledPluginDao.java
@@ -0,0 +1,37 @@
+package de.ids_mannheim.korap.oauth2.dao;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.entity.InstalledPlugin;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+@Repository
+@Transactional
+public class InstalledPluginDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public InstalledPlugin storeUserPlugin (OAuth2Client client,
+            String installedBy) throws KustvaktException {
+        ParameterChecker.checkStringValue(installedBy, "installed_by");
+
+        InstalledPlugin p = new InstalledPlugin();
+        p.setInstalledBy(installedBy);
+        p.setInstalledDate(
+                ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE)));
+        p.setClient(client);
+        entityManager.persist(p);
+        return p;
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
index 13f0397..6961092 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -104,7 +104,7 @@
         }
         catch (NoResultException e) {
             throw new KustvaktException(StatusCodes.CLIENT_NOT_FOUND,
-                    "Unknown client with " + clientId + ".", "invalid_client");
+                    "Unknown client: " + clientId, "invalid_client");
         }
         catch (Exception e) {
             throw new KustvaktException(StatusCodes.CLIENT_NOT_FOUND,
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
index 62bb543..88c572c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
@@ -12,6 +12,7 @@
 import javax.persistence.OneToMany;
 import javax.persistence.Table;
 
+import de.ids_mannheim.korap.entity.InstalledPlugin;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
 
 /** Describe oauth2_client database table mapping.
@@ -53,6 +54,9 @@
     @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
     private List<AccessToken> accessTokens;
     
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<InstalledPlugin> installedPlugins;
+    
     @Override
     public String toString () {
         return "id=" + id + ", name=" + name + ", secret=" + secret + ", type="
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index b9e32c6..076a762 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -141,7 +141,7 @@
 
         if (refreshTokenStr == null || refreshTokenStr.isEmpty()) {
             throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
-                    "Missing parameters: refresh_token",
+                    "Missing parameter: refresh_token",
                     OAuth2Error.INVALID_REQUEST);
         }
 
@@ -317,7 +317,7 @@
         if (clientSecret == null || clientSecret.isEmpty()) {
             throw new KustvaktException(
                     StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                    "Missing parameters: client_secret",
+                    "Missing parameter: client_secret",
                     OAuth2Error.INVALID_REQUEST);
         }
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
index 85bb057..93692ad 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
@@ -154,7 +154,7 @@
         if (clientAuthentication == null) {
             if (clientId == null) {
                 throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
-                        "Missing parameters: client_id",
+                        "Missing parameter: client_id",
                         OAuth2Error.INVALID_REQUEST);
             }
             else {
@@ -208,7 +208,7 @@
         if (clientAuthentication == null) {
             if (clientId == null) {
                 throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
-                        "Missing parameters: client_id",
+                        "Missing parameter: client_id",
                         OAuth2Error.INVALID_REQUEST);
             }
             else {
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
index 33ef04e..acaaa88 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -15,7 +15,9 @@
 
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.dao.AdminDao;
+import de.ids_mannheim.korap.dto.InstalledPluginDto;
 import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
+import de.ids_mannheim.korap.entity.InstalledPlugin;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.interfaces.EncryptionIface;
@@ -25,6 +27,7 @@
 import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
 import de.ids_mannheim.korap.oauth2.dao.OAuth2ClientDao;
 import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.InstalledPluginDao;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2UserClientDto;
@@ -63,6 +66,8 @@
 //                    UrlValidator.NO_FRAGMENTS + UrlValidator.ALLOW_LOCAL_URLS);
 
     @Autowired
+    private InstalledPluginDao pluginDao;
+    @Autowired
     private OAuth2ClientDao clientDao;
     @Autowired
     private AccessTokenDao tokenDao;
@@ -272,26 +277,35 @@
 
     public OAuth2Client authenticateClient (String clientId,
             String clientSecret) throws KustvaktException {
-
+        return authenticateClient(clientId, clientSecret, false);
+    }
+    
+    public OAuth2Client authenticateClient (String clientId,
+            String clientSecret, boolean isSuper) throws KustvaktException {
+        String errorClient = "client";
+        if (isSuper) {
+            errorClient = "super_client";
+        }
+        
         if (clientId == null || clientId.isEmpty()) {
             throw new KustvaktException(
                     StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                    "Missing parameters: client id",
+                    "Missing parameter: "+errorClient+"_id",
                     OAuth2Error.INVALID_REQUEST);
         }
 
         OAuth2Client client = clientDao.retrieveClientById(clientId);
-        authenticateClient(client, clientSecret);
+        authenticateClient(client, clientSecret, errorClient);
         return client;
     }
 
-    public void authenticateClient (OAuth2Client client, String clientSecret)
-            throws KustvaktException {
+    public void authenticateClient (OAuth2Client client, String clientSecret,
+            String errorClient) throws KustvaktException {
         if (clientSecret == null || clientSecret.isEmpty()) {
             if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
                 throw new KustvaktException(
                         StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                        "Missing parameters: client_secret",
+                        "Missing parameter: "+errorClient+"_secret",
                         OAuth2Error.INVALID_REQUEST);
             }
         }
@@ -299,14 +313,14 @@
             if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
                 throw new KustvaktException(
                         StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                        "Client secret was not registered",
+                        errorClient+"_secret was not registered",
                         OAuth2Error.INVALID_CLIENT);
             }
         }
         else if (!encryption.checkHash(clientSecret, client.getSecret())) {
             throw new KustvaktException(
                     StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                    "Invalid client credentials", OAuth2Error.INVALID_CLIENT);
+                    "Invalid "+errorClient+" credentials", OAuth2Error.INVALID_CLIENT);
         }
     }
 
@@ -315,7 +329,7 @@
         if (clientId == null || clientId.isEmpty()) {
             throw new KustvaktException(
                     StatusCodes.CLIENT_AUTHENTICATION_FAILED,
-                    "Missing parameters: client id",
+                    "Missing parameter: client_id",
                     OAuth2Error.INVALID_REQUEST);
         }
 
@@ -376,6 +390,24 @@
         Collections.sort(plugins);
         return createClientDtos(plugins);
     }
+    
+    public InstalledPluginDto installPlugin (String clientId,
+            String installedBy) throws KustvaktException {
+        if (clientId == null || clientId.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "Missing parameter: client_id");
+        }
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        if (!client.isPermitted()) {
+            throw new KustvaktException(StatusCodes.PLUGIN_NOT_PERMITTED,
+                    "Plugin is not permitted");
+        }
+        InstalledPlugin plugin =
+                pluginDao.storeUserPlugin(client, installedBy);
+        
+        InstalledPluginDto dto = new InstalledPluginDto(plugin);
+        return dto;
+    }
 
     private List<OAuth2UserClientDto> createClientDtos (
             List<OAuth2Client> userClients) throws KustvaktException {
@@ -394,10 +426,10 @@
 
     public void verifySuperClient (String clientId, String clientSecret)
             throws KustvaktException {
-        OAuth2Client client = authenticateClient(clientId, clientSecret);
+        OAuth2Client client = authenticateClient(clientId, clientSecret,true);
         if (!client.isSuper()) {
             throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
-                    "Only super client is allowed to list user registered clients.",
+                    "Only super client is allowed to use this service",
                     OAuth2Error.UNAUTHORIZED_CLIENT);
         }
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index ee2a46e..bbd013a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -21,6 +21,7 @@
 import com.sun.jersey.spi.container.ResourceFilters;
 
 import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.dto.InstalledPluginDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
@@ -255,4 +256,29 @@
             throw responseHandler.throwit(e);
         }
     }
+    
+    @POST
+    @Path("/install")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public InstalledPluginDto installPlugin (
+            @Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("client_id") String clientId) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.INSTALL_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.installPlugin(clientId, username);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
index 0a2475c..9e586e4 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
@@ -164,7 +164,7 @@
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuth2Error.INVALID_CLIENT, node.at("/error").asText());
-        assertEquals("Unknown client with unknown-client-id.",
+        assertEquals("Unknown client: unknown-client-id",
                 node.at("/error_description").asText());
     }
 
@@ -556,7 +556,7 @@
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
                 node.at("/error").asText());
-        assertEquals("Missing parameters: client_secret",
+        assertEquals("Missing parameter: client_secret",
                 node.at("/error_description").asText());
     }
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
index 8fcccf2..6860e07 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
@@ -325,7 +325,7 @@
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
-        assertEquals("Missing parameters: client_secret",
+        assertEquals("Missing parameter: client_secret",
                 node.at("/error_description").asText());
     }
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
index 4bb0770..83472e5 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
@@ -196,7 +196,7 @@
         assertFalse(node.at("/0/registration_date").isMissingNode());
         assertFalse(node.at("/0/source").isMissingNode());
         assertFalse(node.at("/0/refresh_token_expiry").isMissingNode());
-        
+
         assertTrue(node.at("/1/refresh_token_expiry").isMissingNode());
     }
 
@@ -222,4 +222,124 @@
         return JsonUtils.readTree(entity);
     }
 
+    @Test
+    public void testInstallConfidentialPlugin () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = getSuperClientForm();
+        form.add("client_id", confidentialClientId2);
+        ClientResponse response = installPlugin(form);
+        
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(confidentialClientId2, node.at("/client_id").asText());
+        assertFalse(node.at("/name").isMissingNode());
+        assertFalse(node.at("/description").isMissingNode());
+        assertFalse(node.at("/url").isMissingNode());
+        assertFalse(node.at("/installed_date").isMissingNode());
+        
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testInstallPublicPlugin () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = getSuperClientForm();
+        form.add("client_id", publicClientId2);
+        ClientResponse response = installPlugin(form);
+        
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(publicClientId2, node.at("/client_id").asText());
+        assertFalse(node.at("/name").isMissingNode());
+        assertFalse(node.at("/description").isMissingNode());
+        assertFalse(node.at("/url").isMissingNode());
+        assertFalse(node.at("/installed_date").isMissingNode());
+        
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
+    @Test
+    public void testInstallPluginMissingClientId () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = getSuperClientForm();
+        ClientResponse response = installPlugin(form);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.MISSING_PARAMETER, node.at("/errors/0/0").asInt());
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testInstallPluginInvalidClientId () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = getSuperClientForm();
+        form.add("client_id", "unknown");
+        ClientResponse response = installPlugin(form);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals("Unknown client: unknown",
+                node.at("/error_description").asText());
+        assertEquals("invalid_client", node.at("/error").asText());
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testInstallPluginMissingSuperClientSecret () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", superClientId);
+        
+        ClientResponse response = installPlugin(form);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        
+        assertEquals("Missing parameter: super_client_secret",
+                node.at("/error_description").asText());
+        assertEquals("invalid_request", node.at("/error").asText());
+        
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testInstallPluginMissingSuperClientId () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        ClientResponse response = installPlugin(form);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        
+        assertEquals("Missing parameter: super_client_id",
+                node.at("/error_description").asText());
+        assertEquals("invalid_request", node.at("/error").asText());
+        
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testInstallPluginUnauthorizedClient ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", confidentialClientId);
+        form.add("super_client_secret", clientSecret);
+        
+        ClientResponse response = installPlugin(form);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals("unauthorized_client", node.at("/error").asText());
+        
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+    }
+
+    private ClientResponse installPlugin (MultivaluedMap<String, String> form)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        return resource().path(API_VERSION).path("oauth2").path("client")
+                .path("install")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+    }
 }