Skip to content
Snippets Groups Projects
Commit 6d382808 authored by Harshika Dhoot's avatar Harshika Dhoot
Browse files

Merge branch 'logs-m' into 'master'

Adding logs for negative test case scenarios better debugging

See merge request !442
parents b795bc74 359a1604
No related branches found
No related tags found
1 merge request!442Adding logs for negative test case scenarios better debugging
Pipeline #227224 failed
...@@ -82,7 +82,7 @@ The following software have components provided under the terms of this license: ...@@ -82,7 +82,7 @@ The following software have components provided under the terms of this license:
- Byte Buddy (without dependencies) (from https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy) - Byte Buddy (without dependencies) (from https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy)
- Byte Buddy Java agent (from https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy-agent) - Byte Buddy Java agent (from https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy-agent)
- ClassMate (from http://github.com/cowtowncoder/java-classmate) - ClassMate (from http://github.com/cowtowncoder/java-classmate)
- Cloud Key Management Service (KMS) API (from https://repo1.maven.org/maven2/com/google/apis/google-api-services-cloudkms) - Cloud Key Management Service (KMS) API v1-rev20231012-2.0.0 (from https://repo1.maven.org/maven2/com/google/apis/google-api-services-cloudkms)
- Collections (from https://repo1.maven.org/maven2/commons-collections/commons-collections) - Collections (from https://repo1.maven.org/maven2/commons-collections/commons-collections)
- Commons Digester (from http://commons.apache.org/digester/) - Commons Digester (from http://commons.apache.org/digester/)
- Converter: Jackson (from https://github.com/square/retrofit, https://repo1.maven.org/maven2/com/squareup/retrofit2/converter-jackson) - Converter: Jackson (from https://github.com/square/retrofit, https://repo1.maven.org/maven2/com/squareup/retrofit2/converter-jackson)
...@@ -520,7 +520,6 @@ The following software have components provided under the terms of this license: ...@@ -520,7 +520,6 @@ The following software have components provided under the terms of this license:
- JBoss Jakarta Annotations API (from <https://github.com/jboss/jboss-jakarta-annotations-api_spec>, https://github.com/jboss/jboss-jakarta-annotations-api_spec) - JBoss Jakarta Annotations API (from <https://github.com/jboss/jboss-jakarta-annotations-api_spec>, https://github.com/jboss/jboss-jakarta-annotations-api_spec)
- Jakarta Annotations API (from https://projects.eclipse.org/projects/ee4j.ca) - Jakarta Annotations API (from https://projects.eclipse.org/projects/ee4j.ca)
- Jakarta WebSocket - Server API (from https://projects.eclipse.org/projects/ee4j.websocket, https://repo1.maven.org/maven2/org/jboss/spec/javax/websocket/jboss-websocket-api_1.1_spec) - Jakarta WebSocket - Server API (from https://projects.eclipse.org/projects/ee4j.websocket, https://repo1.maven.org/maven2/org/jboss/spec/javax/websocket/jboss-websocket-api_1.1_spec)
- RabbitMQ Java Client (from http://www.rabbitmq.com, https://www.rabbitmq.com)
- tomcat-embed-core (from http://tomcat.apache.org/) - tomcat-embed-core (from http://tomcat.apache.org/)
======================================================================== ========================================================================
...@@ -668,7 +667,6 @@ MPL-1.1 ...@@ -668,7 +667,6 @@ MPL-1.1
The following software have components provided under the terms of this license: The following software have components provided under the terms of this license:
- Javassist (from http://www.javassist.org/) - Javassist (from http://www.javassist.org/)
- RabbitMQ Java Client (from http://www.rabbitmq.com, https://www.rabbitmq.com)
======================================================================== ========================================================================
MPL-2.0 MPL-2.0
...@@ -677,13 +675,6 @@ The following software have components provided under the terms of this license: ...@@ -677,13 +675,6 @@ The following software have components provided under the terms of this license:
- RabbitMQ Java Client (from http://www.rabbitmq.com, https://www.rabbitmq.com) - RabbitMQ Java Client (from http://www.rabbitmq.com, https://www.rabbitmq.com)
========================================================================
RSA-MD
========================================================================
The following software have components provided under the terms of this license:
- AWS Java SDK for Amazon SQS (from https://aws.amazon.com/sdkforjava)
======================================================================== ========================================================================
SAX-PD SAX-PD
======================================================================== ========================================================================
......
...@@ -14,7 +14,11 @@ ...@@ -14,7 +14,11 @@
package org.opengroup.osdu.notification.api; package org.opengroup.osdu.notification.api;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Response; import org.apache.catalina.connector.Response;
import org.apache.commons.lang3.time.StopWatch; import org.apache.commons.lang3.time.StopWatch;
import org.asynchttpclient.util.Assertions; import org.asynchttpclient.util.Assertions;
...@@ -32,14 +36,13 @@ import org.opengroup.osdu.notification.util.*; ...@@ -32,14 +36,13 @@ import org.opengroup.osdu.notification.util.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.opengroup.osdu.notification.util.TestUtils.indentatedBody;
@Slf4j
public class TestStorageIntegration extends BaseTestTemplate { public class TestStorageIntegration extends BaseTestTemplate {
Logger logger= Logger.getLogger("");
private String subscriptionId = null; private String subscriptionId = null;
private String notificationId = null; private String notificationId = null;
private final String ackSubscriptionId = "AckSubscription"; private final String ackSubscriptionId = "AckSubscription";
...@@ -88,7 +91,7 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -88,7 +91,7 @@ public class TestStorageIntegration extends BaseTestTemplate {
headers.put(DpsHeaders.AUTHORIZATION, testUtils.getOpsToken()); headers.put(DpsHeaders.AUTHORIZATION, testUtils.getOpsToken());
DpsHeaders dpsHeaders = DpsHeaders.createFromMap(headers); DpsHeaders dpsHeaders = DpsHeaders.createFromMap(headers);
subscriptionService = factory.create(dpsHeaders); subscriptionService = factory.create(dpsHeaders);
logger.info("dps headers created."); log.info("dps headers created.");
// Create a new subscription // Create a new subscription
Subscription subscription = new Subscription(); Subscription subscription = new Subscription();
...@@ -105,10 +108,10 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -105,10 +108,10 @@ public class TestStorageIntegration extends BaseTestTemplate {
subscriptionId = subscriptionCreated.getId(); subscriptionId = subscriptionCreated.getId();
Config.Instance().NotificationId = notificationId; Config.Instance().NotificationId = notificationId;
} catch (SubscriptionException e) { } catch (SubscriptionException e) {
System.out.println("Subscription exception inner response : " + e.getHttpResponse()); log.error("Subscription exception inner response : ", e.getHttpResponse());
throw e; throw e;
} }
logger.info("New subscription created"); log.info("New subscription created");
} }
private void deleteAckSubscription() throws SubscriptionException { private void deleteAckSubscription() throws SubscriptionException {
...@@ -116,9 +119,10 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -116,9 +119,10 @@ public class TestStorageIntegration extends BaseTestTemplate {
subscriptionService.delete(ackSubscriptionId); subscriptionService.delete(ackSubscriptionId);
} catch (SubscriptionException e) { } catch (SubscriptionException e) {
if (e.getHttpResponse().getResponseCode() == Response.SC_NOT_FOUND) { if (e.getHttpResponse().getResponseCode() == Response.SC_NOT_FOUND) {
System.out.println("Test Ack Subscription Not Found for deletion."); log.info("Test Ack Subscription Not Found for deletion.");
return; return;
} }
log.error("Unable to delete Test Ack Subscription. Deletion Failed.", e);
Assert.fail("Unable to delete Test Ack Subscription. Deletion Failed." + e); Assert.fail("Unable to delete Test Ack Subscription. Deletion Failed." + e);
throw e; throw e;
} }
...@@ -145,23 +149,23 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -145,23 +149,23 @@ public class TestStorageIntegration extends BaseTestTemplate {
try { try {
// Creates an actual subscription with a test endpoint i.e REGISTER_CUSTOM_PUSH_URL_HMAC // Creates an actual subscription with a test endpoint i.e REGISTER_CUSTOM_PUSH_URL_HMAC
this.createResource(); this.createResource();
logger.info("Actual subscription with a test endpoint created successfully"); log.info("Actual subscription with a test endpoint created successfully");
// Delay to get the above subscription registered by Notification Service // Delay to get the above subscription registered by Notification Service
TimeUnit.SECONDS.sleep(600); TimeUnit.SECONDS.sleep(600);
logger.info("Subscription registered by Notification service successfully"); log.info("Subscription registered by Notification service successfully");
// Delete the Test Ack subscription if there exists any as a part of cleanup. // Delete the Test Ack subscription if there exists any as a part of cleanup.
// The Test Ack Subscription gets created if the the test endpoint of actual subscription // The Test Ack Subscription gets created if the the test endpoint of actual subscription
// created above receives notification from Notification Service. // created above receives notification from Notification Service.
this.deleteAckSubscription(); this.deleteAckSubscription();
// Delete legal tag // Delete legal tag
logger.info("Cleaning up previous Legal Tag if any. "); log.info("Cleaning up previous Legal Tag if any. ");
LegalTagUtils.delete(LEGAL_TAG, this.testUtils.getAdminToken()); LegalTagUtils.delete(LEGAL_TAG, this.testUtils.getAdminToken());
// Create legal tag used in storage record // Create legal tag used in storage record
logger.info("Creating New legal tag"); log.info("Creating New legal tag");
LegalTagUtils.create(LEGAL_TAG, this.testUtils.getAdminToken()); LegalTagUtils.create(LEGAL_TAG, this.testUtils.getAdminToken());
...@@ -170,7 +174,21 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -170,7 +174,21 @@ public class TestStorageIntegration extends BaseTestTemplate {
// test endpoint i.e REGISTER_CUSTOM_PUSH_URL_HMAC which creates the Test ack Subscription. // test endpoint i.e REGISTER_CUSTOM_PUSH_URL_HMAC which creates the Test ack Subscription.
String URL = Config.Instance().StorageServicePath; String URL = Config.Instance().StorageServicePath;
ClientResponse response = descriptor.run(URL, "", this.testUtils.getAdminToken()); ClientResponse response = descriptor.run(URL, "", this.testUtils.getAdminToken());
assertEquals(error(response.getEntity(String.class)), 201, response.getStatus()); if (response != null) {
JsonNode rootNode = null;
ObjectMapper objectMapper = new ObjectMapper();
try {
String responseBody = response.getEntity(String.class);
rootNode = objectMapper.readTree(responseBody);
// Remove the "secret" field
if (((ObjectNode) rootNode).has("secret"))
((ObjectNode) rootNode).remove("secret");
log.info(String.format("\nThis is the response received : %s\nResponse Body:%s\nResponse Headers: %s\nResponse Status code: %s", response, indentatedBody(objectMapper.writeValueAsString(rootNode)), response.getHeaders(), response.getStatus()));
} catch (Exception ex) {
log.error("Exception in response in test case: ", ex);
}
}
assertEquals(error("error while adding storage record"), 201, response.getStatus());
TimeUnit.SECONDS.sleep(120); TimeUnit.SECONDS.sleep(120);
// Verification of Test Ack Subscription creation from the above process. // Verification of Test Ack Subscription creation from the above process.
...@@ -183,25 +201,25 @@ public class TestStorageIntegration extends BaseTestTemplate { ...@@ -183,25 +201,25 @@ public class TestStorageIntegration extends BaseTestTemplate {
while (ackSubscription == null && retryCount > 0) { while (ackSubscription == null && retryCount > 0) {
TimeUnit.SECONDS.sleep(timeOut); TimeUnit.SECONDS.sleep(timeOut);
ackSubscription = subscriptionService.get(ackSubscriptionId); ackSubscription = subscriptionService.get(ackSubscriptionId);
logger.info("Retry time: "+retryCount+" to get Test Ack subscription."); log.info(String.format("Retry time: %s to get Test Ack subscription.", retryCount));
retryCount--; retryCount--;
} }
Assertions.assertNotNull(ackSubscription, "Unable to retrieve test ack subscription. Elapsed time in minutes : " + (stopWatch.getTime(TimeUnit.MINUTES))); Assertions.assertNotNull(ackSubscription, "Unable to retrieve test ack subscription. Elapsed time in minutes : " + (stopWatch.getTime(TimeUnit.MINUTES)));
if (ackSubscription != null) { if (ackSubscription != null) {
assertEquals("Unexpected Test Ack Subscription.", ackNotificationId, ackSubscription.getNotificationId()); assertEquals("Unexpected Test Ack Subscription.", ackNotificationId, ackSubscription.getNotificationId());
} }
logger.info("IT run successful"); log.info("IT run successful");
stopWatch.stop(); stopWatch.stop();
} catch (Exception e) { } catch (Exception e) {
fail("An exception occurred :" + e); fail("An exception occurred :" + e);
} finally { } finally {
logger.info("Executing finally block"); log.info("Executing finally block");
// Delete Actual Subscription // Delete Actual Subscription
this.deleteResource(); this.deleteResource();
// Deletion of Test Ack Subscription // Deletion of Test Ack Subscription
this.deleteAckSubscription(); this.deleteAckSubscription();
// Delete legal tag // Delete legal tag
logger.info("Deleting legal tag"); log.info("Deleting legal tag");
LegalTagUtils.delete(LEGAL_TAG, this.testUtils.getAdminToken()); LegalTagUtils.delete(LEGAL_TAG, this.testUtils.getAdminToken());
} }
......
...@@ -144,6 +144,12 @@ ...@@ -144,6 +144,12 @@
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.13.0</version> <version>3.13.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
......
...@@ -23,7 +23,6 @@ import java.net.URL; ...@@ -23,7 +23,6 @@ import java.net.URL;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import java.util.logging.Logger;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
...@@ -32,13 +31,21 @@ import javax.net.ssl.X509TrustManager; ...@@ -32,13 +31,21 @@ import javax.net.ssl.X509TrustManager;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class TestUtils { public abstract class TestUtils {
static Logger logger = Logger.getLogger("");
protected String serviceAccountFile; protected String serviceAccountFile;
protected static String opsToken = null; protected static String opsToken = null;
protected static String adminToken = null; protected static String adminToken = null;
...@@ -79,90 +86,105 @@ public abstract class TestUtils { ...@@ -79,90 +86,105 @@ public abstract class TestUtils {
public static ClientResponse send(String path, String httpMethod, String token, String requestBody, String query, public static ClientResponse send(String path, String httpMethod, String token, String requestBody, String query,
Map<String, String> headers, boolean enforceHttp) Map<String, String> headers, boolean enforceHttp)
throws Exception { throws Exception {
ClientResponse response; ClientResponse response = null;
Client client = getClient(); Client client = getClient();
client.setConnectTimeout(300000); client.setConnectTimeout(300000);
client.setReadTimeout(300000); client.setReadTimeout(300000);
client.setFollowRedirects(false); client.setFollowRedirects(false);
String url = getApiPath(path + query, enforceHttp); String url = getApiPath(path + query, enforceHttp);
System.out.println(url);
System.out.println(httpMethod);
System.out.println(requestBody);
System.out.println(headers);
WebResource webResource = client.resource(url); WebResource webResource = client.resource(url);
final WebResource.Builder builder = webResource.type(MediaType.APPLICATION_JSON) int count = 1;
.header("Authorization", token); int MaxRetry = 3;
int retryCount = 2; while (count < MaxRetry) {
headers.forEach((k, v) -> builder.header(k, v)); try {
try{ headers.put("correlation-id", headers.getOrDefault("correlation-id", UUID.randomUUID().toString()));
response = builder.method(httpMethod, ClientResponse.class, requestBody); WebResource.Builder builder = webResource.type(MediaType.APPLICATION_JSON)
while (retryCount > 0) { .header("Authorization", token);
headers.forEach((k, v) -> builder.header(k, v));
//removing Auth header before logging
headers.remove("Authorization");
log.info(String.format("\nRequest URL: %s %s\nRequest Headers: %s\nRequest Body: %s", httpMethod, url, headers, requestBody));
log.info(String.format("Attempt: #%s/%s, CorrelationId: %s", count, MaxRetry, headers.get("correlation-id")));
response = builder.method(httpMethod, ClientResponse.class, requestBody);
if (response.getStatusInfo().getFamily().equals(Response.Status.Family.valueOf("SERVER_ERROR"))) { if (response.getStatusInfo().getFamily().equals(Response.Status.Family.valueOf("SERVER_ERROR"))) {
System.out.println("got resoponse : " + response.getStatusInfo()); count++;
Thread.sleep(5000); Thread.sleep(5000);
System.out.println("Retrying.. "); continue;
response = builder.method(httpMethod, ClientResponse.class, requestBody); } else {
} else
break; break;
retryCount--; }
} } catch (Exception ex) {
System.out.println("sending response from TestUtils send method"); log.error("Exception While Making Request: ", ex);
return response; count++;
} catch (Exception e) { if (count == MaxRetry) {
if (e.getCause() instanceof SocketTimeoutException) { throw new AssertionError("Error: Send request error", ex);
System.out.println("Retrying in case of socket timeout exception"); }
return builder.method(httpMethod, ClientResponse.class, requestBody); } finally {
//log response body
log.info("sending response from TestUtils send method");
if(response!=null)
log.info(String.format("\nThis is the response received : %s\nResponse Headers: %s\nResponse Status code: %s", response, response.getHeaders(), response.getStatus()));
} }
e.printStackTrace();
throw new AssertionError("Error: Send request error", e);
} }
return response;
} }
public static ClientResponse send(String url, String path, String httpMethod, String token, String requestBody, public static ClientResponse send(String url, String path, String httpMethod, String token, String requestBody,
String query, Map<String, String> headers, boolean enforceHttp) String query, Map<String, String> headers, boolean enforceHttp)
throws Exception { throws Exception {
ClientResponse response; ClientResponse response = null;
Client client = getClient(); Client client = getClient();
client.setConnectTimeout(300000); client.setConnectTimeout(300000);
client.setReadTimeout(300000); client.setReadTimeout(300000);
client.setFollowRedirects(false); client.setFollowRedirects(false);
String URL = getApiPath(url, path + query, enforceHttp); String URL = getApiPath(url, path + query, enforceHttp);
System.out.println(url + path);
System.out.println(httpMethod);
System.out.println(requestBody);
System.out.println(headers);
WebResource webResource = client.resource(URL); WebResource webResource = client.resource(URL);
final WebResource.Builder builder = webResource.type(MediaType.APPLICATION_JSON); int count = 1;
if (!token.isEmpty()) { int MaxRetry = 3;
logger.info("Token is not empty so adding to request header"); while (count < MaxRetry) {
builder.header("Authorization", token); try {
} headers.put("correlation-id", headers.getOrDefault("correlation-id", UUID.randomUUID().toString()));
int retryCount = 2; WebResource.Builder builder = webResource.type(MediaType.APPLICATION_JSON);
headers.forEach((k, v) -> builder.header(k, v)); if (!token.isEmpty()) {
try{ log.info("Token is not empty so adding to request header");
response = builder.method(httpMethod, ClientResponse.class, requestBody); builder.header("Authorization", token);
while (retryCount > 0) { }
headers.forEach((k, v) -> builder.header(k, v));
//removing Auth header before logging
headers.remove("Authorization");
log.info(String.format("\nRequest URL: %s %s\nRequest Headers: %s\nRequest Body: %s", httpMethod, URL, headers, indentatedBody(requestBody)));
log.info(String.format("Attempt: #%s/%s, CorrelationId: %s", count, MaxRetry, headers.get("correlation-id")));
response = builder.method(httpMethod, ClientResponse.class, requestBody);
if (response.getStatusInfo().getFamily().equals(Response.Status.Family.valueOf("SERVER_ERROR"))) { if (response.getStatusInfo().getFamily().equals(Response.Status.Family.valueOf("SERVER_ERROR"))) {
System.out.println("got resoponse : " + response.getStatusInfo()); count++;
Thread.sleep(5000); Thread.sleep(5000);
System.out.println("Retrying.. "); continue;
response = builder.method(httpMethod, ClientResponse.class, requestBody); } else {
} else
break; break;
retryCount--; }
} } catch (Exception ex) {
System.out.println("sending response from TestUtils send method"); log.error("Exception While Making Request: ", ex);
return response; count++;
} catch (Exception e) { if (count == MaxRetry) {
if (e.getCause() instanceof SocketTimeoutException) { throw new AssertionError("Error: Send request error", ex);
System.out.println("Retrying in case of socket timeout exception"); }
return builder.method(httpMethod, ClientResponse.class, requestBody); } finally {
//log response body
log.info("sending response from TestUtils send method");
if(response!=null)
log.info(String.format("\nThis is the response received : %s\nResponse Headers: %s\nResponse Status code: %s", response, response.getHeaders(), response.getStatus()));
} }
e.printStackTrace();
throw new AssertionError("Error: Send request error", e);
} }
return response;
}
public static String indentatedBody(String responseBody) {
JsonParser jsonParser = new JsonParser();
if( responseBody== null)
return responseBody;
JsonElement jsonElement = jsonParser.parse(responseBody);
String indentedResponseEntity =new GsonBuilder().setPrettyPrinting().create().toJson(jsonElement);
return indentedResponseEntity;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
...@@ -204,9 +226,9 @@ public abstract class TestUtils { ...@@ -204,9 +226,9 @@ public abstract class TestUtils {
sc.init(null, trustAllCerts, new SecureRandom()); sc.init(null, trustAllCerts, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) { } catch (Exception e) {
logger.info("Exception occurred"); log.info("Exception occurred");
} }
logger.info("Creating client"); log.info("Creating client");
return Client.create(); return Client.create();
} }
} }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment