/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.controller;
import alfio.TestConfiguration;
import alfio.config.DataSourceConfiguration;
import alfio.config.Initializer;
import alfio.config.RepositoryConfiguration;
import alfio.controller.api.ReservationApiController;
import alfio.controller.api.admin.CheckInApiController;
import alfio.controller.api.admin.EventApiController;
import alfio.controller.api.support.TicketHelper;
import alfio.controller.form.PaymentForm;
import alfio.controller.form.ReservationForm;
import alfio.controller.form.UpdateTicketOwnerForm;
import alfio.controller.support.TicketDecorator;
import alfio.manager.EventManager;
import alfio.manager.EventStatisticsManager;
import alfio.manager.i18n.I18nManager;
import alfio.manager.support.CheckInStatus;
import alfio.manager.support.TicketAndCheckInResult;
import alfio.manager.user.UserManager;
import alfio.model.Event;
import alfio.model.TicketCategory;
import alfio.model.audit.ScanAudit;
import alfio.model.modification.DateTimeModification;
import alfio.model.modification.TicketCategoryModification;
import alfio.model.modification.TicketReservationModification;
import alfio.model.transaction.PaymentProxy;
import alfio.repository.TicketReservationRepository;
import alfio.repository.audit.ScanAuditRepository;
import alfio.repository.system.ConfigurationRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.test.util.IntegrationTestUtil;
import alfio.util.TemplateManager;
import com.opencsv.CSVReader;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.security.Principal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static alfio.test.util.IntegrationTestUtil.*;
import static org.junit.Assert.*;
/**
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RepositoryConfiguration.class, DataSourceConfiguration.class, TestConfiguration.class, ReservationFlowIntegrationTest.ControllerConfiguration.class})
@ActiveProfiles({Initializer.PROFILE_DEV, Initializer.PROFILE_DISABLE_JOBS})
@Transactional
public class ReservationFlowIntegrationTest {
@Configuration
@ComponentScan(basePackages = {"alfio.controller"})
public static class ControllerConfiguration {
}
private static final Map<String, String> DESCRIPTION = Collections.singletonMap("en", "desc");
@Autowired
private ConfigurationRepository configurationRepository;
@Autowired
private EventStatisticsManager eventStatisticsManager;
@Autowired
private OrganizationRepository organizationRepository;
@Autowired
private UserManager userManager;
@Autowired
private EventManager eventManager;
@Autowired
private EventController eventController;
@Autowired
private ReservationController reservationController;
@Autowired
private TicketController ticketController;
@Autowired
private EventApiController eventApiController;
@Autowired
private CheckInApiController checkInApiController;
@Autowired
private TicketHelper ticketHelper;
@Autowired
private I18nManager i18nManager;
@Autowired
private TicketReservationRepository ticketReservationRepository;
@Autowired
private ScanAuditRepository scanAuditRepository;
private ReservationApiController reservationApiController;
private Event event;
private String user;
@BeforeClass
public static void initEnv() {
initSystemProperties();
}
@Before
public void ensureConfiguration() {
IntegrationTestUtil.ensureMinimalConfiguration(configurationRepository);
List<TicketCategoryModification> categories = Collections.singletonList(
new TicketCategoryModification(null, "default", AVAILABLE_SEATS,
new DateTimeModification(LocalDate.now().minusDays(1), LocalTime.now()),
new DateTimeModification(LocalDate.now().plusDays(1), LocalTime.now()),
DESCRIPTION, BigDecimal.TEN, false, "", false));
Pair<Event, String> eventAndUser = initEvent(categories, organizationRepository, userManager, eventManager);
event = eventAndUser.getKey();
user = eventAndUser.getValue() + "_owner";
//
TemplateManager templateManager = Mockito.mock(TemplateManager.class);
reservationApiController = new ReservationApiController(ticketHelper, templateManager, i18nManager);
}
/**
* Test a complete offline payment flow.
* Will not check in detail...
*/
@Test
public void reservationFlowTest() throws Exception{
String eventName = event.getShortName();
eventManager.toggleActiveFlag(event.getId(), user, true);
// list events
String eventList = eventController.listEvents(new BindingAwareModelMap(), Locale.ENGLISH);
if(eventManager.getActiveEvents().size() == 1) {
Assert.assertTrue(eventList.startsWith("redirect:/"));
} else {
assertEquals("/event/event-list", eventList);
}
//
// show event
String showEvent = eventController.showEvent(eventName, new BindingAwareModelMap(), new MockHttpServletRequest(), Locale.ENGLISH);
assertEquals("/event/show-event", showEvent);
//
// check calendar
checkCalendar(eventName);
//
String redirectResult = reserveTicket(eventName);
String redirectStart = "redirect:/event/" + eventName + "/reservation/";
// check reservation success
Assert.assertTrue(redirectResult.startsWith(redirectStart));
Assert.assertTrue(redirectResult.endsWith("/book"));
//
String reservationIdentifier = redirectResult.substring(redirectStart.length()).replace("/book", "");
// check that the payment page is shown
String reservationPage = reservationController.showPaymentPage(eventName, reservationIdentifier, null, null, null, null, null, null, null, null, null, null, null, new BindingAwareModelMap(), Locale.ENGLISH);
assertEquals("/event/reservation-page", reservationPage);
//
// pay offline
String successPage = payOffline(eventName, reservationIdentifier);
assertEquals("redirect:/event/" + eventName + "/reservation/" + reservationIdentifier + "/success", successPage);
//
//go to success page, payment is still pending
String confirmationPage = reservationController.showConfirmationPage(eventName, reservationIdentifier, false, false, new BindingAwareModelMap(), Locale.ENGLISH, new MockHttpServletRequest());
Assert.assertTrue(confirmationPage.endsWith("/waitingPayment"));
assertEquals("/event/reservation-waiting-for-payment", reservationController.showWaitingPaymentPage(eventName, reservationIdentifier, new BindingAwareModelMap(), Locale.ENGLISH));
//
validatePayment(eventName, reservationIdentifier);
//
Assert.assertTrue(reservationController.showWaitingPaymentPage(eventName, reservationIdentifier, new BindingAwareModelMap(), Locale.ENGLISH).endsWith("/success"));
//
TicketDecorator ticketDecorator = checkReservationComplete(eventName, reservationIdentifier);
//
String ticketIdentifier = ticketDecorator.getUuid();
//ticket is still not assigned, will redirect
Assert.assertTrue(ticketController.showTicket(eventName, ticketIdentifier, false, Locale.ENGLISH, new BindingAwareModelMap()).startsWith("redirect:/event/"));
Assert.assertTrue(ticketController.showTicketForUpdate(eventName, ticketIdentifier, new BindingAwareModelMap(), Locale.ENGLISH).startsWith("redirect:/event/"));
//
String fname1 = "Test";
String lname1 = "McTest";
//assign ticket to person
assignTicket(eventName, reservationIdentifier, ticketIdentifier, fname1, lname1);
assertEquals("/event/update-ticket", ticketController.showTicketForUpdate(eventName, ticketIdentifier, new BindingAwareModelMap(), Locale.ENGLISH));
//
assertEquals("/event/show-ticket", ticketController.showTicket(eventName, ticketIdentifier, false, Locale.ENGLISH, new BindingAwareModelMap()));
//
checkCSV(eventName, ticketIdentifier, fname1 + " " + lname1);
// use api to update
UpdateTicketOwnerForm updateTicketOwnerForm = new UpdateTicketOwnerForm();
updateTicketOwnerForm.setFirstName("Test");
updateTicketOwnerForm.setLastName("Testson");
updateTicketOwnerForm.setEmail("testmctest@test.com");
updateTicketOwnerForm.setUserLanguage("en");
reservationApiController.assignTicketToPerson(eventName, ticketIdentifier, true,
updateTicketOwnerForm, new BeanPropertyBindingResult(updateTicketOwnerForm, "updateTicketForm"), new MockHttpServletRequest(), new BindingAwareModelMap(),
null);
checkCSV(eventName, ticketIdentifier, "Test Testson");
//
//update
String fname2 = "Test";
String lname2 = "OTest";
assignTicket(eventName, reservationIdentifier, ticketIdentifier, fname2, lname2);
checkCSV(eventName, ticketIdentifier, fname2 + " " + lname2);
//lock ticket
Principal principal = Mockito.mock(Principal.class);
Mockito.when(principal.getName()).thenReturn(user);
eventApiController.toggleTicketLocking(eventName, ticketDecorator.getCategoryId(), ticketDecorator.getId(), principal);
assignTicket(eventName, reservationIdentifier, ticketIdentifier, fname1, fname2);
checkCSV(eventName, ticketIdentifier, fname2 + " " + lname2);
//ticket has changed, update
ticketDecorator = checkReservationComplete(eventName, reservationIdentifier);
//--- check in sequence
String ticketCode = ticketDecorator.ticketCode(event.getPrivateKey());
TicketAndCheckInResult ticketAndCheckInResult = checkInApiController.findTicketWithUUID(event.getId(), ticketIdentifier, ticketCode);
assertEquals(CheckInStatus.OK_READY_TO_BE_CHECKED_IN, ticketAndCheckInResult.getResult().getStatus());
CheckInApiController.TicketCode tc = new CheckInApiController.TicketCode();
tc.setCode(ticketCode);
assertEquals(CheckInStatus.SUCCESS, checkInApiController.checkIn(event.getId(), ticketIdentifier, tc, new TestingAuthenticationToken("ciccio","ciccio")).getResult().getStatus());
List<ScanAudit> audits = scanAuditRepository.findAllForEvent(event.getId());
assertFalse(audits.isEmpty());
assertTrue(audits.stream().anyMatch(sa -> sa.getTicketUuid().equals(ticketIdentifier)));
TicketAndCheckInResult ticketAndCheckInResultOk = checkInApiController.findTicketWithUUID(event.getId(), ticketIdentifier, ticketCode);
assertEquals(CheckInStatus.ALREADY_CHECK_IN, ticketAndCheckInResultOk.getResult().getStatus());
//
eventManager.deleteEvent(event.getId(), principal.getName());
}
private void checkCalendar(String eventName) throws IOException {
MockHttpServletResponse resIcal = new MockHttpServletResponse();
eventController.calendar(eventName, "en", null, resIcal);
assertEquals("text/calendar", resIcal.getContentType());
MockHttpServletResponse resGoogleCal = new MockHttpServletResponse();
eventController.calendar(eventName, "en", "google", resGoogleCal);
Assert.assertTrue(resGoogleCal.getRedirectedUrl().startsWith("https://www.google.com/calendar/event"));
}
private TicketDecorator checkReservationComplete(String eventName, String reservationIdentifier) {
Model confirmationPageModel = new BindingAwareModelMap();
String confirmationPageSuccess = reservationController.showConfirmationPage(eventName, reservationIdentifier, false, false, confirmationPageModel, Locale.ENGLISH, new MockHttpServletRequest());
assertEquals("/event/reservation-page-complete", confirmationPageSuccess);
List<Pair<?, List<TicketDecorator>>> tickets = (List<Pair<?, List<TicketDecorator>>>) confirmationPageModel.asMap().get("ticketsByCategory");
assertEquals(1, tickets.size());
assertEquals(1, tickets.get(0).getRight().size());
return tickets.get(0).getRight().get(0);
}
private void assignTicket(String eventName, String reservationIdentifier, String ticketIdentifier, String firstName, String lastName) throws Exception {
UpdateTicketOwnerForm ticketOwnerForm = new UpdateTicketOwnerForm();
ticketOwnerForm.setFirstName(firstName);
ticketOwnerForm.setLastName(lastName);
ticketOwnerForm.setEmail("testmctest@test.com");
ticketOwnerForm.setUserLanguage("en");
Assert.assertTrue(reservationController.assignTicketToPerson(eventName, reservationIdentifier, ticketIdentifier, ticketOwnerForm, Mockito.mock(BindingResult.class), new MockHttpServletRequest(), new BindingAwareModelMap()).endsWith("/success"));
}
private void checkCSV(String eventName, String ticketIdentifier, String fullName) throws IOException {
//FIXME get all fields :D and put it in the request...
Principal principal = Mockito.mock(Principal.class);
Mockito.when(principal.getName()).thenReturn(user);
MockHttpServletResponse response = new MockHttpServletResponse();
List<String> fields = eventApiController.getAllFields(eventName);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("fields", fields.toArray(new String[]{}));
eventApiController.downloadAllTicketsCSV(eventName, request, response, principal);
CSVReader csvReader = new CSVReader(new StringReader(response.getContentAsString()));
List<String[]> csv = csvReader.readAll();
assertEquals(2, csv.size());
assertEquals(ticketIdentifier, csv.get(1)[0]);
assertEquals("default", csv.get(1)[2]);
assertEquals("ACQUIRED", csv.get(1)[4]);
assertEquals(fullName, csv.get(1)[10]);
}
private void validatePayment(String eventName, String reservationIdentifier) {
Principal principal = Mockito.mock(Principal.class);
Mockito.when(principal.getName()).thenReturn(user);
assertEquals(1, eventApiController.getPendingPayments(eventName, principal).size());
assertEquals("OK", eventApiController.confirmPayment(eventName, reservationIdentifier, principal, new BindingAwareModelMap(), new MockHttpServletRequest()));
assertEquals(0, eventApiController.getPendingPayments(eventName, principal).size());
}
private String payOffline(String eventName, String reservationIdentifier) {
PaymentForm paymentForm = new PaymentForm();
paymentForm.setPaymentMethod(PaymentProxy.OFFLINE);
paymentForm.setEmail("test@test.com");
paymentForm.setBillingAddress("my billing address");
paymentForm.setFirstName("full");
paymentForm.setLastName("name");
paymentForm.setTermAndConditionsAccepted(true);
paymentForm.setPostponeAssignment(true);
BindingResult bindingResult = new BeanPropertyBindingResult(paymentForm, "paymentForm");
Model model = new BindingAwareModelMap();
MockHttpServletRequest request = new MockHttpServletRequest();
RedirectAttributes redirectAttributes = new RedirectAttributesModelMap();
return reservationController.handleReservation(eventName, reservationIdentifier, paymentForm, bindingResult, model, request, Locale.ENGLISH, redirectAttributes);
}
private String reserveTicket(String eventName) {
ReservationForm reservationForm = new ReservationForm();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("POST");
ServletWebRequest servletWebRequest = new ServletWebRequest(request);
BindingResult bindingResult = new BeanPropertyBindingResult(reservationForm, "reservation");
Model model = new BindingAwareModelMap();
RedirectAttributes redirectAttributes = new RedirectAttributesModelMap();
TicketReservationModification ticketReservation = new TicketReservationModification();
ticketReservation.setAmount(1);
ticketReservation.setTicketCategoryId(eventStatisticsManager.loadTicketCategories(event).stream().findFirst().map(TicketCategory::getId).orElseThrow(IllegalStateException::new));
reservationForm.setReservation(Collections.singletonList(ticketReservation));
return eventController.reserveTicket(eventName, reservationForm, bindingResult, model, servletWebRequest, redirectAttributes, Locale.ENGLISH);
}
}