/* * Copyright 2016-2017 EuregJUG. * * 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 eu.euregjug.site.web; import com.github.mkopylec.recaptcha.validation.RecaptchaValidator; import com.github.mkopylec.recaptcha.validation.ValidationResult; import eu.euregjug.site.config.MailChimpConfig; import eu.euregjug.site.events.EventEntity; import eu.euregjug.site.events.EventRepository; import eu.euregjug.site.events.Registration; import eu.euregjug.site.events.RegistrationEntity; import eu.euregjug.site.events.RegistrationService; import eu.euregjug.site.links.LinkEntity; import eu.euregjug.site.links.LinkRepository; import eu.euregjug.site.posts.PostEntity; import eu.euregjug.site.posts.PostEntity.Status; import eu.euregjug.site.posts.PostRenderingService; import eu.euregjug.site.posts.PostRepository; import static eu.euregjug.site.web.EventsIcalView.ICS_LINEBREAK; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import javax.servlet.http.HttpServletRequest; import static org.hamcrest.CoreMatchers.containsString; import org.joor.Reflect; import org.junit.Test; import org.junit.runner.RunWith; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; /** * * @author Michael J. Simons, 2016-07-17 */ @RunWith(SpringRunner.class) @ActiveProfiles("test") @WebMvcTest( controllers = IndexController.class, secure = false, // Need the custom view component and config as well... includeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = { EventsIcalView.class, IndexRssView.class, MailChimpConfig.class, PostRenderingService.class // PostRenderService cannot be mocked (again: @Cacheable) } ) ) @ImportAutoConfiguration(MessageSourceAutoConfiguration.class) @EnableSpringDataWebSupport // Needed to enable resolving of Pageable and other parameters public class IndexControllerTest { /** * Makes the tests locale aware. */ @TestConfiguration static class Config { @Bean public LocaleResolver localeResolver() { return new AcceptHeaderLocaleResolver(); } } @Autowired private MockMvc mvc; @MockBean private EventRepository eventRepository; @MockBean private PostRepository postRepository; @MockBean private LinkRepository linkRepository; @MockBean private RecaptchaValidator recaptchaValidator; @MockBean private RegistrationService registrationService; private final List<EventEntity> events; private final List<PostEntity> posts; private final List<LinkEntity> links; public IndexControllerTest() { final ZonedDateTime eventDate = ZonedDateTime.of(2016, 7, 7, 19, 0, 0, 0, ZoneId.of("Europe/Berlin")); final EventEntity event1 = Reflect.on( new EventEntity(GregorianCalendar.from(eventDate), "name-1", "desc-1") ).set("id", 23).set("createdAt", GregorianCalendar.from(eventDate)).get(); event1.setDuration(60); event1.setSpeaker("Farin Urlaub"); event1.setLocation("Am Strand\n4223 Schlaraffenland\n\nirgendwo"); final ZonedDateTime eventDate2 = ZonedDateTime.of(2016, 11, 22, 18, 0, 0, 0, ZoneId.of("Europe/Berlin")); final EventEntity event2 = Reflect.on( new EventEntity(GregorianCalendar.from(eventDate2), "name-2", "desc-2") ).set("id", 42).set("createdAt", GregorianCalendar.from(eventDate2)).get(); this.events = Arrays.asList(event1, event2); this.posts = new ArrayList<>(); final PostEntity post = Reflect.on(new PostEntity(Date.from(LocalDate.of(2016, 8, 5).atStartOfDay(ZoneId.systemDefault()).toInstant()), "foo", "foo", "foo")).set("id", 2).get(); post.setStatus(Status.published); this.posts.add(post); this.posts.add(Reflect.on(new PostEntity(Date.from(LocalDate.of(2016, 8, 4).atStartOfDay(ZoneId.systemDefault()).toInstant()), "bar", "bar", "bar")).set("id", 1).get()); this.links = new ArrayList<>(); this.links.add(new LinkEntity("http://michael-simons.eu", "Michael Simons")); } @Test public void indexShouldWork() throws Exception { when(this.eventRepository.findUpcomingEvents()).thenReturn(events); when(this.linkRepository.findAllByOrderByTypeAscSortColAscTitleAsc()).thenReturn(links); final PageRequest pageRequest = new PageRequest(0, 5, Sort.Direction.DESC, "publishedOn", "createdAt"); final PageImpl<PostEntity> postsPage = new PageImpl<>(this.posts, pageRequest, 15); when(this.postRepository.findAllByStatus(Status.published, pageRequest)).thenReturn(postsPage); final Map<LinkEntity.Type, List<LinkEntity>> links = new HashMap<>(); links.put(LinkEntity.Type.generic, this.links); this.mvc .perform(get("http://euregjug.eu")) .andExpect(status().isOk()) .andExpect(view().name("index")) .andExpect(model().attribute("upcomingEvents", events)) .andExpect(model().attribute("links", links)) .andExpect(model().attributeExists("posts")); } @Test public void postShouldHandleInvalidDate() throws Exception { this.mvc .perform( get("http://euregjug.eu/{year}/{month}/{day}/{slug}", 2016, 2, 30, "test") ) .andExpect(status().isFound()) .andExpect(view().name("redirect:/")); } @Test public void eventsShouldWork() throws Exception { when(this.eventRepository.findUpcomingEvents()).thenReturn(events); this.mvc .perform(get("http://euregjug.eu/events.ics").accept("text/calendar")) .andExpect(status().isOk()) .andExpect(content().contentType("text/calendar")) .andExpect(content().string("" + "BEGIN:VCALENDAR" + ICS_LINEBREAK + "VERSION:2.0" + ICS_LINEBREAK + "PRODID:http://www.euregjug.eu/events" + ICS_LINEBREAK + "BEGIN:VEVENT" + ICS_LINEBREAK + "UID:23@euregjug.eu" + ICS_LINEBREAK + "ORGANIZER:EuregJUG" + ICS_LINEBREAK + "DTSTAMP:20160707T170000Z" + ICS_LINEBREAK + "DTSTART:20160707T170000Z" + ICS_LINEBREAK + "DTEND:20160707T180000Z" + ICS_LINEBREAK + "SUMMARY:name-1 (Farin Urlaub)" + ICS_LINEBREAK + "DESCRIPTION:desc-1" + ICS_LINEBREAK + "URL:http://euregjug.eu/register/23" + ICS_LINEBREAK + "LOCATION: Am Strand, 4223 Schlaraffenland, irgendwo" + ICS_LINEBREAK + "END:VEVENT" + ICS_LINEBREAK + "BEGIN:VEVENT" + ICS_LINEBREAK + "UID:42@euregjug.eu" + ICS_LINEBREAK + "ORGANIZER:EuregJUG" + ICS_LINEBREAK + "DTSTAMP:20161122T170000Z" + ICS_LINEBREAK + "DTSTART:20161122T170000Z" + ICS_LINEBREAK + "DTEND:20161122T190000Z" + ICS_LINEBREAK + "SUMMARY:name-2" + ICS_LINEBREAK + "DESCRIPTION:desc-2" + ICS_LINEBREAK + "URL:http://euregjug.eu/register/42" + ICS_LINEBREAK + "END:VEVENT" + ICS_LINEBREAK + "END:VCALENDAR" + ICS_LINEBREAK)); verify(this.eventRepository).findUpcomingEvents(); verifyNoMoreInteractions(this.eventRepository); } @Test public void registerFormShouldWork() throws Exception { when(this.eventRepository.findOne(23)).thenReturn(Optional.of(this.events.get(0))); this.mvc .perform(get("http://euregjug.eu/register/{eventId}", 23)) .andExpect(status().isOk()) .andExpect(view().name("register")) .andExpect(model().attribute("registered", false)) .andExpect(model().attribute("event", this.events.get(0))) .andExpect(model().attributeExists("registration")); verify(this.eventRepository).findOne(23); verifyNoMoreInteractions(this.eventRepository); } @Test public void registerFormShouldHandleInvalidEvent() throws Exception { when(this.eventRepository.findOne(23)).thenReturn(Optional.empty()); this.mvc .perform(get("http://euregjug.eu/register/{eventId}", 23)) .andExpect(status().isFound()) .andExpect(view().name("redirect:/")); verify(this.eventRepository).findOne(23); verifyNoMoreInteractions(this.eventRepository); } @Test public void feedShouldWork() throws Exception { when(this.eventRepository.findUpcomingEvents()).thenReturn(events); final PageRequest pageRequest = new PageRequest(1, 5, Sort.Direction.DESC, "publishedOn", "createdAt"); final PageImpl<PostEntity> postsPage = new PageImpl<>(this.posts, pageRequest, 15); when(this.postRepository.findAllByStatus(Status.published, pageRequest)).thenReturn(postsPage); when(this.linkRepository.findAllByOrderByTypeAscSortColAscTitleAsc()).thenReturn(new ArrayList<>()); final ZoneId zoneUtc = ZoneId.of("UTC"); final SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US); final String date1 = df.format(this.posts.get(0).getPublishedOn()); final String date2 = df.format(this.posts.get(1).getPublishedOn()); this.mvc .perform( get("http://euregjug.eu/feed.rss") .param("page", "1") .locale(Locale.ENGLISH) .accept("application/rss+xml")) .andExpect(xpath("/rss/channel/title").string("EuregJUG Maas-Rhine - All things JVM!")) .andExpect(xpath("/rss/channel/link").string("http://euregjug.eu")) .andExpect(xpath("/rss/channel/description").string("RSS Feed from EuregJUG, the Java User Group for the Euregio Maas-Rhine (Aachen, Maastricht, Liege).")) .andExpect(xpath("/rss/channel/pubDate").string(date1)) .andExpect(xpath("/rss/channel/lastBuildDate").string(date1)) .andExpect(xpath("/rss/channel/generator").string("https://github.com/EuregJUG-Maas-Rhine/site")) .andExpect(xpath("/rss/channel/*[local-name() = 'link' and @rel='previous']/@href").string("http://euregjug.eu/feed.rss?page=0")) .andExpect(xpath("/rss/channel/*[local-name() = 'link' and @rel='self']/@href").string("http://euregjug.eu/feed.rss?page=1")) .andExpect(xpath("/rss/channel/*[local-name() = 'link' and @rel='next']/@href").string("http://euregjug.eu/feed.rss?page=2")) .andExpect(xpath("/rss/channel/*[local-name() = 'link' and @rel='next']/@href").string("http://euregjug.eu/feed.rss?page=2")) .andExpect(xpath("/rss/channel/item").nodeCount(2)) .andExpect(xpath("/rss/channel/item[2]/title").string("bar")) .andExpect(xpath("/rss/channel/item[2]/link").string("http://euregjug.eu/2016/8/4/bar")) .andExpect(xpath("/rss/channel/item[2]/*[local-name() = 'encoded']").string(containsString("bar"))) .andExpect(xpath("/rss/channel/item[2]/pubDate").string(date2)) .andExpect(xpath("/rss/channel/item[2]/author").string("euregjug.eu")) .andExpect(xpath("/rss/channel/item[2]/guid").string("http://euregjug.eu/2016/8/4/bar")) .andExpect(status().isOk()); } @Test public void registerShouldHandleInvalidData() throws Exception { when(this.eventRepository.findOne(23)).thenReturn(Optional.of(this.events.get(0))); this.mvc.perform(post("/register/{eventId}", 23)) .andExpect(status().isOk()) .andExpect(view().name("register")) .andExpect(model().attribute("registered", false)) .andExpect(model().attribute("event", this.events.get(0))) .andExpect(model().attribute("alerts", Arrays.asList("invalidRegistration"))) .andExpect(model().attributeExists("registration")); } @Test public void registerShouldHandleInvalidRecaptca() throws Exception { when(this.eventRepository.findOne(23)).thenReturn(Optional.of(this.events.get(0))); when(this.recaptchaValidator.validate(any(HttpServletRequest.class))).thenReturn(new ValidationResult(false, new ArrayList<>())); this.mvc.perform( post("/register/{eventId}", 23) .param("firstName", "Michael") .param("name", "Simons") .param("email", "michael@euregjug.eu") ) .andExpect(status().isOk()) .andExpect(view().name("register")) .andExpect(model().attribute("registered", false)) .andExpect(model().attribute("event", this.events.get(0))) .andExpect(model().attribute("alerts", Arrays.asList("invalidRegistration"))) .andExpect(model().attributeExists("registration")); } @Test public void registerShouldHandleInvalidRegistration() throws Exception { when(this.eventRepository.findOne(23)).thenReturn(Optional.of(this.events.get(0))); when(this.recaptchaValidator.validate(any(HttpServletRequest.class))).thenReturn(new ValidationResult(true, new ArrayList<>())); when(this.registrationService.register(eq(23), any(Registration.class))).thenThrow(new RegistrationService.InvalidRegistrationException("broken", "broken")); this.mvc.perform( post("/register/{eventId}", 23) .param("firstName", "Michael") .param("name", "Simons") .param("email", "michael@euregjug.eu") ) .andExpect(status().isOk()) .andExpect(view().name("register")) .andExpect(model().attribute("registered", false)) .andExpect(model().attribute("event", this.events.get(0))) .andExpect(model().attribute("alerts", Arrays.asList("broken"))) .andExpect(model().attributeExists("registration")); } @Test public void registerShouldWork() throws Exception { final RegistrationEntity registrationEntity = new RegistrationEntity(this.events.get(0), "michael@euregjug.eu", "Simons", "Michael", false); when(this.recaptchaValidator.validate(any(HttpServletRequest.class))).thenReturn(new ValidationResult(true, new ArrayList<>())); when(this.registrationService.register(eq(23), any(Registration.class))).thenReturn(registrationEntity); this.mvc.perform( post("/register/{eventId}", 23) .locale(Locale.GERMAN) .param("firstName", "Michael") .param("name", "Simons") .param("email", "michael@euregjug.eu") ) .andExpect(status().isFound()) .andExpect(view().name("redirect:/register/23")) .andExpect(flash().attribute("registered", true)) .andExpect(flash().attribute("event", this.events.get(0))) .andExpect(flash().attribute("alerts", Arrays.asList("registered"))); verify(this.registrationService).register(eq(23), any(Registration.class)); verify(this.registrationService).sendConfirmationMail(registrationEntity, Locale.GERMAN); verifyNoMoreInteractions(this.eventRepository, this.registrationService); } @Test public void shouldHandleInvalidPost() throws Exception { final Date postDate = Date.from(LocalDate.of(2017, 1, 1).atStartOfDay(ZoneId.systemDefault()).toInstant()); when(this.postRepository.findByPublishedOnAndSlug(postDate, "foo")).thenReturn(Optional.empty()); this.mvc.perform( get("/2017/1/1/foo") ) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isFound()) .andExpect(view().name("redirect:/")); verify(this.postRepository).findByPublishedOnAndSlug(postDate, "foo"); verifyNoMoreInteractions(this.postRepository); } @Test public void shouldHandleInvalidDate() throws Exception { this.mvc.perform( get("/2017/1/32/foo") ) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isFound()) .andExpect(view().name("redirect:/")); verifyZeroInteractions(this.postRepository); } @Test public void shouldDisplayPost() throws Exception { final Date postDate = Date.from(LocalDate.of(2017, 1, 1).atStartOfDay(ZoneId.systemDefault()).toInstant()); when(this.postRepository.findByPublishedOnAndSlug(postDate, "foo")).thenReturn(Optional.of(this.posts.get(0))); final Optional<PostEntity> previousPost = Optional.of(this.posts.get(1)); when(this.postRepository.getPrevious(this.posts.get(0))).thenReturn(previousPost); when(this.postRepository.getNext(this.posts.get(0))).thenReturn(Optional.empty()); this.mvc.perform( get("/2017/1/1/foo") ) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk()) .andExpect(view().name("post")) .andExpect(model().attribute("previousPost", previousPost)) .andExpect(model().attributeExists("post")) .andExpect(model().attribute("nextPost", Optional.empty())); verify(this.postRepository).findByPublishedOnAndSlug(postDate, "foo"); verify(this.postRepository).getPrevious(this.posts.get(0)); verify(this.postRepository).getNext(this.posts.get(0)); verifyNoMoreInteractions(this.postRepository); } @Test public void archiveShouldWork() throws Exception { when(this.postRepository.findAll(any(Sort.class))).thenReturn(this.posts); this.mvc.perform( get("/archive") ) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk()) .andExpect(view().name("archive")) .andExpect(model().attributeExists("posts")); verify(this.postRepository).findAll(any(Sort.class)); verifyNoMoreInteractions(this.postRepository); } }