Compare commits
1 Commits
feature-fl
...
pact-jvm-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8216c90168 |
0
.idea/modules/spring-boot-testing.iml
generated
Normal file
0
.idea/modules/spring-boot-testing.iml
generated
Normal file
@@ -1,4 +1,10 @@
|
|||||||
userservice:
|
userservice:
|
||||||
|
ribbon:
|
||||||
|
eureka:
|
||||||
|
enabled: false
|
||||||
|
listOfServers: localhost:8080
|
||||||
|
|
||||||
|
rootservice:
|
||||||
ribbon:
|
ribbon:
|
||||||
eureka:
|
eureka:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ dependencies {
|
|||||||
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
|
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
|
||||||
compile('com.h2database:h2:1.4.196')
|
compile('com.h2database:h2:1.4.196')
|
||||||
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
|
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
|
||||||
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
|
compile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.16")
|
||||||
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
|
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue;
|
||||||
|
|
||||||
|
public class PactDslJsonBodyLikeMapper {
|
||||||
|
|
||||||
|
private static final Set<Class<?>> SIMPLE_TYPES = new HashSet<>(Arrays.asList(
|
||||||
|
Boolean.class,
|
||||||
|
boolean.class,
|
||||||
|
Integer.class,
|
||||||
|
int.class,
|
||||||
|
Double.class,
|
||||||
|
double.class,
|
||||||
|
Float.class,
|
||||||
|
float.class,
|
||||||
|
BigDecimal.class,
|
||||||
|
Number.class,
|
||||||
|
String.class,
|
||||||
|
Long.class,
|
||||||
|
long.class
|
||||||
|
));
|
||||||
|
|
||||||
|
public static PactDslJsonBody like(Object object) {
|
||||||
|
return like(object, new PactDslJsonBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PactDslJsonBody like(Object object, PactDslJsonBody body) {
|
||||||
|
try {
|
||||||
|
return recursiveLike(object, body);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new IllegalStateException("could not create PactDslJsonBody due to exception!", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PactDslJsonBody recursiveLike(Object object, PactDslJsonBody body) throws IllegalAccessException {
|
||||||
|
Field[] fields = object.getClass().getDeclaredFields();
|
||||||
|
for (Field field : fields) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
|
||||||
|
Object fieldValue = field.get(object);
|
||||||
|
|
||||||
|
if (fieldValue == null) {
|
||||||
|
// fields with null values will not be mapped
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSimpleType(field.getType())) {
|
||||||
|
mapSimpleFieldWithName(field.getName(), fieldValue, body);
|
||||||
|
} else if (isCollectionType(field.getType())) {
|
||||||
|
mapCollectionField(field.getName(), (Collection) fieldValue, body);
|
||||||
|
} else {
|
||||||
|
mapComplexField(field.getName(), fieldValue, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mapSimpleFieldWithName(String fieldName, Object fieldValue, PactDslJsonBody body) throws IllegalAccessException {
|
||||||
|
Class<?> type = fieldValue.getClass();
|
||||||
|
if (String.class == type) {
|
||||||
|
body.stringType(fieldName, (String) fieldValue);
|
||||||
|
} else if (Boolean.class == type || boolean.class == type) {
|
||||||
|
body.booleanType(fieldName, (Boolean) fieldValue);
|
||||||
|
} else if (Integer.class == type || int.class == type || Long.class == type || long.class == type) {
|
||||||
|
body.integerType(fieldName, (Integer) fieldValue);
|
||||||
|
} else if (Double.class == type || double.class == type) {
|
||||||
|
body.decimalType(fieldName, (Double) fieldValue);
|
||||||
|
} else if (Float.class == type || float.class == type) {
|
||||||
|
body.decimalType(fieldName, ((Float) fieldValue).doubleValue());
|
||||||
|
} else if (BigDecimal.class == type) {
|
||||||
|
body.decimalType(fieldName, (BigDecimal) fieldValue);
|
||||||
|
} else if (Number.class.isAssignableFrom(type)) {
|
||||||
|
body.numberType(fieldName, (Number) fieldValue);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException(String.format("field '%s' of type '%s' is not a simple field", fieldName, type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PactDslJsonRootValue getRootValueForType(Class<?> type) {
|
||||||
|
if (String.class == type) {
|
||||||
|
return PactDslJsonRootValue.stringType();
|
||||||
|
} else if (Boolean.class == type || boolean.class == type) {
|
||||||
|
return PactDslJsonRootValue.booleanType();
|
||||||
|
} else if (Integer.class == type || int.class == type || Long.class == type || long.class == type) {
|
||||||
|
return PactDslJsonRootValue.integerType();
|
||||||
|
} else if (Double.class == type || double.class == type) {
|
||||||
|
return PactDslJsonRootValue.decimalType();
|
||||||
|
} else if (Float.class == type || float.class == type) {
|
||||||
|
return PactDslJsonRootValue.decimalType();
|
||||||
|
} else if (BigDecimal.class == type) {
|
||||||
|
return PactDslJsonRootValue.decimalType();
|
||||||
|
} else if (Number.class.isAssignableFrom(type)) {
|
||||||
|
return PactDslJsonRootValue.numberType();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException(String.format("unsupported type '%s'", type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mapCollectionField(String fieldName, Collection<?> collection, PactDslJsonBody body) throws IllegalAccessException {
|
||||||
|
if (collection.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("matching empty lists is not supported!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> listType = collection.iterator().next().getClass();
|
||||||
|
|
||||||
|
if (isSimpleType(listType)) {
|
||||||
|
PactDslJsonRootValue rootValue = getRootValueForType(listType);
|
||||||
|
body.eachLike(fieldName, rootValue);
|
||||||
|
} else if (isCollectionType(listType)) {
|
||||||
|
throw new IllegalArgumentException("collections of collections are not supported");
|
||||||
|
} else {
|
||||||
|
PactDslJsonBody nestedBody = body.eachLike(fieldName);
|
||||||
|
for (Object complexObject : collection) {
|
||||||
|
mapComplexFieldWithoutOpeningObject(complexObject, nestedBody);
|
||||||
|
}
|
||||||
|
nestedBody.closeObject().closeArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mapComplexField(String fieldName, Object fieldValue, PactDslJsonBody body) throws IllegalAccessException {
|
||||||
|
PactDslJsonBody nestedBody = body.object(fieldName);
|
||||||
|
mapComplexFieldWithoutOpeningObject(fieldValue, nestedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mapComplexFieldWithoutOpeningObject(Object fieldValue, PactDslJsonBody nestedBody) throws IllegalAccessException {
|
||||||
|
recursiveLike(fieldValue, nestedBody);
|
||||||
|
nestedBody.closeObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSimpleType(Class<?> type) {
|
||||||
|
return SIMPLE_TYPES.contains(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCollectionType(Class<?> type) {
|
||||||
|
return Collection.class.isAssignableFrom(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
public class Nested {
|
||||||
|
|
||||||
|
private String stringField = "nested string";
|
||||||
|
private Integer integerField = 42;
|
||||||
|
private String nullField = null;
|
||||||
|
|
||||||
|
public String getStringField() {
|
||||||
|
return stringField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStringField(String stringField) {
|
||||||
|
this.stringField = stringField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getIntegerField() {
|
||||||
|
return integerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIntegerField(Integer integerField) {
|
||||||
|
this.integerField = integerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNullField() {
|
||||||
|
return nullField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNullField(String nullField) {
|
||||||
|
this.nullField = nullField;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
import au.com.dius.pact.consumer.Pact;
|
||||||
|
import au.com.dius.pact.consumer.PactProviderRuleMk2;
|
||||||
|
import au.com.dius.pact.consumer.PactVerification;
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslJsonRootValue;
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
|
||||||
|
import au.com.dius.pact.model.RequestResponsePact;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(properties = {
|
||||||
|
// overriding provider address
|
||||||
|
"rootservice.ribbon.listOfServers: localhost:8888"
|
||||||
|
})
|
||||||
|
public class PactDslJsonBodyLikeMapperConsumerTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("testprovider", "localhost", 8888, this);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RootClient rootClient;
|
||||||
|
|
||||||
|
@Pact(state = "teststate", provider = "testprovider", consumer = "testclient")
|
||||||
|
public RequestResponsePact createPact(PactDslWithProvider builder) {
|
||||||
|
return builder
|
||||||
|
.given("teststate")
|
||||||
|
.uponReceiving("a POST request with a Root object")
|
||||||
|
.path("/root")
|
||||||
|
.method("POST")
|
||||||
|
// .body(PactDslJsonBodyLikeMapper.like(new PactDslJsonBodyLikeMapperTest.Root()))
|
||||||
|
.willRespondWith()
|
||||||
|
.status(201)
|
||||||
|
.matchHeader("Content-Type", "application/json")
|
||||||
|
.body(PactDslJsonBodyLikeMapper.like(new Root()))
|
||||||
|
.toPact();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pact(state = "teststate2", provider = "testprovider", consumer = "testclient")
|
||||||
|
public RequestResponsePact createPact2(PactDslWithProvider builder) {
|
||||||
|
return builder
|
||||||
|
.given("teststate2")
|
||||||
|
.uponReceiving("a POST request with a Root object")
|
||||||
|
.path("/root")
|
||||||
|
.method("POST")
|
||||||
|
.willRespondWith()
|
||||||
|
.status(201)
|
||||||
|
.matchHeader("Content-Type", "application/json")
|
||||||
|
.body(new PactDslJsonBody()
|
||||||
|
.eachLike("arrayField", PactDslJsonRootValue.numberType()))
|
||||||
|
.toPact();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@PactVerification(fragment = "createPact")
|
||||||
|
public void verifyPact() {
|
||||||
|
rootClient.createRoot(new Root());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@PactVerification(fragment = "createPact2")
|
||||||
|
public void verifyPact2() {
|
||||||
|
rootClient.createRoot(new Root());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
|
||||||
|
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class PactDslJsonBodyLikeMapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createsMatchersForAllFields() {
|
||||||
|
Root object = new Root();
|
||||||
|
PactDslJsonBody jsonBody = PactDslJsonBodyLikeMapper.like(object);
|
||||||
|
assertNoMatcher(jsonBody, ".nullField");
|
||||||
|
assertMatcherType(jsonBody, ".stringField", "type");
|
||||||
|
assertMatcherType(jsonBody, ".booleanField", "type");
|
||||||
|
assertMatcherType(jsonBody, ".primitiveBooleanField", "type");
|
||||||
|
assertMatcherType(jsonBody, ".integerField", "integer");
|
||||||
|
assertMatcherType(jsonBody, ".primitiveIntegerField", "integer");
|
||||||
|
assertMatcherType(jsonBody, ".doubleField", "decimal");
|
||||||
|
assertMatcherType(jsonBody, ".primitiveDoubleField", "decimal");
|
||||||
|
assertMatcherType(jsonBody, ".floatField", "decimal");
|
||||||
|
assertMatcherType(jsonBody, ".primitiveFloatField", "decimal");
|
||||||
|
assertMatcherType(jsonBody, ".bigDecimalField", "decimal");
|
||||||
|
assertMatcherType(jsonBody, ".numberField", "number");
|
||||||
|
assertMatcherType(jsonBody, ".nested.stringField", "type");
|
||||||
|
assertMatcherType(jsonBody, ".nested.integerField", "integer");
|
||||||
|
assertNoMatcher(jsonBody, ".nested.nullField");
|
||||||
|
assertMatcherType(jsonBody, ".complexListField[*].stringField", "type");
|
||||||
|
assertMatcherType(jsonBody, ".complexListField[*].integerField", "integer");
|
||||||
|
assertMatcherType(jsonBody, ".simpleListField[*]", "integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatcherType(PactDslJsonBody jsonBody, String fieldName, String expectedMatcher) {
|
||||||
|
assertMatcher(jsonBody, fieldName);
|
||||||
|
assertEquals(String.format("expected matcher for field '%s' to be of type '%s'", fieldName, expectedMatcher),
|
||||||
|
ImmutableMap.of("match", expectedMatcher),
|
||||||
|
jsonBody.getMatchers().getMatchingRules().get(fieldName).getRules().get(0).toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatcher(PactDslJsonBody jsonBody, String fieldName) {
|
||||||
|
assertNotNull(String.format("expected a matcher for field '%s'", fieldName),
|
||||||
|
jsonBody.getMatchers().getMatchingRules().get(fieldName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNoMatcher(PactDslJsonBody jsonBody, String fieldName) {
|
||||||
|
assertNull(jsonBody.getMatchers().getMatchingRules().get(fieldName));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Root {
|
||||||
|
|
||||||
|
private String nullField = null;
|
||||||
|
private String stringField = "string";
|
||||||
|
private Boolean booleanField = Boolean.TRUE;
|
||||||
|
private Integer integerField = 1;
|
||||||
|
private Double doubleField = 1d;
|
||||||
|
private Float floatField = 1f;
|
||||||
|
private BigDecimal bigDecimalField = BigDecimal.ONE;
|
||||||
|
private boolean primitiveBooleanField = true;
|
||||||
|
private int primitiveIntegerField = 1;
|
||||||
|
private double primitiveDoubleField = 1d;
|
||||||
|
private float primitiveFloatField = 1f;
|
||||||
|
private Number numberField = BigInteger.valueOf(1L);
|
||||||
|
private Nested nested = new Nested();
|
||||||
|
private List<Nested> complexListField = Arrays.asList(new Nested(), new Nested());
|
||||||
|
private List<Integer> simpleListField = Arrays.asList(1,2);
|
||||||
|
|
||||||
|
public String getNullField() {
|
||||||
|
return nullField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNullField(String nullField) {
|
||||||
|
this.nullField = nullField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStringField() {
|
||||||
|
return stringField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStringField(String stringField) {
|
||||||
|
this.stringField = stringField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getBooleanField() {
|
||||||
|
return booleanField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBooleanField(Boolean booleanField) {
|
||||||
|
this.booleanField = booleanField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getIntegerField() {
|
||||||
|
return integerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIntegerField(Integer integerField) {
|
||||||
|
this.integerField = integerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getDoubleField() {
|
||||||
|
return doubleField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDoubleField(Double doubleField) {
|
||||||
|
this.doubleField = doubleField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Float getFloatField() {
|
||||||
|
return floatField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFloatField(Float floatField) {
|
||||||
|
this.floatField = floatField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getBigDecimalField() {
|
||||||
|
return bigDecimalField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBigDecimalField(BigDecimal bigDecimalField) {
|
||||||
|
this.bigDecimalField = bigDecimalField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPrimitiveBooleanField() {
|
||||||
|
return primitiveBooleanField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrimitiveBooleanField(boolean primitiveBooleanField) {
|
||||||
|
this.primitiveBooleanField = primitiveBooleanField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPrimitiveIntegerField() {
|
||||||
|
return primitiveIntegerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrimitiveIntegerField(int primitiveIntegerField) {
|
||||||
|
this.primitiveIntegerField = primitiveIntegerField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPrimitiveDoubleField() {
|
||||||
|
return primitiveDoubleField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrimitiveDoubleField(double primitiveDoubleField) {
|
||||||
|
this.primitiveDoubleField = primitiveDoubleField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPrimitiveFloatField() {
|
||||||
|
return primitiveFloatField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrimitiveFloatField(float primitiveFloatField) {
|
||||||
|
this.primitiveFloatField = primitiveFloatField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Number getNumberField() {
|
||||||
|
return numberField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNumberField(Number numberField) {
|
||||||
|
this.numberField = numberField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Nested getNested() {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNested(Nested nested) {
|
||||||
|
this.nested = nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Nested> getComplexListField() {
|
||||||
|
return complexListField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComplexListField(List<Nested> complexListField) {
|
||||||
|
this.complexListField = complexListField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getSimpleListField() {
|
||||||
|
return simpleListField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSimpleListField(List<Integer> simpleListField) {
|
||||||
|
this.simpleListField = simpleListField;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package io.reflectoring.dsl;
|
||||||
|
|
||||||
|
import org.springframework.cloud.netflix.feign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
|
@FeignClient(name = "rootservice")
|
||||||
|
public interface RootClient {
|
||||||
|
|
||||||
|
@RequestMapping(method = RequestMethod.POST, path = "/root")
|
||||||
|
Root createRoot(@RequestBody Root root);
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user