From e21bc9b7b57321e5e430ab08deee450ebe870932 Mon Sep 17 00:00:00 2001 From: Rukshan Date: Thu, 14 Jun 2018 18:17:27 +0530 Subject: [PATCH] Added throttling and jwt test cases --- .../resources/templates/policy_init.mustache | 1 - .../gateway/tests/common/BaseTestCase.java | 83 ++++++++++ .../gateway/tests/common/CLIExecutor.java | 11 +- .../tests/common/MockAPIPublisher.java | 30 ++++ .../gateway/tests/common/MockHttpServer.java | 25 ++- .../tests/common/model/ApplicationDTO.java | 55 +++++++ .../tests/common/model/ApplicationPolicy.java | 105 ++++++++++++ .../tests/common/model/DefaultLimit.java | 60 +++++++ .../tests/common/model/SubscribedApiDTO.java | 80 ++++++++++ .../common/model/SubscriptionPolicy.java | 150 ++++++++++++++++++ .../tests/services/APIInvokeTestCase.java | 22 ++- .../tests/services/ThrottlingTestCase.java | 124 +++++++++++++++ .../resources/confs/default-test-config.conf | 2 +- .../resources/key-validation-response.xml | 2 +- tests/src/test/resources/testng.xml | 4 + tests/src/test/resources/wso2carbon.jks | Bin 0 -> 33497 bytes 16 files changed, 738 insertions(+), 16 deletions(-) create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationDTO.java create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationPolicy.java create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/common/model/DefaultLimit.java create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscribedApiDTO.java create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscriptionPolicy.java create mode 100644 tests/src/test/java/org/wso2/micro/gateway/tests/services/ThrottlingTestCase.java create mode 100644 tests/src/test/resources/wso2carbon.jks diff --git a/components/micro-gateway-cli/src/main/resources/templates/policy_init.mustache b/components/micro-gateway-cli/src/main/resources/templates/policy_init.mustache index 9d8dde6a6c..a6d07b785f 100644 --- a/components/micro-gateway-cli/src/main/resources/templates/policy_init.mustache +++ b/components/micro-gateway-cli/src/main/resources/templates/policy_init.mustache @@ -5,7 +5,6 @@ import wso2/gateway; future ftr = start initThrottlePolicies(); function initThrottlePolicies() { - runtime:sleep(100); while (true) { if(gateway:isStreamsInitialized == true) { log:printDebug("Throttle streams initialized."); diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/BaseTestCase.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/BaseTestCase.java index b1f7a5b0c7..898aa580e9 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/common/BaseTestCase.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/BaseTestCase.java @@ -17,10 +17,25 @@ */ package org.wso2.micro.gateway.tests.common; +import org.json.JSONArray; +import org.json.JSONObject; +import org.wso2.micro.gateway.tests.common.model.API; +import org.wso2.micro.gateway.tests.common.model.ApplicationDTO; +import org.wso2.micro.gateway.tests.common.model.SubscribedApiDTO; import org.wso2.micro.gateway.tests.context.ServerInstance; import org.wso2.micro.gateway.tests.util.TestConstant; import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Signature; +import java.util.Arrays; +import java.util.Base64; +import java.util.UUID; /** * Base test class for CLI based tests @@ -50,5 +65,73 @@ public void init(String label) throws Exception { public void finalize() throws Exception { mockHttpServer.stopIt(); microGWServer.stopServer(false); + MockAPIPublisher.getInstance().clear(); + } + + protected String getJWT(API api, ApplicationDTO applicationDTO, String tier) throws Exception { + return getJWT(api.getName(), "/" + api.getContext() + "/" + api.getVersion(), api.getVersion(), tier, + applicationDTO.getName(), applicationDTO.getTier()); + } + + protected String getJWT(String apiName, String context, String version, String subTier, String appName, + String appTier) throws Exception { + ApplicationDTO applicationDTO = new ApplicationDTO(); + applicationDTO.setId(10); + applicationDTO.setName(appName); + applicationDTO.setTier(appTier); + + SubscribedApiDTO subscribedApiDTO = new SubscribedApiDTO(); + subscribedApiDTO.setContext(context); + subscribedApiDTO.setName(apiName); + subscribedApiDTO.setVersion(version); + subscribedApiDTO.setPublisher("admin"); + subscribedApiDTO.setSubscriptionTier(subTier); + subscribedApiDTO.setSubscriberTenantDomain("carbon.super"); + + JSONObject jwtTokenInfo = new JSONObject(); + jwtTokenInfo.put("aud", "http://org.wso2.apimgt/gateway"); + jwtTokenInfo.put("sub", "admin"); + jwtTokenInfo.put("application", new JSONObject(applicationDTO)); + jwtTokenInfo.put("scope", "am_application_scope default"); + jwtTokenInfo.put("iss", "https://localhost:8244/token"); + jwtTokenInfo.put("keytype", "PRODUCTION"); + jwtTokenInfo.put("subscribedAPIs", new JSONArray(Arrays.asList(subscribedApiDTO))); + jwtTokenInfo.put("exp", System.currentTimeMillis() + 3600 * 1000); + jwtTokenInfo.put("iat", System.currentTimeMillis()); + jwtTokenInfo.put("jti", UUID.randomUUID()); + + String payload = jwtTokenInfo.toString(); + + JSONObject head = new JSONObject(); + head.put("typ", "JWT"); + head.put("alg", "RS256"); + head.put("x5t", "UB_BQy2HFV3EMTgq64Q-1VitYbE"); + String header = head.toString(); + + String base64UrlEncodedHeader = Base64.getUrlEncoder() + .encodeToString(header.getBytes(Charset.defaultCharset())); + String base64UrlEncodedBody = Base64.getUrlEncoder().encodeToString(payload.getBytes(Charset.defaultCharset())); + + Signature signature = Signature.getInstance("SHA256withRSA"); + String jksPath = getClass().getClassLoader().getResource("wso2carbon.jks").getPath(); + FileInputStream is = new FileInputStream(jksPath); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, "wso2carbon".toCharArray()); + String alias = "wso2carbon"; + Key key = keystore.getKey(alias, "wso2carbon".toCharArray()); + Key privateKey = null; + if (key instanceof PrivateKey) { + privateKey = key; + } + signature.initSign((PrivateKey) privateKey); + String assertion = base64UrlEncodedHeader + "." + base64UrlEncodedBody; + byte[] dataInBytes = assertion.getBytes(StandardCharsets.UTF_8); + signature.update(dataInBytes); + //sign the assertion and return the signature + byte[] signedAssertion = signature.sign(); + String base64UrlEncodedAssertion = Base64.getUrlEncoder().encodeToString(signedAssertion); + String jwt = base64UrlEncodedHeader + '.' + base64UrlEncodedBody + '.' + base64UrlEncodedAssertion; + + return jwt; } } diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/CLIExecutor.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/CLIExecutor.java index 8e0a1f4461..f9dfcaee3c 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/common/CLIExecutor.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/CLIExecutor.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wso2.apimgt.gateway.cli.constants.GatewayCliConstants; +import org.wso2.micro.gateway.tests.context.Constants; import java.io.File; import java.nio.file.Files; @@ -41,15 +42,17 @@ public class CLIExecutor { public void generate(String label) throws Exception { org.wso2.apimgt.gateway.cli.cmd.Main main = new org.wso2.apimgt.gateway.cli.cmd.Main(); - Path path = Files.createTempDirectory("userProject", new FileAttribute[0]); + String baseDir = (System.getProperty(Constants.SYSTEM_PROP_BASE_DIR, ".")) + File.separator + "target"; + Path path = Files.createTempDirectory(new File(baseDir).toPath(), "userProject", new FileAttribute[0]); log.info("CLI Project Home: " + path.toString()); System.setProperty(GatewayCliConstants.CLI_HOME, this.cliHome); log.info("CLI Home: " + this.cliHome); - File asd = new File(path.toString() + File.separator + GatewayCliConstants.MAIN_DIRECTORY_NAME + File.separator - + GatewayCliConstants.GW_DIST_CONF); - asd.mkdirs(); + File gwConfDir = new File( + path.toString() + File.separator + GatewayCliConstants.MAIN_DIRECTORY_NAME + File.separator + + GatewayCliConstants.GW_DIST_CONF); + gwConfDir.mkdirs(); Files.copy(new File( getClass().getClassLoader().getResource("confs" + File.separator + "default-cli-test-config.toml") .getPath()).toPath(), new File( diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockAPIPublisher.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockAPIPublisher.java index d40ba8c084..3a12788c7b 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockAPIPublisher.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockAPIPublisher.java @@ -23,6 +23,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.wso2.micro.gateway.tests.common.model.API; +import org.wso2.micro.gateway.tests.common.model.ApplicationPolicy; +import org.wso2.micro.gateway.tests.common.model.SubscriptionPolicy; import java.io.FileInputStream; import java.io.IOException; @@ -41,6 +43,8 @@ public class MockAPIPublisher { private Map> apis; private Map tokenInfo; private static MockAPIPublisher instance; + private static List subscriptionPolicies; + private static List applicationPolicies; public static MockAPIPublisher getInstance() { if (instance == null) { @@ -52,6 +56,8 @@ public static MockAPIPublisher getInstance() { public MockAPIPublisher() { apis = new HashMap<>(); tokenInfo = new HashMap<>(); + subscriptionPolicies = new ArrayList<>(); + applicationPolicies = new ArrayList<>(); } public void addApi(String label, API api) { @@ -127,10 +133,34 @@ public String getKeyValidationResponseForToken(String token) { try { String xmlResponse = IOUtils.toString(new FileInputStream( getClass().getClassLoader().getResource("key-validation-response.xml").getPath())); + xmlResponse = xmlResponse.replace("$APINAME", info.getApiName()); return xmlResponse; } catch (IOException e) { log.error("Error occurred when generating response", e); throw new RuntimeException(e.getMessage(), e); } } + + public void clear() { + tokenInfo.clear(); + apis.clear(); + subscriptionPolicies.clear(); + applicationPolicies.clear(); + } + + public void addSubscriptionPolicy(SubscriptionPolicy subscriptionPolicy) { + subscriptionPolicies.add(subscriptionPolicy); + } + + public static List getSubscriptionPolicies() { + return subscriptionPolicies; + } + + public void addApplicationPolicy(ApplicationPolicy applicationPolicy) { + applicationPolicies.add(applicationPolicy); + } + + public static List getApplicationPolicies() { + return applicationPolicies; + } } diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockHttpServer.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockHttpServer.java index 00c415ccc2..228c45b040 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockHttpServer.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/MockHttpServer.java @@ -17,16 +17,20 @@ */ package org.wso2.micro.gateway.tests.common; +import com.google.gson.Gson; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.wso2.apimgt.gateway.cli.constants.GatewayCliConstants; +import org.wso2.micro.gateway.tests.common.model.ApplicationPolicy; +import org.wso2.micro.gateway.tests.common.model.SubscriptionPolicy; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; @@ -117,7 +121,7 @@ public void handle(HttpExchange exchange) throws IOException { String label = null; for (String para : paras) { String[] searchQuery = para.split(":"); - if("label".equalsIgnoreCase(searchQuery[0])){ + if ("label".equalsIgnoreCase(searchQuery[0])) { label = searchQuery[1]; } } @@ -167,8 +171,13 @@ public void handle(HttpExchange exchange) throws IOException { }); httpServer.createContext(AdminRestAPIBasePath + "/throttling/policies/application", new HttpHandler() { public void handle(HttpExchange exchange) throws IOException { - byte[] response = IOUtils.toString(new FileInputStream( - getClass().getClassLoader().getResource("application-policies.json").getPath())).getBytes(); + String defaultPolicies = IOUtils.toString(new FileInputStream( + getClass().getClassLoader().getResource("application-policies.json").getPath())); + JSONObject policies = new JSONObject(defaultPolicies); + for (ApplicationPolicy policy : MockAPIPublisher.getInstance().getApplicationPolicies()) { + policies.getJSONArray("list").put(new JSONObject(new Gson().toJson(policy))); + } + byte[] response = policies.toString().getBytes(); exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length); exchange.getResponseBody().write(response); exchange.close(); @@ -176,9 +185,13 @@ public void handle(HttpExchange exchange) throws IOException { }); httpServer.createContext(AdminRestAPIBasePath + "/throttling/policies/subscription", new HttpHandler() { public void handle(HttpExchange exchange) throws IOException { - byte[] response = IOUtils.toString(new FileInputStream( - getClass().getClassLoader().getResource("subscription-policies.json").getPath())) - .getBytes(); + String defaultPolicies = IOUtils.toString(new FileInputStream( + getClass().getClassLoader().getResource("subscription-policies.json").getPath())); + JSONObject policies = new JSONObject(defaultPolicies); + for (SubscriptionPolicy policy : MockAPIPublisher.getInstance().getSubscriptionPolicies()) { + policies.getJSONArray("list").put(new JSONObject(new Gson().toJson(policy))); + } + byte[] response = policies.toString().getBytes(); exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length); exchange.getResponseBody().write(response); exchange.close(); diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationDTO.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationDTO.java new file mode 100644 index 0000000000..fd5c504ddf --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationDTO.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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.wso2.micro.gateway.tests.common.model; + +import java.io.Serializable; + +/** + * Application model + */ +public class ApplicationDTO implements Serializable { + + private int id; + private String name; + private String tier; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTier() { + return tier; + } + + public void setTier(String tier) { + this.tier = tier; + } +} \ No newline at end of file diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationPolicy.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationPolicy.java new file mode 100644 index 0000000000..0d941035fd --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/ApplicationPolicy.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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.wso2.micro.gateway.tests.common.model; + +import java.util.UUID; + +/** + * Application policy model + */ +public class ApplicationPolicy { + private String policyId = UUID.randomUUID().toString(); + private String policyName; + private String displayName; + private String description; + private boolean isDeployed = true; + private DefaultLimit defaultLimit = new DefaultLimit(); + + public String getPolicyId() { + return policyId; + } + + public void setPolicyId(String policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isDeployed() { + return isDeployed; + } + + public void setDeployed(boolean deployed) { + isDeployed = deployed; + } + + public String getType() { + return this.defaultLimit.getType(); + } + + public void setType(String type) { + this.defaultLimit.setType(type); + } + + public String getTimeUnit() { + return this.defaultLimit.getTimeUnit(); + } + + public void setTimeUnit(String timeUnit) { + this.defaultLimit.setTimeUnit(timeUnit); + } + + public int getUnitTime() { + return this.defaultLimit.getUnitTime(); + } + + public void setUnitTime(int unitTime) { + this.defaultLimit.setUnitTime(unitTime); + } + + public int getRequestCount() { + return this.defaultLimit.getRequestCount(); + } + + public void setRequestCount(int requestCount) { + this.defaultLimit.setRequestCount(requestCount); + ; + } +} diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/DefaultLimit.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/DefaultLimit.java new file mode 100644 index 0000000000..01919e22d0 --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/DefaultLimit.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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.wso2.micro.gateway.tests.common.model; + +/** + * Default limit model + */ +public class DefaultLimit { + private String type = "RequestCountLimit"; + private String timeUnit = "min"; + private int unitTime = 1; + private int requestCount = 5000; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTimeUnit() { + return timeUnit; + } + + public void setTimeUnit(String timeUnit) { + this.timeUnit = timeUnit; + } + + public int getUnitTime() { + return unitTime; + } + + public void setUnitTime(int unitTime) { + this.unitTime = unitTime; + } + + public int getRequestCount() { + return requestCount; + } + + public void setRequestCount(int requestCount) { + this.requestCount = requestCount; + } +} \ No newline at end of file diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscribedApiDTO.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscribedApiDTO.java new file mode 100644 index 0000000000..23e4d37a51 --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscribedApiDTO.java @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you 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.wso2.micro.gateway.tests.common.model; + +/** + * Subscription API model + */ +public class SubscribedApiDTO { + + private String name; + private String context; + private String version; + private String publisher; + private String subscriptionTier; + private String subscriberTenantDomain; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public String getSubscriptionTier() { + return subscriptionTier; + } + + public void setSubscriptionTier(String subscriptionTier) { + this.subscriptionTier = subscriptionTier; + } + + public String getSubscriberTenantDomain() { + return subscriberTenantDomain; + } + + public void setSubscriberTenantDomain(String subscriberTenantDomain) { + this.subscriberTenantDomain = subscriberTenantDomain; + } +} \ No newline at end of file diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscriptionPolicy.java b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscriptionPolicy.java new file mode 100644 index 0000000000..169a1b9513 --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/common/model/SubscriptionPolicy.java @@ -0,0 +1,150 @@ +package org.wso2.micro.gateway.tests.common.model;/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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. + */ + +import java.util.List; +import java.util.UUID; + +/** + * Subscription policy model + */ +public class SubscriptionPolicy { + private String policyId = UUID.randomUUID().toString(); + private String policyName; + private String displayName; + private String description; + private boolean isDeployed = true; + private int rateLimitCount = 0; + private String rateLimitTimeUnit = null; + private List customAttributes; + boolean stopOnQuotaReach = true; + private String billingPlan = "FREE"; + private DefaultLimit defaultLimit = new DefaultLimit(); + + public String getPolicyId() { + return policyId; + } + + public void setPolicyId(String policyId) { + this.policyId = policyId; + } + + public String getPolicyName() { + return policyName; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isDeployed() { + return isDeployed; + } + + public void setDeployed(boolean deployed) { + isDeployed = deployed; + } + + public int getRateLimitCount() { + return rateLimitCount; + } + + public void setRateLimitCount(int rateLimitCount) { + this.rateLimitCount = rateLimitCount; + } + + public String getRateLimitTimeUnit() { + return rateLimitTimeUnit; + } + + public void setRateLimitTimeUnit(String rateLimitTimeUnit) { + this.rateLimitTimeUnit = rateLimitTimeUnit; + } + + public List getCustomAttributes() { + return customAttributes; + } + + public void setCustomAttributes(List customAttributes) { + this.customAttributes = customAttributes; + } + + public boolean isStopOnQuotaReach() { + return stopOnQuotaReach; + } + + public void setStopOnQuotaReach(boolean stopOnQuotaReach) { + this.stopOnQuotaReach = stopOnQuotaReach; + } + + public String getBillingPlan() { + return billingPlan; + } + + public void setBillingPlan(String billingPlan) { + this.billingPlan = billingPlan; + } + + public String getType() { + return this.defaultLimit.getType(); + } + + public void setType(String type) { + this.defaultLimit.setType(type); + } + + public String getTimeUnit() { + return this.defaultLimit.getTimeUnit(); + } + + public void setTimeUnit(String timeUnit) { + this.defaultLimit.setTimeUnit(timeUnit); + } + + public int getUnitTime() { + return this.defaultLimit.getUnitTime(); + } + + public void setUnitTime(int unitTime) { + this.defaultLimit.setUnitTime(unitTime); + } + + public int getRequestCount() { + return this.defaultLimit.getRequestCount(); + } + + public void setRequestCount(int requestCount) { + this.defaultLimit.setRequestCount(requestCount); + ; + } +} \ No newline at end of file diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/services/APIInvokeTestCase.java b/tests/src/test/java/org/wso2/micro/gateway/tests/services/APIInvokeTestCase.java index db942bad45..a693f7cf45 100644 --- a/tests/src/test/java/org/wso2/micro/gateway/tests/services/APIInvokeTestCase.java +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/services/APIInvokeTestCase.java @@ -24,10 +24,11 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.wso2.micro.gateway.tests.common.model.API; import org.wso2.micro.gateway.tests.common.BaseTestCase; import org.wso2.micro.gateway.tests.common.KeyValidationInfo; import org.wso2.micro.gateway.tests.common.MockAPIPublisher; +import org.wso2.micro.gateway.tests.common.model.API; +import org.wso2.micro.gateway.tests.common.model.ApplicationDTO; import org.wso2.micro.gateway.tests.util.HttpClientRequest; import java.util.HashMap; @@ -36,7 +37,7 @@ public class APIInvokeTestCase extends BaseTestCase { private static final Logger log = LoggerFactory.getLogger(APIInvokeTestCase.class); private String label = "apimTestLabel"; - private String token; + private String token, jwtToken; @BeforeClass public void start() throws Exception { @@ -51,6 +52,12 @@ public void start() throws Exception { //Register API with label pub.addApi(label, api); + //Define application info + ApplicationDTO application = new ApplicationDTO(); + application.setName("jwtApp"); + application.setTier("Unlimited"); + application.setId((int) (Math.random() * 1000)); + //create key validation and subscription info for the API KeyValidationInfo info = new KeyValidationInfo(); info.setApiName(api.getName()); @@ -59,7 +66,7 @@ public void start() throws Exception { //Register a token with key validation info token = pub.getAndRegisterAccessToken(info); - + jwtToken = getJWT(api, application, "Unlimited"); //generate apis with CLI and start the micro gateway server super.init(label); } @@ -72,7 +79,16 @@ public void testApiInvoke() throws Exception { .doGet(microGWServer.getServiceURLHttp("pizzashack/1.0.0/menu"), headers); Assert.assertNotNull(response); Assert.assertEquals(response.getResponseCode(), 200, "Response code mismatched"); + } + @Test(description = "Test API invocation with a JWT token") + public void testApiInvokeWithJWT() throws Exception { + Map headers = new HashMap<>(); + headers.put(HttpHeaderNames.AUTHORIZATION.toString(), "Bearer " + jwtToken); + org.wso2.micro.gateway.tests.util.HttpResponse response = HttpClientRequest + .doGet(microGWServer.getServiceURLHttp("pizzashack/1.0.0/menu"), headers); + Assert.assertNotNull(response); + Assert.assertEquals(response.getResponseCode(), 200, "Response code mismatched"); } @AfterClass diff --git a/tests/src/test/java/org/wso2/micro/gateway/tests/services/ThrottlingTestCase.java b/tests/src/test/java/org/wso2/micro/gateway/tests/services/ThrottlingTestCase.java new file mode 100644 index 0000000000..633506adf3 --- /dev/null +++ b/tests/src/test/java/org/wso2/micro/gateway/tests/services/ThrottlingTestCase.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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.wso2.micro.gateway.tests.services; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.micro.gateway.tests.common.BaseTestCase; +import org.wso2.micro.gateway.tests.common.KeyValidationInfo; +import org.wso2.micro.gateway.tests.common.MockAPIPublisher; +import org.wso2.micro.gateway.tests.common.model.API; +import org.wso2.micro.gateway.tests.common.model.ApplicationDTO; +import org.wso2.micro.gateway.tests.common.model.ApplicationPolicy; +import org.wso2.micro.gateway.tests.common.model.SubscriptionPolicy; +import org.wso2.micro.gateway.tests.util.HttpClientRequest; + +import java.util.HashMap; +import java.util.Map; + +public class ThrottlingTestCase extends BaseTestCase { + private static final Logger log = LoggerFactory.getLogger(ThrottlingTestCase.class); + private String label = "apimTestLabel"; + private String token, jwtToken, jwtToken2; + + @BeforeClass + public void start() throws Exception { + //get mock APIM Instance + MockAPIPublisher pub = MockAPIPublisher.getInstance(); + API api = new API(); + api.setName("PizzaShackAPI"); + api.setContext("/pizzashack"); + api.setEndpoint("http://localhost:9443/echo"); + api.setVersion("1.0.0"); + api.setProvider("admin"); + //Register API with label + pub.addApi(label, api); + + //Define application info + ApplicationDTO application = new ApplicationDTO(); + application.setName("jwtApp"); + application.setTier("Unlimited"); + application.setId((int) (Math.random() * 1000)); + + SubscriptionPolicy subscriptionPolicy = new SubscriptionPolicy(); + subscriptionPolicy.setPolicyName("10MinSubPolicy"); + subscriptionPolicy.setRequestCount(10); + pub.addSubscriptionPolicy(subscriptionPolicy); + + ApplicationPolicy applicationPolicy = new ApplicationPolicy(); + applicationPolicy.setPolicyName("10MinAppPolicy"); + applicationPolicy.setRequestCount(10); + pub.addApplicationPolicy(applicationPolicy); + + ApplicationDTO application2 = new ApplicationDTO(); + application2.setName("jwtApp2"); + application2.setTier(applicationPolicy.getPolicyName()); + application2.setId((int) (Math.random() * 1000)); + + //Register a token with key validation info + jwtToken = getJWT(api, application, subscriptionPolicy.getPolicyName()); + jwtToken2 = getJWT(api, application2, "Unlimited"); + //generate apis with CLI and start the micro gateway server + super.init(label); + } + + @Test(description = "Test subscription throttling with a JWT token") + public void testSubscriptionThrottlingWithJWT() throws Exception { + Map headers = new HashMap<>(); + headers.put(HttpHeaderNames.AUTHORIZATION.toString(), "Bearer " + jwtToken); + int retry = 15; + int responseCode = -1; + while (retry > 0) + for (int i = 0; i < 15; i++) { + org.wso2.micro.gateway.tests.util.HttpResponse response = HttpClientRequest + .doGet(microGWServer.getServiceURLHttp("pizzashack/1.0.0/menu"), headers); + Assert.assertNotNull(response); + responseCode = response.getResponseCode(); + retry--; + } + Assert.assertEquals(responseCode, 429, "Request should have throttled out"); + } + + @Test(description = "Test application throttling with a JWT token") + public void testApplicationThrottlingWithJWT() throws Exception { + Map headers = new HashMap<>(); + headers.put(HttpHeaderNames.AUTHORIZATION.toString(), "Bearer " + jwtToken2); + int retry = 15; + int responseCode = -1; + while (retry > 0) + for (int i = 0; i < 15; i++) { + org.wso2.micro.gateway.tests.util.HttpResponse response = HttpClientRequest + .doGet(microGWServer.getServiceURLHttp("pizzashack/1.0.0/menu"), headers); + Assert.assertNotNull(response); + responseCode = response.getResponseCode(); + retry--; + } + Assert.assertEquals(responseCode, 429, "Request should have throttled out"); + } + + @AfterClass + public void stop() throws Exception { + //Stop all the mock servers + super.finalize(); + } +} diff --git a/tests/src/test/resources/confs/default-test-config.conf b/tests/src/test/resources/confs/default-test-config.conf index 0fe74a06a2..2508d4560c 100644 --- a/tests/src/test/resources/confs/default-test-config.conf +++ b/tests/src/test/resources/confs/default-test-config.conf @@ -21,7 +21,7 @@ tokenContext="oauth2" timestampSkew=5000 [jwtTokenConfig] -issuer="http://localhost:9443/token" +issuer="https://localhost:8244/token" audience="http://org.wso2.apimgt/gateway" certificateAlias="wso2apim" trustStore.path="${ballerina.home}/bre/security/ballerinaTruststore.p12" diff --git a/tests/src/test/resources/key-validation-response.xml b/tests/src/test/resources/key-validation-response.xml index c07f6eb940..4364dc768a 100644 --- a/tests/src/test/resources/key-validation-response.xml +++ b/tests/src/test/resources/key-validation-response.xml @@ -7,7 +7,7 @@ xmlns:ax2135="http://model.api.apimgt.carbon.wso2.org/xsd" xmlns:ax2136="http://dto.api.apimgt.carbon.wso2.org/xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ax2133:APIKeyValidationInfoDTO"> - PizzaShackAPI + $APINAME admin 11 diff --git a/tests/src/test/resources/testng.xml b/tests/src/test/resources/testng.xml index 26d3ffc337..cf85dcbfad 100644 --- a/tests/src/test/resources/testng.xml +++ b/tests/src/test/resources/testng.xml @@ -30,6 +30,10 @@ + + + + diff --git a/tests/src/test/resources/wso2carbon.jks b/tests/src/test/resources/wso2carbon.jks new file mode 100644 index 0000000000000000000000000000000000000000..b4b6220baec4547220626b5664ef5271e4b1feb0 GIT binary patch literal 33497 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 literal 0 HcmV?d00001