/* * 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 <http://www.gnu.org/licenses/>. */ package com.juick; 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.service.*; import com.juick.util.MessageUtils; import com.juick.www.Utils; import com.juick.www.WebApp; 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.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.FileSystemUtils; 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.List; import java.util.stream.Collectors; 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.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * Created by vitalyster on 12.01.2017. */ @RunWith(SpringRunner.class) @AutoConfigureMockMvc @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { Application.class}) @TestPropertySource(properties = {"ios_app_id=12345678.com.juick.ExampleApp"}) public class WebAppTests { @MockBean private ImagesService imagesService; @Inject private WebApp webApp; @Inject private MockMvc mockMvc; @Inject private WebClient webClient; @Inject private UserService userService; @Inject private MessagesService messagesService; @Inject private PrivacyQueriesService privacyQueriesService; @Inject private JdbcTemplate jdbcTemplate; @Inject private SubscriptionService subscriptionService; @Inject private ApplicationEventPublisher applicationEventPublisher; @Inject private PebbleEngine pebbleEngine; @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") private String imgPath; @Value("${ios_app_id:}") private String appId; 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) { webClient.getOptions().setJavaScriptEnabled(false); webClient.getOptions().setCssEnabled(false); webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); ugnichName = "ugnich"; ugnichPassword = "secret"; freefdName = "freefd"; freefdPassword = "MyPassw0rd!"; userService.createUser(ugnichName, ugnichPassword); ugnich = userService.getUserByName(ugnichName); 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 repliesList() throws IOException { int mid = messagesService.createMessage(ugnich.getUid(), "hello", null, null); IntStream.range(1, 15).forEach(i -> messagesService.createReply(mid, i-1, freefd, 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(14L)); } @Test public void userShouldNotSeeReplyButtonToBannedUser() throws Exception { int mid = messagesService.createMessage(ugnich.getUid(), "freefd bl me", null, null); messagesService.createReply(mid, 0, ugnich, "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, userService.getUserByUID(renhaId).orElseThrow(IllegalStateException::new), "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("<a href=\"/ugnich/?tag=%26gt%3B_%26lt%3B\">>_<</a>")); } 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("/post2").param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); MvcResult loginResult = mockMvc.perform(post("/login") .param("username", ugnichName) .param("password", ugnichPassword)).andReturn(); String msgText = "yoppppl"; mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) .param("body", msgText)).andExpect(status().isFound()); Message lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); assertThat(lastMessage.getText(), equalTo(msgText)); mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isFound()); lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); assertThat(lastMessage.getAttachmentType(), equalTo("png")); mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) .param("img", "bad_url")).andExpect(status().isBadRequest()); FileInputStream fi = new FileInputStream(new ClassPathResource("static/tagscloud.png").getFile()); MockMultipartFile file = new MockMultipartFile("attach", fi); mockMvc.perform(multipart("/post2") .file(file) .cookie(loginResult.getResponse().getCookies())).andExpect(status().isFound()); 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))); mockMvc.perform(post("/post2") .cookie(loginResult.getResponse().getCookies()) .param("body", String.format("D #%d/%d", mid, 3))) .andExpect(status().isFound()); assertThat(messagesService.getReplies(ugnich, mid).size(), equalTo(2)); } @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()); } @Test public void discussionsShouldBePageableByTimestamp() throws Exception { String msgText = "Привет, я снова Угнич"; int mid = messagesService.createMessage(ugnich.getUid(), msgText, null, null); int midNew = messagesService.createMessage(ugnich.getUid(), "Я более новый Угнич", null, 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())); String discussionsUrl = "http://localhost:8080/?show=discuss"; HtmlPage discussions = webClient.getPage(discussionsUrl); assertThat(discussions.querySelectorAll("article").size(), is(0)); subscriptionService.subscribeMessage(messagesService.getMessage(mid), freefd); discussions = (HtmlPage) discussions.refresh(); assertThat(discussions.querySelectorAll("article").size(), is(1)); subscriptionService.subscribeMessage(messagesService.getMessage(midNew), freefd); discussions = (HtmlPage) discussions.refresh(); assertThat(discussions.querySelectorAll("article").size(), is(2)); assertThat(discussions.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); messagesService.createReply(mid, 0, freefd, "I'm replied", null); discussions = (HtmlPage) discussions.refresh(); assertThat(discussions.querySelectorAll("article").size(), is(2)); assertThat(discussions.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); Message msg = messagesService.getMessage(mid); HtmlPage discussionsOld = webClient.getPage(discussionsUrl + "&to=" + msg.getUpdated().toEpochMilli()); assertThat(discussionsOld.querySelectorAll("article").size(), is(1)); assertThat(discussionsOld.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); List<Integer> newMids = IntStream.rangeClosed(1, 19).map(i -> messagesService.createMessage(ugnich.getUid(), String.valueOf(i), null, null)).boxed().collect(Collectors.toList()); for (Integer m : newMids) { subscriptionService.subscribeMessage(messagesService.getMessage(m), freefd); } discussions = (HtmlPage) discussions.refresh(); assertThat(discussions.querySelectorAll("article").size(), is(20)); assertThat(discussions.querySelectorAll("article") .get(19).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); messagesService.createReply(midNew, 0, freefd, "I'm replied", null); discussions = (HtmlPage) discussions.refresh(); assertThat(discussions.querySelectorAll("article") .get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); Message old = messagesService.getMessage(newMids.get(0)); discussionsOld = webClient.getPage(discussionsUrl + "&to=" + old.getUpdated().toEpochMilli()); assertThat(discussionsOld.querySelectorAll("article").size(), is(1)); assertThat(discussionsOld.querySelectorAll("article") .get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); } @Test public void redirectParamShouldCorrectlyRedirectLoggedUser() throws Exception { MvcResult formLoginResult = mockMvc.perform(post("/login") .param("username", ugnichName) .param("password", ugnichPassword)).andReturn(); Cookie rememberMeFromForm = formLoginResult.getResponse().getCookie("juick-remember-me"); mockMvc.perform(get("/login").cookie(rememberMeFromForm)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/")); mockMvc.perform(get("/login?redirect=false").cookie(rememberMeFromForm)) .andExpect(status().is3xxRedirection()) .andExpect(redirectedUrl("/login/success")); } @Test public void anythingRedirects() throws Exception { int mid = messagesService.createMessage(ugnich.getUid(), "yo", null, null); mockMvc.perform(get(String.format("/%d", mid))) .andExpect(status().isMovedPermanently()) .andExpect(redirectedUrl(String.format("/%s/%d", ugnich.getName(), mid))); } @Test public void appAssociationsTest() throws Exception { mockMvc.perform((get("/.well-known/apple-app-site-association"))) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.webcredentials.apps[0]", is(appId))); } @Test public void notificationsTests() throws Exception { 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())); int mid = messagesService.createMessage(ugnich.getUid(), "new test", null, null); subscriptionService.subscribeMessage(messagesService.getMessage(mid), freefd); int rid = messagesService.createReply(mid, 0, ugnich, "new reply", null); HtmlPage discussionsPage = webClient.getPage("http://localhost:8080/?show=discuss"); assertThat(discussionsPage.querySelectorAll("#global a .badge").size(), is(1)); HtmlPage unreadThread = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); assertThat(unreadThread.querySelectorAll("#global a .badge").size(), is(0)); } @Test public void escapeSqlTests() { String sql = String.format("SELECT * FROM table WHERE data='%s'", Utils.encodeSphinx("';-- DROP TABLE table")); assertThat(sql, is("SELECT * FROM table WHERE data='\\';-- DROP TABLE table\'")); } }