SPR-6464 Use the redirect URL and its request parameters (if any), instead of a synthetic key, to match a FlashMap instance to its intended recepient request. This should help to prevent conflicts in most cases transparently. Only if necessary a controller can add extra request parameters to distinguish the request even more uniquely.
This commit is contained in:
@@ -133,7 +133,6 @@ import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.multipart.support.StringMultipartFileEditor;
|
||||
import org.springframework.web.servlet.FlashMap;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
@@ -1479,21 +1478,20 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
|
||||
response = new MockHttpServletResponse();
|
||||
getServlet().service(request, response);
|
||||
|
||||
FlashMap flashMap = RequestContextUtils.getFlashMap(request);
|
||||
|
||||
assertNotNull(flashMap);
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals("/messages/1?name=value&_flashKey=" + flashMap.getKey(), response.getRedirectedUrl());
|
||||
assertEquals("/messages/1?name=value", response.getRedirectedUrl());
|
||||
assertEquals("yay!", RequestContextUtils.getFlashMap(request).get("successMessage"));
|
||||
|
||||
// GET after POST
|
||||
request = new MockHttpServletRequest("GET", "/messages/1");
|
||||
request.addParameter("name", "value");
|
||||
request.setSession(session);
|
||||
request.setParameter("_flashKey", String.valueOf(flashMap.getKey()));
|
||||
response = new MockHttpServletResponse();
|
||||
getServlet().service(request, response);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals("Got: yay!", response.getContentAsString());
|
||||
assertTrue(RequestContextUtils.getFlashMap(request).isEmpty());
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -23,14 +23,14 @@ import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.servlet.FlashMap;
|
||||
import org.springframework.web.servlet.FlashMapManager;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link DefaultFlashMapManager} tests.
|
||||
@@ -50,125 +50,102 @@ public class DefaultFlashMapManagerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAlreadyStarted() {
|
||||
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, new FlashMap());
|
||||
boolean actual = this.flashMapManager.requestStarted(this.request);
|
||||
public void requestStarted() {
|
||||
boolean initialized = this.flashMapManager.requestStarted(this.request);
|
||||
|
||||
assertTrue("Current FlashMap not initialized on first call", initialized);
|
||||
assertNotNull("Current FlashMap not found", RequestContextUtils.getFlashMap(request));
|
||||
|
||||
assertFalse(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFlashMap() {
|
||||
boolean actual = this.flashMapManager.requestStarted(this.request);
|
||||
FlashMap flashMap = RequestContextUtils.getFlashMap(this.request);
|
||||
|
||||
assertTrue(actual);
|
||||
assertNotNull(flashMap);
|
||||
assertNotNull(flashMap.getKey());
|
||||
assertEquals("_flashKey", flashMap.getKeyParameterName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createFlashMapWithoutKey() {
|
||||
this.flashMapManager.setUseUniqueFlashKey(false);
|
||||
boolean actual = this.flashMapManager.requestStarted(this.request);
|
||||
FlashMap flashMap = RequestContextUtils.getFlashMap(this.request);
|
||||
|
||||
assertTrue(actual);
|
||||
assertNotNull(flashMap);
|
||||
assertNull(flashMap.getKey());
|
||||
assertNull(flashMap.getKeyParameterName());
|
||||
initialized = this.flashMapManager.requestStarted(this.request);
|
||||
|
||||
assertFalse("Current FlashMap initialized twice", initialized);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lookupPreviousFlashMap() {
|
||||
FlashMap flashMap = new FlashMap("key", "_flashKey");
|
||||
flashMap.put("name", "value");
|
||||
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
|
||||
allFlashMaps.put(flashMap.getKey(), flashMap);
|
||||
|
||||
this.request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
|
||||
this.request.addParameter("_flashKey", flashMap.getKey());
|
||||
FlashMap flashMap = new FlashMap();
|
||||
|
||||
List<FlashMap> allMaps = createFlashMapsSessionAttribute();
|
||||
allMaps.add(flashMap);
|
||||
|
||||
this.flashMapManager.requestStarted(this.request);
|
||||
|
||||
assertSame(flashMap, request.getAttribute(DefaultFlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE));
|
||||
assertEquals("value", request.getAttribute("name"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void lookupPreviousFlashMapWithoutKey() {
|
||||
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
|
||||
request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
|
||||
public void lookupPreviousFlashMapExpectedUrlPath() {
|
||||
FlashMap emptyFlashMap = new FlashMap();
|
||||
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.put("name", "value");
|
||||
allFlashMaps.put("key", flashMap);
|
||||
FlashMap oneFlashMap = new FlashMap();
|
||||
oneFlashMap.setExpectedUrlPath(null, "/one");
|
||||
|
||||
FlashMap oneOtherFlashMap = new FlashMap();
|
||||
oneOtherFlashMap.setExpectedUrlPath(null, "/one/other");
|
||||
|
||||
this.flashMapManager.setUseUniqueFlashKey(false);
|
||||
List<FlashMap> allMaps = createFlashMapsSessionAttribute();
|
||||
allMaps.add(emptyFlashMap);
|
||||
allMaps.add(oneFlashMap);
|
||||
allMaps.add(oneOtherFlashMap);
|
||||
Collections.shuffle(allMaps);
|
||||
|
||||
this.request.setRequestURI("/one");
|
||||
this.flashMapManager.requestStarted(this.request);
|
||||
|
||||
assertSame(flashMap, this.request.getAttribute(DefaultFlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE));
|
||||
assertEquals("value", this.request.getAttribute("name"));
|
||||
assertSame(oneFlashMap, request.getAttribute(DefaultFlashMapManager.PREVIOUS_FLASH_MAP_ATTRIBUTE));
|
||||
}
|
||||
|
||||
@SuppressWarnings("static-access")
|
||||
@Test
|
||||
public void removeExpired() throws InterruptedException {
|
||||
FlashMap[] flashMapArray = new FlashMap[5];
|
||||
flashMapArray[0] = new FlashMap("key0", "_flashKey");
|
||||
flashMapArray[1] = new FlashMap("key1", "_flashKey");
|
||||
flashMapArray[2] = new FlashMap("key2", "_flashKey");
|
||||
flashMapArray[3] = new FlashMap("key3", "_flashKey");
|
||||
flashMapArray[4] = new FlashMap("key4", "_flashKey");
|
||||
|
||||
Map<String, FlashMap> allFlashMaps = new HashMap<String, FlashMap>();
|
||||
for (FlashMap flashMap : flashMapArray) {
|
||||
allFlashMaps.put(flashMap.getKey(), flashMap);
|
||||
public void removeExpiredFlashMaps() throws InterruptedException {
|
||||
List<FlashMap> allMaps = createFlashMapsSessionAttribute();
|
||||
for (int i=0; i < 5; i++) {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
allMaps.add(flashMap);
|
||||
flashMap.startExpirationPeriod(0);
|
||||
}
|
||||
|
||||
flashMapArray[1].startExpirationPeriod(0);
|
||||
flashMapArray[3].startExpirationPeriod(0);
|
||||
|
||||
Thread.currentThread().sleep(5);
|
||||
Thread.sleep(5);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.getSession().setAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE, allFlashMaps);
|
||||
request.setParameter("_flashKey", "key0");
|
||||
this.flashMapManager.requestStarted(request);
|
||||
this.flashMapManager.requestStarted(this.request);
|
||||
|
||||
assertEquals(2, allFlashMaps.size());
|
||||
assertNotNull(allFlashMaps.get("key2"));
|
||||
assertNotNull(allFlashMaps.get("key4"));
|
||||
assertEquals(0, allMaps.size());
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "static-access" })
|
||||
@Test
|
||||
public void saveFlashMap() throws InterruptedException {
|
||||
FlashMap flashMap = new FlashMap("key", "_flashKey");
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.put("name", "value");
|
||||
request.setAttribute(DefaultFlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
|
||||
|
||||
this.flashMapManager.setFlashMapTimeout(0);
|
||||
this.flashMapManager.requestCompleted(this.request);
|
||||
|
||||
Thread.currentThread().sleep(1);
|
||||
Thread.sleep(1);
|
||||
|
||||
String sessionKey = DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE;
|
||||
Map<String, FlashMap> allFlashMaps = (Map<String, FlashMap>) this.request.getSession().getAttribute(sessionKey);
|
||||
List<FlashMap> allMaps = getFlashMapsSessionAttribute();
|
||||
|
||||
assertSame(flashMap, allFlashMaps.get("key"));
|
||||
assertNotNull(allMaps);
|
||||
assertSame(flashMap, allMaps.get(0));
|
||||
assertTrue(flashMap.isExpired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveEmptyFlashMap() throws InterruptedException {
|
||||
FlashMap flashMap = new FlashMap("key", "_flashKey");
|
||||
request.setAttribute(DefaultFlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
|
||||
|
||||
this.flashMapManager.setFlashMapTimeout(0);
|
||||
public void saveFlashMapIsEmpty() throws InterruptedException {
|
||||
request.setAttribute(DefaultFlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, new FlashMap());
|
||||
this.flashMapManager.requestCompleted(this.request);
|
||||
|
||||
assertNull(this.request.getSession().getAttribute(DefaultFlashMapManager.FLASH_MAPS_SESSION_ATTRIBUTE));
|
||||
assertNull(getFlashMapsSessionAttribute());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<FlashMap> getFlashMapsSessionAttribute() {
|
||||
return (List<FlashMap>) this.request.getSession().getAttribute(DefaultFlashMapManager.class + ".FLASH_MAPS");
|
||||
}
|
||||
|
||||
private List<FlashMap> createFlashMapsSessionAttribute() {
|
||||
List<FlashMap> allMaps = new CopyOnWriteArrayList<FlashMap>();
|
||||
this.request.getSession().setAttribute(DefaultFlashMapManager.class + ".FLASH_MAPS", allMaps);
|
||||
return allMaps;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.support;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.servlet.FlashMap;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link FlashMap} tests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class FlashMapTests {
|
||||
|
||||
@Test
|
||||
public void matchAnyUrlPath() {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchUrlPath() {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.setExpectedUrlPath(null, "/yes");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes/")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/yes/but")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/no")));
|
||||
|
||||
flashMap.setExpectedUrlPath(null, "/thats it?");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/thats it")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/thats%20it")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchRelativeUrlPath() {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest("GET", "/oh/no"), "yes");
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/oh/yes")));
|
||||
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest("GET", "/oh/not/again"), "../ok");
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/oh/ok")));
|
||||
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest("GET", "/yes/it/is"), "..");
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest("GET", "/yes/it/is"), "../");
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest("GET", "/thats it/really"), "./");
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/thats%20it")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchAbsoluteUrlPath() {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.setExpectedUrlPath(new MockHttpServletRequest(), "http://example.com");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/no")));
|
||||
|
||||
flashMap.setExpectedUrlPath(null, "http://example.com/");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/no")));
|
||||
|
||||
flashMap.setExpectedUrlPath(null, "http://example.com/yes");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes/")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/no")));
|
||||
assertFalse(flashMap.matches(new MockHttpServletRequest("GET", "/yes/no")));
|
||||
|
||||
flashMap.setExpectedUrlPath(null, "http://example.com/yes?a=1");
|
||||
|
||||
assertTrue(flashMap.matches(new MockHttpServletRequest("GET", "/yes")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchExpectedRequestParameter() {
|
||||
String parameterName = "numero";
|
||||
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.setExpectedRequestParameters(new ModelMap(parameterName, "uno"));
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
assertFalse(flashMap.matches(request));
|
||||
|
||||
request.setParameter(parameterName, "uno");
|
||||
assertTrue(flashMap.matches(request));
|
||||
|
||||
request.setParameter(parameterName, "dos");
|
||||
assertFalse(flashMap.matches(request));
|
||||
|
||||
request.setParameter(parameterName, (String) null);
|
||||
assertFalse(flashMap.matches(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isExpired() throws InterruptedException {
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.startExpirationPeriod(0);
|
||||
|
||||
Thread.sleep(1);
|
||||
|
||||
assertTrue(flashMap.isExpired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notExpired() throws InterruptedException {
|
||||
assertFalse(new FlashMap().isExpired());
|
||||
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.startExpirationPeriod(10);
|
||||
Thread.sleep(100);
|
||||
|
||||
assertFalse(flashMap.isExpired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void compareTo() {
|
||||
FlashMap flashMap1 = new FlashMap();
|
||||
FlashMap flashMap2 = new FlashMap();
|
||||
assertEquals(0, flashMap1.compareTo(flashMap2));
|
||||
|
||||
flashMap1.setExpectedUrlPath(null, "/path1");
|
||||
assertEquals(-1, flashMap1.compareTo(flashMap2));
|
||||
assertEquals(1, flashMap2.compareTo(flashMap1));
|
||||
|
||||
flashMap2.setExpectedUrlPath(null, "/path2");
|
||||
assertEquals(0, flashMap1.compareTo(flashMap2));
|
||||
|
||||
flashMap1.setExpectedRequestParameters(new ModelMap("id", "1"));
|
||||
assertEquals(-1, flashMap1.compareTo(flashMap2));
|
||||
assertEquals(1, flashMap2.compareTo(flashMap1));
|
||||
|
||||
flashMap2.setExpectedRequestParameters(new ModelMap("id", "2"));
|
||||
assertEquals(0, flashMap1.compareTo(flashMap2));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2010 the original author or authors.
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,6 +32,7 @@ import org.springframework.beans.TestBean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.servlet.FlashMap;
|
||||
import org.springframework.web.servlet.FlashMapManager;
|
||||
import org.springframework.web.servlet.View;
|
||||
@@ -96,30 +97,20 @@ public class RedirectViewTests {
|
||||
@Test
|
||||
public void flashMap() throws Exception {
|
||||
RedirectView rv = new RedirectView();
|
||||
rv.setUrl("http://url.somewhere.com");
|
||||
rv.setUrl("http://url.somewhere.com/path");
|
||||
rv.setHttp10Compatible(false);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FlashMap flashMap = new FlashMap("key", "_flashKey");
|
||||
flashMap.put("name", "value");
|
||||
FlashMap flashMap = new FlashMap();
|
||||
flashMap.put("successMessage", "yay!");
|
||||
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
|
||||
rv.render(new HashMap<String, Object>(), request, response);
|
||||
rv.render(new ModelMap("id", "1"), request, response);
|
||||
assertEquals(303, response.getStatus());
|
||||
assertEquals("http://url.somewhere.com?_flashKey=key", response.getHeader("Location"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyFlashMap() throws Exception {
|
||||
RedirectView rv = new RedirectView();
|
||||
rv.setUrl("http://url.somewhere.com");
|
||||
rv.setHttp10Compatible(false);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FlashMap flashMap = new FlashMap("key", "_flashKey");
|
||||
request.setAttribute(FlashMapManager.CURRENT_FLASH_MAP_ATTRIBUTE, flashMap);
|
||||
rv.render(new HashMap<String, Object>(), request, response);
|
||||
assertEquals(303, response.getStatus());
|
||||
assertEquals("http://url.somewhere.com", response.getHeader("Location"));
|
||||
assertEquals("http://url.somewhere.com/path?id=1", response.getHeader("Location"));
|
||||
|
||||
MockHttpServletRequest nextRequest = new MockHttpServletRequest("GET", "/path");
|
||||
nextRequest.addParameter("id", "1");
|
||||
assertTrue(flashMap.matches(nextRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user