From 23202a30c41448d317a34ef87210bc236030ff89 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Fri, 6 Jan 2017 20:14:23 +0100 Subject: [PATCH] Add unit test for sone template page and fix parameter encoding --- .../pterodactylus/sone/web/SoneTemplatePage.java | 19 +- .../net/pterodactylus/sone/web/WebPageTest.java | 35 ++- .../pterodactylus/sone/web/SoneTemplatePageTest.kt | 276 +++++++++++++++++++++ 3 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt diff --git a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java index 35c39b6..6278321 100644 --- a/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java +++ b/src/main/java/net/pterodactylus/sone/web/SoneTemplatePage.java @@ -272,24 +272,29 @@ public class SoneTemplatePage extends FreenetTemplatePage { StringBuilder requestParameters = new StringBuilder(); for (String parameterName : httpRequest.getParameterNames()) { if (requestParameters.length() > 0) { - requestParameters.append("%26"); + requestParameters.append("&"); } String[] parameterValues = httpRequest.getMultipleParam(parameterName); for (String parameterValue : parameterValues) { - try { - requestParameters.append(URLEncoder.encode(parameterName, "UTF-8")).append("%3d").append(URLEncoder.encode(parameterValue, "UTF-8")); - } catch (UnsupportedEncodingException uee1) { - /* A JVM without UTF-8? I don’t think so. */ - } + requestParameters.append(urlEncode(parameterName)).append("=").append(urlEncode(parameterValue)); } } originalUrl += "?" + requestParameters.toString(); } - return "login.html?target=" + originalUrl; + return "login.html?target=" + urlEncode(originalUrl); } return null; } + private static String urlEncode(String value) { + try { + return URLEncoder.encode(value, "UTF-8"); + } catch (UnsupportedEncodingException uee1) { + /* A JVM without UTF-8? I don’t think so. */ + throw new RuntimeException(uee1); + } + } + /** * {@inheritDoc} */ diff --git a/src/test/java/net/pterodactylus/sone/web/WebPageTest.java b/src/test/java/net/pterodactylus/sone/web/WebPageTest.java index c79f676..df000b8 100644 --- a/src/test/java/net/pterodactylus/sone/web/WebPageTest.java +++ b/src/test/java/net/pterodactylus/sone/web/WebPageTest.java @@ -46,11 +46,14 @@ import net.pterodactylus.util.web.Method; import net.pterodactylus.util.web.Response; import freenet.clients.http.ToadletContext; +import freenet.l10n.BaseL10n; import freenet.support.api.HTTPRequest; import com.google.common.base.Optional; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; import com.google.common.eventbus.EventBus; import com.google.common.io.ByteStreams; import org.junit.Before; @@ -74,12 +77,13 @@ public abstract class WebPageTest { protected final WebInterface webInterface = mock(WebInterface.class, RETURNS_DEEP_STUBS); protected final EventBus eventBus = mock(EventBus.class); protected final Core core = webInterface.getCore(); + protected final BaseL10n l10n = webInterface.getL10n(); protected final Sone currentSone = mock(Sone.class); protected final TemplateContext templateContext = new TemplateContext(); protected final HTTPRequest httpRequest = mock(HTTPRequest.class); - protected final Map requestParameters = new HashMap<>(); + protected final Multimap requestParameters = HashMultimap.create(); protected final Map requestHeaders = new HashMap<>(); protected final FreenetRequest freenetRequest = mock(FreenetRequest.class); private final PipedOutputStream responseOutputStream = new PipedOutputStream(); @@ -104,32 +108,52 @@ public abstract class WebPageTest { public final void setupFreenetRequest() { when(freenetRequest.getToadletContext()).thenReturn(toadletContext); when(freenetRequest.getHttpRequest()).thenReturn(httpRequest); + when(httpRequest.getMultipleParam(anyString())).thenAnswer(new Answer() { + @Override + public String[] answer(InvocationOnMock invocation) throws Throwable { + return requestParameters.get(invocation.getArgument(0)).toArray(new String[0]); + } + }); when(httpRequest.getPartAsStringFailsafe(anyString(), anyInt())).thenAnswer(new Answer() { @Override public String answer(InvocationOnMock invocation) throws Throwable { String parameter = invocation.getArgument(0); int maxLength = invocation.getArgument(1); - return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).substring(0, Math.min(maxLength, requestParameters.get(parameter).length())) : ""; + Collection values = requestParameters.get(parameter); + return requestParameters.containsKey(parameter) ? values.iterator().next().substring(0, Math.min(maxLength, values.iterator().next().length())) : ""; + } + }); + when(httpRequest.hasParameters()).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return !requestParameters.isEmpty(); + } + }); + when(httpRequest.getParameterNames()).thenAnswer(new Answer>() { + @Override + public Collection answer(InvocationOnMock invocation) throws Throwable { + return requestParameters.keySet(); } }); when(httpRequest.getParam(anyString())).thenAnswer(new Answer() { @Override public String answer(InvocationOnMock invocation) throws Throwable { String parameter = invocation.getArgument(0); - return requestParameters.containsKey(parameter) ? requestParameters.get(parameter) : ""; + return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : ""; } }); when(httpRequest.getParam(anyString(), ArgumentMatchers.any())).thenAnswer(new Answer() { @Override public String answer(InvocationOnMock invocation) throws Throwable { String parameter = invocation.getArgument(0); - return requestParameters.containsKey(parameter) ? requestParameters.get(parameter) : invocation.getArgument(1); + return requestParameters.containsKey(parameter) ? requestParameters.get(parameter).iterator().next() : invocation.getArgument(1); } }); when(httpRequest.isPartSet(anyString())).thenAnswer(new Answer() { @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { - return requestParameters.get(invocation.getArgument(0)) != null; + return requestParameters.containsKey(invocation.getArgument(0)) && + requestParameters.get(invocation.getArgument(0)).iterator().next() != null; } }); when(httpRequest.getParts()).thenAnswer(new Answer() { @@ -208,6 +232,7 @@ public abstract class WebPageTest { protected void request(String uri, Method method) { try { + when(httpRequest.getPath()).thenReturn(uri); when(freenetRequest.getUri()).thenReturn(new URI(uri)); } catch (URISyntaxException e) { throw new RuntimeException(e); diff --git a/src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt b/src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt new file mode 100644 index 0000000..c16fdef --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/sone/web/SoneTemplatePageTest.kt @@ -0,0 +1,276 @@ +package net.pterodactylus.sone.web + +import net.pterodactylus.sone.data.Sone +import net.pterodactylus.sone.main.SonePlugin +import net.pterodactylus.sone.test.mock +import net.pterodactylus.sone.test.whenever +import net.pterodactylus.sone.web.page.FreenetRequest +import net.pterodactylus.util.notify.Notification +import net.pterodactylus.util.template.TemplateContext +import net.pterodactylus.util.version.Version +import net.pterodactylus.util.web.Method.GET +import org.hamcrest.Matcher +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.anyOf +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.containsInAnyOrder +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.nullValue +import org.junit.Test +import org.mockito.Mockito.verify + +/** + * Unit test for [SoneTemplatePage]. + */ +class SoneTemplatePageTest : WebPageTest() { + + // @get:JvmName("getPage1") + private val page = object : SoneTemplatePage("path.html", template, webInterface, true) {} + + @Test + fun `current sone is retrieved from web interface`() { + assertThat(page.getCurrentSone(toadletContext), equalTo(currentSone)) + } + + @Test + fun `retrieving current sone with creation is forwarded to web interface`() { + mock().let { + whenever(webInterface.getCurrentSone(toadletContext, true)).thenReturn(it) + assertThat(page.getCurrentSone(toadletContext, true), equalTo(it)) + } + } + + @Test + fun `retrieving current sone without creation is forwarded to web interface`() { + mock().let { + whenever(webInterface.getCurrentSone(toadletContext, false)).thenReturn(it) + assertThat(page.getCurrentSone(toadletContext, false), equalTo(it)) + } + } + + @Test + fun `setting the current sone is forwarded to web interface`() { + mock().let { + page.setCurrentSone(toadletContext, it) + verify(webInterface).setCurrentSone(toadletContext, it) + } + } + + @Test + fun `page title is empty string if no page title key was given`() { + SoneTemplatePage("path.html", template, null, webInterface).let { page -> + assertThat(page.getPageTitle(freenetRequest), equalTo("")) + } + } + + @Test + fun `page title is retrieved from l10n if page title key is given`() { + SoneTemplatePage("path.html", template, "page.title", webInterface).let { page -> + whenever(l10n.getString("page.title")).thenReturn("Page Title") + assertThat(page.getPageTitle(freenetRequest), equalTo("Page Title")) + } + } + + @Test + fun `additional link nodes contain open search link`() { + addHttpRequestHeader("Host", "www.example.com") + assertThat(page.getAdditionalLinkNodes(freenetRequest), contains(mapOf( + "rel" to "search", + "type" to "application/opensearchdescription+xml", + "title" to "Sone", + "href" to "http://www.example.com/Sone/OpenSearch.xml" + ))) + } + + @Test + fun `style sheets contains sone CSS file`() { + assertThat(page.styleSheets, contains("css/sone.css")) + } + + @Test + fun `shortcut icon is the sone icon`() { + assertThat(page.shortcutIcon, equalTo("images/icon.png")) + } + + @Test + fun `page requires login if require login was specified in the constructor`() { + SoneTemplatePage("path.html", template, webInterface, true).let { page -> + assertThat(page.requiresLogin(), equalTo(true)) + } + } + + @Test + fun `page does not require login if require login was not specified in the constructor`() { + SoneTemplatePage("path.html", template, webInterface, false).let { page -> + assertThat(page.requiresLogin(), equalTo(false)) + } + } + + private fun verifyVariableIsSet(name: String, value: Any) = verifyVariableMatches(name, equalTo(value)) + + private fun verifyVariableMatches(name: String, matcher: Matcher) { + page.processTemplate(freenetRequest, templateContext) + @Suppress("UNCHECKED_CAST") + assertThat(templateContext[name] as T, matcher) + } + + @Test + fun `core is set in template context`() { + verifyVariableIsSet("core", core) + } + + @Test + fun `current sone is set in template context`() { + verifyVariableIsSet("currentSone", currentSone) + } + + @Test + fun `local sones are set in template context`() { + val localSones = listOf(mock(), mock()) + whenever(core.localSones).thenReturn(localSones) + verifyVariableMatches("localSones", containsInAnyOrder(*localSones.toTypedArray())) + } + + @Test + fun `freenet request is set in template context`() { + verifyVariableIsSet("request", freenetRequest) + } + + @Test + fun `current version is set in template context`() { + verifyVariableIsSet("currentVersion", SonePlugin.getPluginVersion()) + } + + @Test + fun `has latest version is set correctly in template context if true`() { + whenever(core.updateChecker.hasLatestVersion()).thenReturn(true) + verifyVariableIsSet("hasLatestVersion", true) + } + + @Test + fun `has latest version is set correctly in template context if false`() { + whenever(core.updateChecker.hasLatestVersion()).thenReturn(false) + verifyVariableIsSet("hasLatestVersion", false) + } + + @Test + fun `latest edition is set in template context`() { + whenever(core.updateChecker.latestEdition).thenReturn(1234L) + verifyVariableIsSet("latestEdition", 1234L) + } + + @Test + fun `latest version is set in template context`() { + whenever(core.updateChecker.latestVersion).thenReturn(Version(1, 2, 3)) + verifyVariableIsSet("latestVersion", Version(1, 2, 3)) + } + + @Test + fun `latest version time is set in template context`() { + whenever(core.updateChecker.latestVersionDate).thenReturn(12345L) + verifyVariableIsSet("latestVersionTime", 12345L) + } + + private fun createNotification(time: Long) = mock().apply { + whenever(createdTime).thenReturn(time) + } + + @Test + fun `notifications are set in template context`() { + val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000)) + whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications) + verifyVariableMatches("notifications", contains(notifications[1], notifications[2], notifications[0])) + } + + @Test + fun `notification hash is set in template context`() { + val notifications = listOf(createNotification(3000), createNotification(1000), createNotification(2000)) + whenever(webInterface.getNotifications(currentSone)).thenReturn(notifications) + verifyVariableIsSet("notificationHash", listOf(notifications[1], notifications[2], notifications[0]).hashCode()) + } + + @Test + fun `handleRequest method is called`() { + var called = false + val page = object : SoneTemplatePage("path.html", template, webInterface, true) { + override fun handleRequest(request: FreenetRequest, templateContext: TemplateContext) { + called = true + } + } + page.processTemplate(freenetRequest, templateContext) + assertThat(called, equalTo(true)) + } + + @Test + fun `redirect does not happen if login is not required`() { + val page = SoneTemplatePage("page.html", template, webInterface, false) + assertThat(page.getRedirectTarget(freenetRequest), nullValue()) + } + + @Test + fun `redirect does not happen if sone is logged in`() { + assertThat(page.getRedirectTarget(freenetRequest), nullValue()) + } + + @Test + fun `redirect does happen if sone is not logged in`() { + unsetCurrentSone() + request("index.html", GET) + assertThat(page.getRedirectTarget(freenetRequest), equalTo("login.html?target=index.html")) + } + + @Test + fun `redirect does happen with parameters encoded correctly if sone is not logged in`() { + unsetCurrentSone() + request("index.html", GET) + addHttpRequestParameter("foo", "b=r") + addHttpRequestParameter("baz", "q&o") + assertThat(page.getRedirectTarget(freenetRequest), anyOf( + equalTo("login.html?target=index.html%3Ffoo%3Db%253Dr%26baz%3Dq%2526o"), + equalTo("login.html?target=index.html%3Fbaz%3Dq%2526o%26foo%3Db%253Dr") + )) + } + + @Test + fun `full access requirement is correctly forwarded from the preferences if false`() { + assertThat(page.isFullAccessOnly, equalTo(false)) + } + + @Test + fun `full access requirement is correctly forwarded from the preferences if true`() { + core.preferences.isRequireFullAccess = true + assertThat(page.isFullAccessOnly, equalTo(true)) + } + + @Test + fun `page is disabled if full access is required but request does not have full access`() { + core.preferences.isRequireFullAccess = true + assertThat(page.isEnabled(toadletContext), equalTo(false)) + } + + @Test + fun `page is disabled if login is required but there is no current sone`() { + unsetCurrentSone() + assertThat(page.isEnabled(toadletContext), equalTo(false)) + } + + @Test + fun `page is enabled if login is required and there is a current sone`() { + assertThat(page.isEnabled(toadletContext), equalTo(true)) + } + + @Test + fun `page is enabled if full access is required and request has full access and login is required and there is a current sone`() { + core.preferences.isRequireFullAccess = true + whenever(toadletContext.isAllowedFullAccess).thenReturn(true) + assertThat(page.isEnabled(toadletContext), equalTo(true)) + } + + @Test + fun `page is enabled if no full access is required and login is not required`() { + SoneTemplatePage("path.html", template, webInterface, false).let { page -> + assertThat(page.isEnabled(toadletContext), equalTo(true)) + } + } + +} -- 2.7.4