/* * Copyright (C) 2008-2017, Juick * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package com.juick.www; import com.gargoylesoftware.htmlunit.CookieManager; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.css.StyleElement; import com.gargoylesoftware.htmlunit.html.DomElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.juick.Message; import com.juick.Tag; import com.juick.User; import com.juick.configuration.DataConfiguration; import com.juick.configuration.RepositoryConfiguration; import com.juick.service.ImagesService; import com.juick.service.MessagesService; import com.juick.service.MockImagesService; import com.juick.service.PrivacyQueriesService; import com.juick.service.UserService; import com.juick.util.MessageUtils; import com.juick.www.configuration.SapeConfiguration; import com.juick.www.configuration.WebSecurityConfig; import com.juick.www.configuration.WwwAppConfiguration; import com.juick.www.configuration.WwwServletConfiguration; import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.template.PebbleTemplate; import org.apache.commons.text.StringEscapeUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.FileSystemUtils; import org.springframework.web.context.WebApplicationContext; import javax.inject.Inject; import javax.servlet.http.Cookie; import java.io.FileInputStream; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Collections; import java.util.stream.IntStream; import java.util.stream.StreamSupport; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * Created by vitalyster on 12.01.2017. */ @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(classes = { DataConfiguration.class, WwwServletConfiguration.class, WwwAppConfiguration.class, SapeConfiguration.class, RepositoryConfiguration.class, WebSecurityConfig.class }) public class WebAppTests { @Configuration @ComponentScan(basePackages = "com.juick.www.controllers") static class Config { @Bean public ImagesService imagesService() { return new MockImagesService(); } } @Inject private WebApplicationContext wac; @Inject private WebApp webApp; private static MockMvc mockMvc; private static WebClient webClient; @Inject private UserService userService; @Inject private MessagesService messagesService; @Inject private PrivacyQueriesService privacyQueriesService; @Inject private JdbcTemplate jdbcTemplate; @Inject private PebbleEngine pebbleEngine; @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String imgPath; private static User ugnich, freefd; private static String ugnichName, ugnichPassword, freefdName, freefdPassword; private static boolean isSetUp = false; @Before public void setup() throws IOException { if (!isSetUp) { mockMvc = MockMvcBuilders.webAppContextSetup(wac) .apply(springSecurity()) .build(); webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build(); webClient.getOptions().setJavaScriptEnabled(false); webClient.getOptions().setCssEnabled(false); webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); ugnichName = "ugnich"; ugnichPassword = "MyPassw0rd!"; freefdName = "freefd"; freefdPassword = "MyPassw0rd!"; int ugnichId = userService.createUser(ugnichName, ugnichPassword); ugnich = userService.getUserByUID(ugnichId).orElseThrow(IllegalStateException::new); int freefdId = userService.createUser(freefdName, freefdPassword); freefd = userService.getUserByUID(freefdId).orElseThrow(IllegalStateException::new); isSetUp = true; } Files.createDirectory(Paths.get(imgPath, "p")); Files.createDirectory(Paths.get(imgPath, "photos-1024")); Files.createDirectory(Paths.get(imgPath, "photos-512")); Files.createDirectory(Paths.get(imgPath, "ps")); } @After public void teardown() throws IOException { FileSystemUtils.deleteRecursively(Paths.get(imgPath, "p")); FileSystemUtils.deleteRecursively(Paths.get(imgPath, "photos-1024")); FileSystemUtils.deleteRecursively(Paths.get(imgPath, "photos-512")); FileSystemUtils.deleteRecursively(Paths.get(imgPath, "ps")); } @Test public void postWithoutTagsShouldNotHaveAsteriskInTitle() throws Exception { String msgText = "Привет, я - Угнич"; int mid = messagesService.createMessage(ugnich.getUid(), msgText, null, null); HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); assertThat(threadPage.getTitleText(), equalTo("ugnich:")); } @Test public void bannedUserBlogandPostShouldReturn404() throws IOException { String userName = "isilmine"; String userPassword = "secret"; String msgText = "автор этого поста был забанен"; String hash = "12345678"; User isilmine = userService.getUserByUID(userService.createUser(userName, userPassword)).orElseThrow(IllegalStateException::new); int mid = messagesService.createMessage(isilmine.getUid(), msgText, null, null); jdbcTemplate.update("UPDATE users SET banned=1 WHERE id=?", isilmine.getUid()); Page blogPage = webClient.getPage("http://localhost:8080/isilmine"); Page threadPage = webClient.getPage(String.format("http://localhost:8080/isilmine/%d", mid)); assertThat(blogPage.getWebResponse().getStatusCode(), equalTo(404)); assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(404)); } @Test public void repliesTree() throws IOException { int mid = messagesService.createMessage(ugnich.getUid(), "hello", null, null); IntStream.range(1, 15).forEach(i -> messagesService.createReply(mid, i-1, freefd.getUid(), String.valueOf(i-1), null )); HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); Long visibleItems = StreamSupport.stream(threadPage.getHtmlElementById("replies") .getChildElements().spliterator(), false).filter(e -> { StyleElement display = e.getStyleElement("display"); return display == null || !display.getValue().equals("none"); }).count(); assertThat(visibleItems, equalTo(1L)); } @Test public void userShouldNotSeeReplyButtonToBannedUser() throws Exception { int mid = messagesService.createMessage(ugnich.getUid(), "freefd bl me", null, null); messagesService.createReply(mid, 0, ugnich.getUid(), "yo", null); MvcResult loginResult = mockMvc.perform(post("/login") .param("username", freefdName) .param("password", freefdPassword)).andReturn(); Cookie loginCookie = loginResult.getResponse().getCookie("juick-remember-me"); webClient.setCookieManager(new CookieManager()); webClient.getCookieManager().addCookie( new com.gargoylesoftware.htmlunit.util.Cookie(loginCookie.getDomain(), loginCookie.getName(), loginCookie.getValue())); HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); assertThat(threadPage.querySelectorAll(".msg-comment-target").isEmpty(), equalTo(false)); assertThat(threadPage.querySelectorAll(".a-thread-comment").isEmpty(), equalTo(false)); privacyQueriesService.blacklistUser(freefd, ugnich); assertThat(userService.isInBLAny(freefd.getUid(), ugnich.getUid()), equalTo(true)); int renhaId = userService.createUser("renha", "secret"); messagesService.createReply(mid, 0, renhaId, "people", null); threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); assertThat(threadPage.querySelectorAll(".msg-comment-target").isEmpty(), equalTo(true)); assertThat(threadPage.querySelectorAll(".a-thread-comment").isEmpty(), equalTo(true)); } @Test public void correctTagsEscaping() throws PebbleException, IOException { PebbleTemplate template = pebbleEngine.getTemplate("views/test"); Writer writer = new StringWriter(); template.evaluate(writer, Collections.singletonMap("tagsList", Collections.singletonList(StringEscapeUtils.escapeHtml4(new Tag(">_<").getName())))); String output = writer.toString().trim(); assertThat(output, equalTo(">_<")); } public DomElement fetchMeta(String url, String name) throws IOException { HtmlPage page = webClient.getPage(url); DomElement emptyMeta = new DomElement("", "meta", null, null); return page.getElementsByTagName("meta").stream() .filter(t -> t.getAttribute("name").equals(name)).findFirst().orElse(emptyMeta); } @Test public void testTwitterCards() throws Exception { int mid = messagesService.createMessage(ugnich.getUid(), "without image", null, null); assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid), "twitter:card") .getAttribute("content"), equalTo("summary")); int mid2 = messagesService.createMessage(ugnich.getUid(), "with image", "png", null); Message message = messagesService.getMessage(mid2); assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid2), "twitter:card") .getAttribute("content"), equalTo("summary_large_image")); assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid2), "og:description") .getAttribute("content"), startsWith(StringEscapeUtils.escapeHtml4(MessageUtils.getMessageHashTags(message)))); } @Test public void postMessageTests() throws Exception { mockMvc.perform(post("/post").param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); MvcResult loginResult = mockMvc.perform(post("/login") .param("username", ugnichName) .param("password", ugnichPassword)).andReturn(); mockMvc.perform(post("/post") .cookie(loginResult.getResponse().getCookies()) .param("wrong_param", "yo")).andExpect(status().isBadRequest()); mockMvc.perform(post("/post") .cookie(loginResult.getResponse().getCookies()) .param("body", "yo")).andExpect(status().isOk()); mockMvc.perform(post("/post") .cookie(loginResult.getResponse().getCookies()) .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isOk()); mockMvc.perform(post("/post") .cookie(loginResult.getResponse().getCookies()) .param("img", "bad_url")).andExpect(status().isBadRequest()); FileInputStream fi = new FileInputStream(new ClassPathResource("tagscloud.png").getFile()); MockMultipartFile file = new MockMultipartFile("attach", fi); mockMvc.perform(multipart("/post") .file(file) .cookie(loginResult.getResponse().getCookies())).andExpect(status().isOk()); int mid = messagesService.createMessage(ugnich.getUid(), "dummy message", null, null); mockMvc.perform(post("/comment") .param("mid", String.valueOf(mid)) .param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); mockMvc.perform(post("/comment") .cookie(loginResult.getResponse().getCookies()) .param("wrong_param", "yo")).andExpect(status().isBadRequest()); mockMvc.perform(post("/comment") .cookie(loginResult.getResponse().getCookies()) .param("mid", String.valueOf(mid)) .param("wrong_param", "yo")).andExpect(status().isBadRequest()); mockMvc.perform(post("/comment") .cookie(loginResult.getResponse().getCookies()) .param("mid", String.valueOf(mid)) .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isFound()); mockMvc.perform(multipart("/comment") .file(file) .cookie(loginResult.getResponse().getCookies()) .param("mid", String.valueOf(mid))).andExpect(status().isFound()); mockMvc.perform(post("/comment") .cookie(loginResult.getResponse().getCookies()) .param("mid", String.valueOf(mid)) .param("body", "yo")).andExpect(redirectedUrl(String.format("/%s/%d#%d", ugnichName, mid, 3))); } @Test public void hashLoginShouldNotUseSession() throws Exception { String hash = userService.getHashByUID(ugnich.getUid()); MvcResult hashLoginResult = mockMvc.perform(get("/?show=my&hash=" + hash)) .andExpect(status().isOk()) .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) .andExpect(content().string(containsString(hash))) .andReturn(); Cookie rememberMeFromHash = hashLoginResult.getResponse().getCookie("juick-remember-me"); MvcResult formLoginResult = mockMvc.perform(post("/login") .param("username", ugnichName) .param("password", ugnichPassword)).andReturn(); Cookie rememberMeFromForm = formLoginResult.getResponse().getCookie("juick-remember-me"); mockMvc.perform(get("/?show=my").cookie(rememberMeFromForm)).andExpect(status().isOk()) .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) .andExpect(content().string(containsString(hash))); mockMvc.perform(get("/?show=my").cookie(rememberMeFromHash)).andExpect(status().isOk()) .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) .andExpect(content().string(containsString(hash))); } @Test public void nonExistentBlogShouldReturn404() throws Exception { mockMvc.perform(get("/ololoe/")).andExpect(status().isNotFound()); } }